2. 进程地址空间 mm_struct:数据结构与核心字段深度解析

在 Linux 内核中,每个进程都拥有独立的虚拟地址空间,而 mm_struct 正是描述这个地址空间的核心数据结构。它记录了进程从代码段、数据段、堆、栈到内存映射文件的所有内存区域布局,是内存管理子系统中最顶层的抽象。本章将深入剖析 mm_struct 的完整结构,并重点解读 pgd、mmap、mm_rb、map_count 四个关键字段的设计意图与协同机制。

2.1 mm_struct 数据结构全景

mm_struct 定义在 include/linux/mm_types.h 中,是进程内存管理的“总控室”。它聚合了页表、虚拟内存区域(VMA)、内存使用统计、锁与引用计数等关键信息。以下表格列出了其核心成员及其功能分类:

分类 字段名 类型 功能描述
页表 pgd_t * pgd 指针 指向进程页全局目录(Page Global Directory),是虚拟地址到物理地址转换的入口
VMA 链表 struct vm_area_struct * mmap 链表头 以双向链表组织所有虚拟内存区域,便于遍历和顺序访问
VMA 红黑树 struct rb_root mm_rb 树根 以红黑树组织所有 VMA,支持按虚拟地址快速查找
区域计数 unsigned long map_count 计数器 记录当前进程中 VMA 的总数量,用于限制和监控
地址范围 unsigned long start_code, end_code, start_data, end_data 地址 代码段和数据段的起始与结束虚拟地址
堆与栈 unsigned long start_brk, brk, start_stack 地址 堆的起始与当前边界,以及栈的起始地址
内存统计 struct mm_rss_stat rss_stat 结构体 记录常驻内存页(RSS)的统计信息
锁与引用 struct rw_semaphore mmap_lock 读写信号量 保护 VMA 链表和红黑树的并发访问
设计要点: mm_struct 通过“链表 + 红黑树”双结构管理 VMA,链表保证遍历效率,红黑树保证查找效率(O(log n))。这种组合设计是内核中“空间换时间”的经典实践。

2.2 核心字段深度分析

2.2.1 pgd:页表入口

pgd 字段指向进程的页全局目录(Page Global Directory),是 CPU 进行虚拟地址到物理地址转换的硬件级入口。当进程被调度运行时,内核会将 pgd 的值加载到 MMU 的页表基址寄存器中。

  • 进程切换:每次上下文切换时,内核通过 switch_mm() 更新 CR3 寄存器(x86)或 TTBR(ARM),指向新进程的 pgd。
  • 共享内核空间:所有进程的 pgd 中,内核态页表项(高地址部分)通常指向同一组物理页,实现内核空间共享。
  • 写时复制:fork() 时子进程复制父进程的 pgd,但页表项标记为只读,触发缺页时再复制物理页。
关键代码路径: mm_init() 中调用 pgd_alloc() 分配新的页全局目录,并将内核页表项(如 swapper_pg_dir)拷贝到新 pgd 的高半部分。

2.2.2 mmap 与 mm_rb:VMA 的双重索引

mmap 是虚拟内存区域(VMA)的双向链表头,mm_rb 是红黑树的根节点。两者维护同一组 VMA 对象,但组织方式不同:

特性 mmap 链表 mm_rb 红黑树
数据结构 双向链表 红黑树(平衡二叉搜索树)
遍历顺序 按虚拟地址升序 中序遍历升序
查找复杂度 O(n) O(log n)
插入/删除 O(1)(已知位置) O(log n)
典型用途 遍历所有 VMA(如 /proc/pid/maps) 按地址查找 VMA(如缺页处理)

当内核需要根据虚拟地址查找所属 VMA 时(例如缺页异常处理),会优先使用红黑树进行快速定位;而当需要顺序输出所有内存区域时,则遍历链表。

