当前位置: 首页 > news >正文

mmap的调用层级与内核态陷入全过程

目录

一、用户态的三层封装

(1)第应用层发起 mmap 请求

(2)C 标准库封装:衔接用户态与内核

(3)syscall 指令:用户态→内核态的 “跳板”

二、内核入口:系统调用的 “接待处”

(一)汇编入口:快速切换与准备

(二)系统调用表:“通讯录” 式的映射

三、内核态初步处理:sys_mmap 与 sys_mmap_pgoff

(一)sys_mmap:参数校验与转换

(2)sys_mmap_pgoff:页粒度处理

四、内存映射核心流程:从 do_mmap_pgoff 到 mmap_region

(一)do_mmap_pgoff:统筹调度

(二)do_mmap:地址分配与准备

(三)mmap_region:构建 vm_area_struct ,完成映射

六、总结:理解 mmap 陷入内核的意义


        对于初学者而言,我们只知道mmap是用来管理一个进程的虚拟地址空间的函数,他能像brk一样挪动堆的边界,从而获得堆空间。他是malloc的底层调用,但是他具体是如何陷入内核态的,如何获得内存的,我们并不清晰。于是本篇文章将从新手入门的角度,逐层剖析这个过程,从应用程序调用到内核深处的处理,带您看清你看清每一层的作用和它们之间的联系。

一、用户态的三层封装

(1)第应用层发起 mmap 请求

当你在应用程序中写下如下代码时,就正式开启了 mmap 的 “旅程”:

#include <sys/mman.h>
void *addr = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);

(这里的NULL其实是让内核自己帮我们选择一个地址,而非程序员手动指定虚拟地址)

        这一步,你只是向系统 “表达需求”:希望映射一段 4096 字节的内存,以私有只读方式关联文件描述符 fd 对应的文件,偏移量为 0 。但应用程序运行在用户态,没有直接操作硬件、管理内存的权限,必须借助内核的能力,于是要触发系统调用,从用户态陷入内核态。

(2)C 标准库封装:衔接用户态与内核

应用层调用的 mmap ,实际是 C 标准库(如 glibc )提供的封装函数。它的作用是:

  1. 参数校验:简单检查传入参数是否合理,比如偏移量是否符合基本规则(后续内核会做更严格校验)。
  2. 准备系统调用:把应用层参数整理成内核能识别的格式,然后通过 syscall 指令,带着 系统调用号(mmap 对应号为 9 ) ,正式向内核 “递请求” 。
// glibc 中 mmap 简化逻辑
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) {// 简单校验,如偏移量页对齐初步检查(非严格)if (offset & (PAGE_SIZE - 1)) {errno = EINVAL;return MAP_FAILED;}// 触发系统调用,传递参数return (void *)syscall(SYS_mmap, addr, length, prot, flags, fd, offset);
}

        在Linux中,用户态代码无法直接调用到内核函数,因为其两者运行在不同的权限等级,用户态为Ring3,、内核态为Ring0。因此,glibc中的mmap函数必须通过系统调用指令syscall从用户态切换成内核态,再由内核根据系统调用号执行对应的内核函数。

        可以看到在c标准库中,有一句代码很奇怪,函数名叫做syscall。它其实就是从用户态转换成内核态的接口。它的第一个参数是内核态函数的名字或者编号,后面的参数则和c标准库的参数一致。

(3)syscall 指令:用户态→内核态的 “跳板”

syscall 是 CPU 提供的特殊指令,执行它会发生关键变化:

  • 特权级切换:CPU 从用户态(特权级 3 ,权限低)切换到内核态(特权级 0 ,权限高 )。
  • 上下文保存:自动保存用户态的执行现场(比如程序计数器、寄存器值 ),方便后续返回。
  • 跳转到内核入口:按照系统初始化时设置的 “路线”,进入内核预先准备好的系统调用处理入口。

二、内核入口:系统调用的 “接待处”

(一)汇编入口:快速切换与准备

        内核通过汇编代码(如 arch/x86/entry/entry_64.S 中的 system_call )处理 syscall 触发的切换:

ENTRY(system_call)swapgs                  ; 切换 GS 寄存器,隔离用户态与内核态数据movq    %rsp, PER_CPU_VAR(rsp_scratch)  ; 保存用户栈指针movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp  ; 切到内核栈; 保存用户态寄存器到内核栈,为内核处理做准备pushq   $__USER_DSpushq   PER_CPU_VAR(rsp_scratch)pushq   %r11pushq   $__USER_CSpushq   %rcx; 根据系统调用号,找内核处理函数movq    %rax, %rdi         ; 系统调用号给 %rdicall    *sys_call_table(, %rax, 8)  

        虽然我也看不懂汇编代码,但这一步,就像 “安检 + 登记”:切换到内核专属的栈空间,保存用户态信息,然后根据 %rax 里的系统调用号(mmap 是 9 ),到 系统调用表(sys_call_table ) 里找对应的内核处理函数 sys_mmap 。

(二)系统调用表:“通讯录” 式的映射

        内核维护的 sys_call_table ,是一个函数指针数组,把每个系统调用号对应到内核具体处理函数:

