mmap 是 Linux 提供的内存映射系统调用 ,核心作用是:将「内核中的一段内存 / 磁盘文件」,直接映射到当前进程的虚拟地址空间;进程访问这段虚拟地址,等价于直接访问映射的内核内存 / 磁盘文件,全程无内核缓冲区中转、无数据拷贝,是高性能内存操作 / 进程通信的基石。
mmap 函数原型
mmap 两大核心参数
prot= 访问权限,flags= 映射属性,这两个参数是 mmap 的灵魂,不同组合直接决定 mmap 是「文件映射」「匿名内存分配」「共享内存」还是「私有内存」,所有用法都是这两个参数的组合:
prot 访问权限(4 种基础值,可位或组合)
PROT_READ:映射区可读PROT_WRITE:映射区可写PROT_EXEC:映射区可执行(存储代码时用)PROT_NONE:映射区无任何权限(占位用)
规则:权限不能超过文件的打开权限(比如文件只读,prot 不能设 PROT_WRITE),否则 mmap 失败。
flags 映射属性
这是 mmap 最核心的参数,决定映射的「内存归属、共享特性、写时行为」,分为两大核心分类:
- 共享属性(二选一,必选)
MAP_SHARED:共享映射 → 进程对映射区的修改,会同步到内核 / 磁盘文件 / 其他进程,是「内存共享」的核心标志;MAP_PRIVATE:私有映射 → 进程对映射区的修改,仅当前进程可见,内核会触发「写时复制 (COW)」创建副本,不影响原数据;
- 内存来源(可选,组合使用)
MAP_ANONYMOUS/MAP_ANON:匿名映射 → 无磁盘文件作为映射源,内存来源是「内核物理内存」,fd 必须传 - 1,offset 传 0;MAP_FIXED:强制映射到指定的 addr 地址,不推荐(容易地址冲突);
共享看 MAP_SHARED/MAP_PRIVATE,内存来源看 MAP_ANONYMOUS
mmap 核心特性
- 映射地址天然对齐 :mmap 返回的虚拟地址,必然是 4KB 物理页面对齐(Linux 最小内存页单位);
- 按需分配物理内存 :mmap 调用时仅分配虚拟地址空间,不分配物理内存;进程第一次访问映射地址时触发「缺页异常」,内核才分配物理页并建立映射;
- 无数据拷贝:所有映射访问都是「虚拟地址→物理地址」直接访问,无内核缓冲区中转,无数据拷贝,这是 mmap 高性能的核心原因;
- 映射大小按页对齐:即使 length 传 1,内核也会按 4KB 向上对齐分配虚拟地址,物理内存也是按需按页分配;
- munmap 是必须的 :进程退出时内核会自动解除映射,但手动调用
munmap能及时释放虚拟地址空间和物理内存,避免内存泄漏。
mmap 内核底层实现
步骤 1:用户态发起 mmap 系统调用,内核接收参数
进程调用mmap(NULL, len, prot, flags, fd, 0),触发软中断进入内核态,内核的sys_mmap()函数接收所有参数,并做合法性校验:
- 校验 prot 权限是否合法、是否与文件 fd 的打开权限匹配;
- 校验 flags 参数组合是否合法(比如 MAP_ANONYMOUS 时 fd 必须是 - 1);
- 校验 length 是否大于 0;
步骤 2:内核分配「虚拟地址空间」
内核在进程的mm_struct中,查找一段连续的、未被使用的虚拟地址空间,大小为「按 4KB 向上对齐后的 length」,这段地址是进程私有的,不会与其他 VMA 重叠。
步骤 3:内核创建「虚拟内存区域 (VMA)」
内核为本次映射创建一个新的 struct vm_area_struct(VMA 结构体),并填充关键属性:
- 虚拟地址起始 / 结束地址、映射长度;
- 访问权限:prot 对应的读写执行权限;
- 映射属性:
VM_SHARED/VM_PRIVATE(对应 flags)、VM_ANON(匿名映射)/VM_MAPPED(文件映射); - 关联资源:文件映射关联对应的文件 inode;匿名映射关联内核的物理内存池;最后将该 VMA 加入进程的 VMA 链表中,此时仅分配了虚拟地址,无任何物理内存分配。
步骤 4:内核返回映射的虚拟地址到用户态
内核将分配的虚拟地址起始值,通过系统调用返回给用户态进程,进程拿到该地址后,即可开始访问。
此时进程的虚拟地址空间已经分配完成,但物理内存还是空的,页表中也没有对应的映射关系!
步骤 5:进程第一次访问映射地址,触发「缺页异常」
进程执行 *(char*)addr = 10; 访问映射地址,CPU 的 MMU(内存管理单元)在进程的多级页表 (PGD/PUD/PMD/PTE) 中查找该虚拟地址,发现无对应的物理页映射 ,触发 缺页异常 (page fault),CPU 暂停进程执行,进入内核态处理异常。
步骤 6:内核处理缺页异常,分配物理内存 + 建立页表映射
这是 mmap 的核心步骤,内核的do_page_fault()函数处理异常,完成「物理内存分配 + 页表映射」:
- 内核根据虚拟地址找到对应的 VMA 结构体,确认该地址是合法的映射地址;
- 内核从物理内存伙伴系统中,分配一块空闲的 4KB 物理页帧;
- 内核修改进程的多级页表:将当前虚拟地址,通过页表项 (PTE) 映射到刚分配的物理页帧的物理地址;
- 如果是文件映射 :内核还会从磁盘读取对应的数据到物理页中(仅一次);如果是匿名映射:物理页初始化为全 0;
- 页表映射建立完成后,内核返回用户态,恢复进程执行。
步骤 7:进程正常访问映射内存,无内核介入
进程再次执行访问指令,MMU 通过页表直接找到物理地址,直接读写物理内存,无任何内核介入,无数据拷贝,效率和访问进程私有内存完全一致;
步骤 8:munmap 的内核流程
调用munmap(addr, len)时,内核执行:
- 根据虚拟地址找到对应的 VMA 结构体;
- 删除该 VMA,释放对应的虚拟地址空间;
- 清空进程页表中对应的映射关系;
- 将物理页帧归还给内核(匿名映射)/ 标记为脏页等待刷盘(文件映射);
- 物理页会被加入内核 LRU 链表 ,内存紧张时被 kswapd 回收,也可以用
mlock()锁定物理页避免回收。
核心场景:
- 匿名私有 → malloc 大内存;
- 匿名共享 → 父子进程轻量共享;
- 文件私有 → 高性能读文件;
- 文件共享 → Posix 共享内存(/dev/shm),任意进程共享;