注意: 链表和红黑树必须保持一致性。所有对 VMA 的添加、删除操作都必须同时更新 mmap 和 mm_rb,否则会导致内核崩溃。这一操作在 vma_link() 和 vma_unlink() 中完成。

2.2.3 map_count:区域数量监控

map_count 记录当前进程拥有的 VMA 总数。内核通过它实现以下机制:

  • 资源限制:检查 RLIMIT_AS 和 RLIMIT_MEMLOCK,防止进程创建过多内存区域导致内核 VMA 结构耗尽。
  • 性能优化:当 map_count 超过阈值(如 sysctl_max_map_count,默认 65530)时,mmap() 系统调用返回 ENOMEM。
  • 调试接口:/proc/pid/maps 和 /proc/pid/smaps 的行数直接等于 map_count。

2.3 mm_struct 与 VMA 的关系:内联 SVG 架构图

下图展示了 mm_struct 如何通过 pgd、mmap、mm_rb 三个核心字段组织进程地址空间:

mm_struct 核心字段与 VMA 组织架构 mm_struct 核心字段与 VMA 组织架构 mm_struct pgd: 页全局目录指针 mmap: VMA 链表头 mm_rb: VMA 红黑树根 页全局目录 (pgd_t 数组) VMA 双向链表 (mmap) VMA 1 VMA 2 VMA 3 VMA n VMA 红黑树 (mm_rb 根) VMA 2 (根) VMA 1 (左) VMA 3 (右) 说明: mmap 链表按地址升序链接所有 VMA,适合顺序遍历;mm_rb 红黑树支持 O(log n) 的地址查找。 pgd 指向硬件页表,是虚拟地址转换的最终入口。三者协同完成进程地址空间的管理。

2.4 关键操作流程:缺页异常中的 VMA 查找

当 CPU 访问一个虚拟地址但页表项无效时,触发缺页异常。内核通过 mm_struct 的字段完成以下流程:

  1. 获取 mm_struct:从当前进程的 task_struct->mm 获取。
  2. 红黑树查找:调用 find_vma(),以缺页地址为 key,在 mm_rb 红黑树中查找包含该地址的 VMA。
  3. 权限检查:检查 VMA 的 vm_flags 是否允许本次访问(读/写/执行)。
  4. 物理页分配:通过 alloc_page() 分配物理页,并更新页表。
  5. 统计更新:增加 rss_stat 中的对应计数。
性能关键点: find_vma() 是内核中最频繁调用的函数之一。红黑树查找的 O(log n) 复杂度保证了即使进程拥有数万个 VMA,缺页处理仍能保持高效。

2.5 实战:查看进程的 mm_struct 信息

在 Linux 系统中,可以通过 /proc 文件系统查看进程的地址空间布局,这些信息直接来源于 mm_struct:

# 查看当前 shell 进程的内存映射
cat /proc/self/maps

# 输出示例(每行对应一个 VMA):
# 00400000-00452000 r-xp 00000000 08:01 1234567    /usr/bin/bash
# 00651000-00652000 r--p 00051000 08:01 1234567    /usr/bin/bash
# 00652000-00656000 rw-p 00052000 08:01 1234567    /usr/bin/bash
# 7f8c1a000000-7f8c1a021000 rw-p 00000000 00:00 0 [heap]
# 7ffd5c000000-7ffd5c021000 rw-p 00000000 00:00 0 [stack]

每一行的格式为:起始地址-结束地址 权限 偏移 主设备号:次设备号 inode 路径。内核在生成此文件时,正是遍历了 mm_struct->mmap 链表。

2.6 本章小结

字段 核心作用 关联子系统
pgd 页表入口,硬件地址转换 MMU、缺页异常、进程切换
mmap VMA 链表头,顺序遍历 /proc 文件系统、coredump
mm_rb VMA 红黑树根,快速查找 缺页异常、mprotect、munmap
map_count VMA 数量计数器 资源限制、系统调用 mmap