// 简化示意
const sys_call_ptr_t sys_call_table[] = 
{[__NR_read] = sys_read,[__NR_write] = sys_write,[__NR_mmap] = sys_mmap,  // mmap 对应内核函数 sys_mmap// 其他系统调用...
};

        虽然这里的数组下标都是一些大写字母,但他其实是宏定义的数字,与我们平常使用的数组并无本质区别。

        执行 call *sys_call_table(, %rax, 8) ,就是根据 %rax 里的调用号(9 ),找到并调用 sys_mmap ,正式进入内核态的 mmap 处理逻辑。

三、内核态初步处理:sys_mmap 与 sys_mmap_pgoff

(一)sys_mmap:参数校验与转换

sys_mmap 是内核处理 mmap 的 “第一站”,主要做这些事:

// mm/mmap.c 简化版
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,unsigned long, fd, unsigned long, off) 
{struct file *file = NULL;int error;// 处理文件映射(非匿名映射,如 MAP_ANONYMOUS 则无文件)if (!(flags & MAP_ANONYMOUS)) {file = fget(fd);  // 通过文件描述符取 file 结构体if (!file) return -EBADF;}// 严格校验:偏移量必须是页(PAGE_SIZE ,通常 4096 )的整数倍if (off & ~PAGE_MASK) {error = -EINVAL;goto out;}// 转换偏移量为页单位,调用下一层 sys_mmap_pgofferror = sys_mmap_pgoff(addr, len, prot, flags, file, off >> PAGE_SHIFT);
out:if (file) fput(file);  // 释放文件引用return error;
}

这里主要做了以下这些事情:

1.创建管理文件的结构体struct file。

2.将文件描述结构体struct file和文件描述符fd进行关联。

3.偏移量校验,确保偏移量off是页的整数倍,如果不是整数倍就设置错误码,并返回。

4.调用sys_mmap_pgoff。把字节偏移量转换为页偏移量,即直接将off>>PAGE_SHIFT,并进行后续的处理过程

(2)sys_mmap_pgoff:页粒度处理

   我们来看看sys_mmap_pgoff具体做了什么事情:

SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,unsigned long, prot, unsigned long, flags,struct file *, file, unsigned long, pgoff) 
{struct mm_struct *mm = current->mm;  // 当前进程内存描述符unsigned long retval;// 加锁保护进程地址空间,避免并发修改down_write(&mm->mmap_sem);retval = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);up_write(&mm->mmap_sem);  return retval;
}

 (1)先通过 current->mm 拿到当前进程的 内存描述符(mm_struct ) ,它记录了进程地址空间的所有信息(比如已映射的内存区域、堆 / 栈位置等 )。

(2)然后,加锁(down_write )保护进程地址空间。(防止多线程 / 进程并发修改出问题 ),调用 do_mmap_pgoff ,进入更核心的内存映射流程。

        其中mmap_sem是mm_struct结构体中的一个信号量,用于控制对进程虚拟内存空间的并发访问,确保线程安全。down_write和up_write就是获取读写信号量的写锁和释放写锁。

四、内存映射核心流程:从 do_mmap_pgoff 到 mmap_region

   sys_mmap_pgoff 之后,会根据映射类型(文件映射、匿名巨型页映射等 ),进入不同分支,最终汇聚到 do_mmap 、mmap_region 等函数。这里以文件映射为例,拆解关键步骤:

(一)do_mmap_pgoff:统筹调度

        主要做合法性校验:长度不能为 0 ,不能超过进程地址空间最大可映射范围(TASK_SIZE - mm->mmap_base )。校验通过后,调用 do_mmap ,进入地址分配与映射的核心逻辑。

unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flags, unsigned long pgoff) 
{struct mm_struct *mm = current->mm;unsigned long retval;// 检查长度是否合法(不能为 0 ,不能超进程地址空间上限 )if (!len) return 0;if (len > TASK_SIZE - mm->mmap_base) return -ENOMEM;// 调用 do_mmap ,进一步处理retval = do_mmap(file, addr, len, prot, flags, pgoff);return retval;
}

(二)do_mmap:地址分配与准备

unsigned long do_mmap(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flags, unsigned long pgoff) 
{// 计算/分配合适的虚拟地址addr = get_unmapped_area(file, addr, len, pgoff, flags);if (IS_ERR_VALUE(addr)) return addr;// 创建虚拟内存区域(VMA )return mmap_region(mm, file, addr, len, prot, flags, pgoff);
}

这一步分两大步骤:

  1. get_unmapped_area:找一块未被映射的虚拟地址区间。如果应用层传了 addr=NULL ,内核会自动选一个合适的地址;如果传了具体地址,会检查该地址及后续区域是否可用,确保不会和已有映射冲突。
  2. mmap_region:真正创建虚拟内存区域(VMA ) ,把文件和内存的映射关系 “落地”。

(三)mmap_region:构建 vm_area_struct ,完成映射

