4. 内存描述符 mm_struct:进程内存描述符结构
在 Linux 内核中,每个进程(或线程)都拥有一个独立的虚拟地址空间。内核使用 mm_struct 结构体来描述和管理这个虚拟地址空间的所有元数据。它是内存管理子系统中最重要的数据结构之一,直接关联到进程的页表、VMA 链表、代码段/数据段/堆/栈的边界以及内存使用统计。
4.1 mm_struct 核心作用与生命周期
mm_struct 位于进程描述符 task_struct 的 mm 和 active_mm 字段中。其生命周期与进程的地址空间绑定:
- 创建:
fork()系统调用时,通过copy_mm()复制父进程的mm_struct(写时复制)。 - 共享:
clone()创建线程时,CLONE_VM标志使子线程共享父进程的mm_struct(mm指针指向同一实例)。 - 销毁:进程调用
exit()时,exit_mm()解除映射并释放mm_struct,当引用计数降为 0 时回收。
关键区分:普通用户进程的
task_struct->mm 指向其专属的 mm_struct;内核线程的 mm 字段为 NULL,但 active_mm 会借用上一个用户进程的 mm_struct 以访问页表。
4.2 mm_struct 关键字段解析
下表列出了 mm_struct 中最核心的字段及其作用:
| 字段名 | 类型 | 作用描述 |
|---|---|---|
pgd |
pgd_t * |
指向进程页全局目录(Page Global Directory)的指针,是页表层次结构的根。 |
mmap |
struct vm_area_struct * |
指向虚拟内存区域(VMA)链表的头节点,描述地址空间的所有线性区间。 |
mm_rb |
struct rb_root |
VMA 红黑树的根节点,用于快速查找给定虚拟地址所属的 VMA。 |
start_code, end_code |
unsigned long |
代码段的起始和结束虚拟地址。 |
start_data, end_data |
unsigned long |
数据段的起始和结束虚拟地址。 |
start_brk, brk |
unsigned long |
堆的起始地址(start_brk)和当前堆顶(brk),由 brk() 系统调用调整。 |
start_stack |
unsigned long |
用户栈的起始地址(栈顶,高地址)。 |
arg_start, arg_end |
unsigned long |
命令行参数在用户栈中的起始和结束地址。 |
env_start, env_end |
unsigned long |
环境变量在用户栈中的起始和结束地址。 |
map_count |
atomic_t |
当前地址空间中 VMA 的数量。 |
total_vm |
unsigned long |
进程使用的总虚拟页面数(包含所有映射)。 |
rss_stat |
struct mm_rss_stat |
常驻内存集(Resident Set Size)统计信息,记录各类型物理页数量。 |
mm_users |
atomic_t |
共享该 mm_struct 的用户态进程/线程数量。 |
mm_count |
atomic_t |
内核中对该 mm_struct 的引用计数(包括借用)。 |
mmlist |
struct list_head |
用于将所有 mm_struct 链接到全局链表 mmlist 中。 |
4.3 进程地址空间布局与 mm_struct 的映射
下图展示了典型 32 位 Linux 进程的虚拟地址空间布局,以及 mm_struct 中各边界字段如何划分区域:
4.4 VMA 组织:链表与红黑树
mm_struct 通过两种数据结构管理所有虚拟内存区域(VMA):
- 双向链表 (
mmap):按虚拟地址升序链接所有 VMA,便于遍历全部区域。 - 红黑树 (
mm_rb):以 VMA 的起始地址为键值构建平衡树,实现 O(log n) 的地址查找。
内核在查找虚拟地址所属 VMA 时,优先使用红黑树;在需要遍历所有 VMA(如 /proc/pid/maps 输出)时,使用链表。
性能优化:内核还维护了
mm_struct->mmap_cache 字段,缓存最近一次查找的 VMA 指针。由于程序访问具有时间局部性,该缓存命中率极高,可避免频繁的红黑树查找。
4.5 内核线程与 mm_struct
内核线程(kthread)是运行在内核空间的特殊进程,没有用户态地址空间。其 mm_struct 处理方式与普通进程截然不同:
| 特性 | 普通用户进程 | 内核线程 |
|---|---|---|
task_struct->mm |
指向进程自己的 mm_struct |
NULL |
task_struct->active_mm |
与 mm 相同 |
指向借用的用户进程 mm_struct |
| 页表访问 | 使用自己的页表 | 使用 active_mm 的页表(仅内核映射部分) |
| 地址空间切换 | 完整切换(TLB 刷新) | 仅切换 active_mm,不刷新用户 TLB |
| 内存回收 | 参与 OOM 和 RSS 统计 | 不参与用户内存统计 |
内核线程借用 active_mm 的机制避免了为每个内核线程创建独立的 mm_struct,同时保证了内核线程在访问内核线性映射区时能够使用正确的页表。
4.6 上下文切换中的 mm 处理
进程调度时的地址空间切换由 context_switch() 函数完成,其核心逻辑如下:
// kernel/sched/core.c (简化)
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
// 准备新进程的 mm
mm = next->mm;
oldmm = prev->active_mm;
if (!mm) {
// 内核线程:借用 prev 的 active_mm
mm = oldmm;
atomic_inc(&mm->mm_count);
next->active_mm = oldmm;
} else {
// 用户进程:切换地址空间
switch_mm(oldmm, mm, next);
}
// 如果 prev 是内核线程,释放对 oldmm 的借用
if (!prev->mm) {
prev->active_mm = NULL;
mmdrop(oldmm);
}
// ... 切换寄存器状态等
}
注意:内核线程在调度出去时,会释放对
active_mm 的引用(mmdrop)。如果此时 mm_count 降为 0,该 mm_struct 会被彻底销毁。这要求用户进程在退出时必须确保所有借用其 mm 的内核线程已经释放引用。
4.7 mm_struct 的引用计数机制
mm_struct 使用两个原子计数器管理生命周期:
mm_users:记录共享该地址空间的用户态实体数量(进程/线程)。当mm_users降为 0 时,内核开始清理用户页表。mm_count:记录内核对该mm_struct的引用次数(包括内核线程的借用)。当mm_count降为 0 时,释放mm_struct本身占用的内存。
这种双层计数设计确保了:即使所有用户线程都已退出(mm_users = 0),只要还有内核线程在借用该 mm(mm_count > 0),mm_struct 就不会被释放。
4.8 关键数据结构关系总结
以下代码片段展示了 task_struct 与 mm_struct 的关联方式:
// include/linux/sched.h (简化)
struct task_struct {
// ...
struct mm_struct *mm; // 进程的地址空间描述符
struct mm_struct *active_mm; // 内核线程借用时使用
// ...
};
// include/linux/mm_types.h (简化)
struct mm_struct {
struct vm_area_struct *mmap; // VMA 链表头
struct rb_root mm_rb; // VMA 红黑树根
pgd_t *pgd; // 页全局目录
atomic_t mm_users; // 用户态引用计数
atomic_t mm_count; // 内核态引用计数
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long total_vm; // 总虚拟页数
unsigned long hiwater_rss; // 历史最大 RSS
// ...
};
调试技巧:通过
/proc/<pid>/maps 可以查看进程的 VMA 布局;通过 /proc/<pid>/smaps 可以查看每个 VMA 的详细内存统计。这些信息直接来源于 mm_struct 中的 mmap 链表和 rss_stat 统计。