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 时(例如缺页异常处理),会优先使用红黑树进行快速定位;而当需要顺序输出所有内存区域时,则遍历链表。
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 三个核心字段组织进程地址空间:
2.4 关键操作流程:缺页异常中的 VMA 查找
当 CPU 访问一个虚拟地址但页表项无效时,触发缺页异常。内核通过 mm_struct 的字段完成以下流程:
- 获取 mm_struct:从当前进程的
task_struct->mm获取。 - 红黑树查找:调用
find_vma(),以缺页地址为 key,在mm_rb红黑树中查找包含该地址的 VMA。 - 权限检查:检查 VMA 的
vm_flags是否允许本次访问(读/写/执行)。 - 物理页分配:通过
alloc_page()分配物理页,并更新页表。 - 统计更新:增加
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 |