static unsigned long mmap_region(struct mm_struct *mm, struct file *file,unsigned long addr, unsigned long len,unsigned long prot, unsigned long flags,unsigned long pgoff) 
{struct vm_area_struct *vma;unsigned long vm_flags;// 分配 VMA 结构体vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);if (!vma) return -ENOMEM;// 初始化 VMA 关键属性vma->vm_mm = mm;          // 关联进程内存描述符vma->vm_start = addr;     // 映射起始地址vma->vm_end = addr + len; // 映射结束地址// 计算内存权限标志(如可读、可写、可执行等 )vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags);vma->vm_flags = vm_flags; vma->vm_page_prot = vm_get_page_prot(prot);vma->vm_pgoff = pgoff;    // 页粒度的偏移量// 关联文件(文件映射时 )if (file) {vma->vm_file = get_file(file);vma->vm_ops = &file->f_op->mmap; // 设置文件操作方法}// 将 VMA 插入进程地址空间if (insert_vm_struct(mm, vma)) {kmem_cache_free(vm_area_cachep, vma);return -ENOMEM;}return addr;
}

这是 mmap 在内核最核心的一步,做了以下事情:

  1. 分配 VMA 结构体VMA(Virtual Memory Area )是内核管理进程虚拟内存的 “基本单元”,每个 VMA 描述一段连续的虚拟地址区间,以及它的权限、关联文件等信息。
  2. 初始化属性:把映射的地址范围、权限、文件关联(如果是文件映射 )等信息,填到 VMA 里。比如,vm_flags 会标识这段内存是可读、可写还是可执行,是否是共享映射等。
  3. 插入地址空间:通过 insert_vm_struct ,把新建的 VMA 插入到进程的 mm_struct 管理的地址空间中。这样,进程后续访问这段虚拟地址时,内核就知道该怎么处理(比如从关联文件读数据、检查权限等 )。

六、总结:理解 mmap 陷入内核的意义

        通过这样逐层拆解,能清晰看到:mmap 从用户态到内核态,是一个 “需求传递 + 权限切换 + 内核逐步处理” 的过程。每一层函数都有明确职责(校验、加锁、地址分配、构建 VMA 等 ),最终通过创建 VMA ,让进程拥有了一段 “特殊” 的虚拟内存 —— 访问它时,内核会根据 VMA 的设置,完成文件与内存的数据交换、权限检查等工作。

联系到之前的mm_struct、vm_area_struct等:

        理解这个过程,不仅能掌握 mmap 本身的原理,也能窥见 Linux 内核 “分层处理”“权限隔离” 的设计思想:用户态提出需求,内核态安全、有序地完成复杂的资源管理工作,保障系统稳定又能灵活响应用户请求。

最后值得注意的是:

        mmap必须依赖open得到的fd。即mmap只能映射已经打开的文件。我们说文件都是存放于磁盘中的,一个进程打开了该文件才会在内核数据结构中创建struct file结构体,才会存放于文件描述符表中。试想一下,一文件没有打开,内核中根本就没有fd,何来的mmap呢?

        不过有一种特殊情况,匿名映射并没有存在于硬盘的实体,不关联任何磁盘文件,不需要fd和struct file来刻画描述其文件状态。所以在使用mmap的时候传入的fd为-1.

http://www.lryc.cn/news/601152.html

相关文章:

  • 六、搭建springCloudAlibaba2021.1版本分布式微服务-admin监控中心
  • 记录一次薛定谔bug
  • 基于LNMP架构的分布式个人博客搭建
  • Java大数据面试实战:Hadoop生态与分布式计算
  • 数据权属雷区:原始数据与衍生数据的法律边界如何划清?
  • AI与区块链Web3技术融合:重塑数字经济的未来格局
  • ROS2入门到精通教程(三)快速体验
  • Linux vimgrep 详解
  • VGG 改进:融合CNN与Transformer的VGG模型
  • vmware虚拟机中显示“网络电缆被拔出“的解决方法
  • 【面板数据】中国A股上市公司制造业智能制造数据集(1992-2024年)
  • 从稀疏数据(CSV)创建非常大的 GeoTIFF(和 WMS)
  • 【温度传感器】热电偶、热敏电阻、热电阻、热成像仪原理及精度解析
  • 立式加工中心X-Y轴传动机械结构设“cad【6张】三维图+设计说明书
  • Day32| 509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯
  • 基于springboot的在线数码商城/在线电子产品商品销售系统的设计与实现
  • 06-ES6
  • Effective C++ 条款04:确定对象被使用前已先被初始化
  • 【C++】定义常量
  • HTTPS的基本理解以及加密流程
  • 基于图神经网络的星间路由与计算卸载强化学习算法设计与实现
  • C++___快速入门(上)
  • 人形机器人_双足行走动力学:弹性势能存储和步态能量回收
  • LeetCode|Day26|191. 位 1 的个数|Python刷题笔记
  • hot100-每日温度
  • MyBatis-Plus 通用 Service
  • 睡眠函数 Sleep() C语言
  • 缓存一致性:从单核到异构多核的演进之路
  • [RPA] 日期时间练习案例
  • 免费 PDF 转 Word 工具:无水印 / 支持批量转换,本地运行更安全【附工具下载】