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 流程步骤分解

  1. 参数校验与对齐:检查 len 是否为 0,offset 是否页对齐,prot 和 flags 组合是否合法。
  2. 地址空间预留:调用 get_unmapped_area() 在进程地址空间中查找空闲区域,返回起始虚拟地址 addr。
  3. VMA 合并检查:判断新映射是否可以与相邻 VMA 合并,减少管理开销。
  4. 文件映射准备:若为文件映射,调用 file->f_op->mmap() 文件系统钩子,通常指向 generic_file_mmap()。
  5. VMA 插入红黑树:将新 VMA 插入进程的 mm_struct 红黑树和链表。
  6. 页表初始化:对于 MAP_POPULATE 或 MAP_LOCKED 标志,立即分配物理页并建立页表。
性能要点: do_mmap 的耗时主要取决于 get_unmapped_area 的查找效率(红黑树 O(log n))和 VMA 合并 的判断次数。频繁 mmap/munmap 会导致 VMA 碎片化,增加查找开销。

4.1.2 do_mmap 函数流程 SVG 图

do_mmap 内核执行流程 sys_mmap 入口 参数校验与对齐 get_unmapped_area VMA 合并检查 文件映射钩子调用 VMA 插入红黑树 页表初始化 (可选) 返回 vma->vm_start 地址空间查找策略 • MAP_FIXED: 精确地址 • 默认: 从 mmap_base 向下搜索 • 大页: 对齐到 hugepage 边界 文件映射钩子 generic_file_mmap() → 设置 vma->vm_ops

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 图

VMA 合并与分裂操作 合并前: VMA A VMA B (新) VMA C 合并后: 合并 VMA (A+B) VMA C 分裂(munmap 部分区域): 原始 VMA VMA 左 空洞 VMA 右 合并条件 1. 相邻 VMA 地址连续 2. vm_flags 完全相同 3. vm_file 指向同一文件 4. vm_pgoff 连续

4.3 文件映射的 Page Cache 交互

文件映射 mmap 的核心优势在于利用 Page Cache 实现文件内容与用户空间的直接共享,避免 read/write 系统调用的数据拷贝。

4.3.1 缺页中断处理路径

当用户首次访问 mmap 映射的虚拟地址时,触发缺页中断,内核通过以下路径填充物理页:

  1. 地址查找:根据虚拟地址找到所属 VMA。
  2. 文件偏移计算:offset = (addr - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT)。
  3. Page Cache 查找:调用 find_get_page(mapping, index) 在文件地址空间 mapping 中查找缓存页。
  4. 缓存命中:直接映射到用户页表,设置 pte。
  5. 缓存未命中:调用 page_cache_read() 从磁盘读取数据到 Page Cache,再建立映射。
性能关键: 文件映射的缺页处理路径比匿名映射多一次 Page Cache 查找和可能的磁盘 I/O。使用 MAP_POPULATE 标志可以预读并填充页表,避免运行时缺页抖动。

4.3.2 Page Cache 与文件映射交互 SVG 图

文件映射缺页中断与 Page Cache 交互 用户进程虚拟地址空间 mmap 映射区域 内核缺页中断处理 handle_mm_fault → filemap_fault Page Cache (address_space) radix tree / xarray 索引 块设备 / 文件系统 磁盘 I/O 读取 缺页中断 查找 Page Cache 缓存未命中 → 磁盘 I/O 返回缓存页 建立页表映射 Page Cache 交互要点 • 文件映射直接使用 Page Cache 页 • 多个进程映射同一文件共享同一缓存页 • 写回通过脏页回写机制(pdflush) • MAP_SHARED 修改直接反映到 Page Cache • MAP_PRIVATE 触发 COW 复制

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 在写回和共享语义上有本质区别,选择不当会导致数据一致性问题或性能下降。