4. mmap 内核实现路径:do_mmap 函数流程、vma 结构体管理、文件映射的 page cache 交互
mmap 系统调用的内核实现核心是 do_mmap() 函数,它负责将用户虚拟地址空间与物理内存或文件内容建立映射。本章深入剖析 do_mmap 的执行流程、虚拟内存区域(VMA)的管理机制,以及文件映射场景下与 Page Cache 的交互细节。
4.1 do_mmap 函数核心流程
do_mmap() 是 mmap 系统调用的内核入口函数,位于 mm/mmap.c。其核心流程分为三个阶段:地址空间检查与分配、VMA 创建与插入、以及文件映射的页表初始化。
关键路径: sys_mmap_pgoff → ksys_mmap_pgoff → vm_mmap_pgoff → do_mmap → mmap_region
4.1.1 流程步骤分解
- 参数校验与对齐:检查
len是否为 0,offset是否页对齐,prot和flags组合是否合法。 - 地址空间预留:调用
get_unmapped_area()在进程地址空间中查找空闲区域,返回起始虚拟地址addr。 - VMA 合并检查:判断新映射是否可以与相邻 VMA 合并,减少管理开销。
- 文件映射准备:若为文件映射,调用
file->f_op->mmap()文件系统钩子,通常指向generic_file_mmap()。 - VMA 插入红黑树:将新 VMA 插入进程的
mm_struct红黑树和链表。 - 页表初始化:对于 MAP_POPULATE 或 MAP_LOCKED 标志,立即分配物理页并建立页表。
性能要点: do_mmap 的耗时主要取决于 get_unmapped_area 的查找效率(红黑树 O(log n))和 VMA 合并 的判断次数。频繁 mmap/munmap 会导致 VMA 碎片化,增加查找开销。
4.1.2 do_mmap 函数流程 SVG 图
4.2 VMA 结构体管理
VMA(Virtual Memory Area)是 Linux 内核管理进程虚拟地址空间的基本单位。每个 VMA 描述一段连续的虚拟地址区间及其访问属性。
4.2.1 vm_area_struct 核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
| vm_start | unsigned long | VMA 起始虚拟地址(包含) |
| vm_end | unsigned long | VMA 结束虚拟地址(不包含) |
| vm_flags | unsigned long | 访问权限标志(VM_READ/VM_WRITE/VM_EXEC/VM_SHARED) |
| vm_file | struct file * | 映射的文件对象(匿名映射为 NULL) |
| vm_ops | const struct vm_operations_struct * | VMA 操作函数表(open/close/fault) |
| vm_pgoff | unsigned long | 文件映射的页偏移量 |
| vm_next / vm_prev | struct vm_area_struct * | 双向链表指针 |
| vm_rb | struct rb_node | 红黑树节点(用于快速查找) |
4.2.2 VMA 组织与查找
进程的 mm_struct 使用两种数据结构管理 VMA:
- 双向链表:通过
vm_next/vm_prev串联所有 VMA,用于遍历(如/proc/pid/maps)。 - 红黑树:以
vm_start为键值组织,用于 O(log n) 快速查找虚拟地址所属的 VMA。
避坑指南: 当进程进行大量 mmap/munmap 操作时,VMA 数量会快速增长。内核使用 vma_count 统计 VMA 数量,超过 sysctl_max_map_count(默认 65530)将返回 ENOMEM。频繁映射小区域(如 4KB)会快速耗尽 VMA 配额。
4.2.3 VMA 合并与分裂 SVG 图
4.3 文件映射的 Page Cache 交互
文件映射 mmap 的核心优势在于利用 Page Cache 实现文件内容与用户空间的直接共享,避免 read/write 系统调用的数据拷贝。
4.3.1 缺页中断处理路径
当用户首次访问 mmap 映射的虚拟地址时,触发缺页中断,内核通过以下路径填充物理页:
- 地址查找:根据虚拟地址找到所属 VMA。
- 文件偏移计算:
offset = (addr - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT)。 - Page Cache 查找:调用
find_get_page(mapping, index)在文件地址空间mapping中查找缓存页。 - 缓存命中:直接映射到用户页表,设置
pte。 - 缓存未命中:调用
page_cache_read()从磁盘读取数据到 Page Cache,再建立映射。
性能关键: 文件映射的缺页处理路径比匿名映射多一次 Page Cache 查找和可能的磁盘 I/O。使用 MAP_POPULATE 标志可以预读并填充页表,避免运行时缺页抖动。
4.3.2 Page Cache 与文件映射交互 SVG 图
4.3.3 文件映射 vs 匿名映射对比
| 特性 | 文件映射 (MAP_SHARED) | 文件映射 (MAP_PRIVATE) | 匿名映射 (MAP_ANONYMOUS) |
|---|---|---|---|
| 后端存储 | 文件 + Page Cache | 文件 + Page Cache + COW | swap 分区 |
| 数据来源 | 文件内容 | 文件内容(修改不写回) | 全零页 / 匿名页 |
| 缺页处理 | filemap_fault | filemap_fault + COW | do_anonymous_page |
| 写回机制 | 脏页回写 | 无(丢弃修改) | swap 换出 |
| 进程间共享 | 是(共享同一 Page Cache) | 否(COW 隔离) | 否(除非 MAP_SHARED 匿名) |
| 典型用途 | 文件 I/O、共享内存 | 加载可执行文件、动态库 | 堆、栈、私有内存 |
4.4 关键数据结构:address_space
文件映射与 Page Cache 的交互通过 struct address_space 完成,它是文件系统与内存管理之间的桥梁。
struct address_space {
struct inode *host; /* 所属 inode */
struct radix_tree_root i_pages; /* 页缓存基数树(xarray) */
atomic_t i_mmap_writable;/* 可写映射计数 */
struct rb_root_cached i_mmap; /* 所有映射该文件的 VMA 红黑树 */
unsigned long nrpages; /* 缓存页数量 */
const struct address_space_operations *a_ops; /* 操作函数表 */
};
工程实践: 当多个进程通过 mmap 映射同一个文件时,所有 VMA 都会注册到
inode->i_mapping 的 i_mmap 红黑树中。内核通过 反向映射(RMAP) 机制,在回收 Page Cache 页时,能够快速找到所有映射该页的 VMA 并更新对应的页表项。
4.5 本章小结
- do_mmap 是 mmap 的内核实现核心,负责地址分配、VMA 创建和文件映射初始化。
- VMA 通过双向链表和红黑树双重组织,支持 O(log n) 查找和 O(1) 遍历。
- 文件映射 利用 Page Cache 实现零拷贝数据访问,缺页中断处理路径涉及
filemap_fault和address_space查找。 - MAP_SHARED 与 MAP_PRIVATE 在写回和共享语义上有本质区别,选择不当会导致数据一致性问题或性能下降。