3. 内存描述符(struct mm_struct)
在 Linux 内核中,每个进程都拥有独立的虚拟地址空间,而描述这个地址空间的核心数据结构就是 struct mm_struct,通常被称为内存描述符。它是进程内存管理的总控结构,记录了进程地址空间的布局、内存映射、页表信息以及各类统计计数。理解 mm_struct 是掌握进程地址空间管理的第一步。
3.1 mm_struct 结构体定义
struct mm_struct 定义在 include/linux/mm_types.h 中,是一个体量较大的结构体。下面给出其核心字段的简化定义,省略了部分条件编译和锁相关细节:
struct mm_struct {
struct vm_area_struct *mmap; /* 虚拟内存区域链表头 */
struct rb_root mm_rb; /* 虚拟内存区域红黑树根 */
unsigned long mmap_base; /* mmap 映射起始地址 */
unsigned long task_size; /* 进程地址空间大小 */
pgd_t *pgd; /* 页全局目录指针 */
atomic_t mm_users; /* 用户空间引用计数 */
atomic_t mm_count; /* 内核引用计数 */
int map_count; /* 虚拟内存区域个数 */
spinlock_t page_table_lock; /* 页表锁 */
struct rw_semaphore mmap_sem; /* 内存区域写信号量 */
unsigned long start_code, end_code; /* 代码段起始和结束 */
unsigned long start_data, end_data; /* 数据段起始和结束 */
unsigned long start_brk, brk; /* 堆起始和当前结束 */
unsigned long start_stack; /* 栈起始地址 */
unsigned long arg_start, arg_end; /* 命令行参数区 */
unsigned long env_start, env_end; /* 环境变量区 */
unsigned long total_vm; /* 虚拟页面总数 */
unsigned long locked_vm; /* 锁定页面数 */
unsigned long pinned_vm; /* 固定页面数 */
unsigned long data_vm; /* 数据段页面数 */
unsigned long exec_vm; /* 可执行页面数 */
unsigned long stack_vm; /* 栈页面数 */
unsigned long hiwater_rss; /* 最大驻留集大小 */
unsigned long hiwater_vm; /* 最大虚拟内存大小 */
struct mm_struct *prev, *next; /* 用于内核线程的链表 */
};
关键提示:
mm_users 和 mm_count 是理解内存描述符生命周期的核心。前者记录共享该地址空间的用户态进程数,后者记录内核中对该结构体的引用次数。两者共同决定了 mm_struct 何时被释放。
3.2 核心字段解析
根据功能划分,mm_struct 的核心字段可以分为以下几类:
| 类别 | 字段 | 说明 |
|---|---|---|
| VMA 管理 | mmap |
虚拟内存区域(VMA)的双向链表头,用于遍历所有映射区域 |
mm_rb |
VMA 的红黑树根,用于快速查找给定地址所属的 VMA | |
| 地址空间布局 | mmap_base |
mmap 映射的起始地址,受 ASLR 影响 |
task_size |
用户空间大小,32 位通常为 3GB,64 位为 128TB | |
| 页表 | pgd |
指向进程页全局目录的指针,是硬件页表遍历的入口 |
| 引用计数 | mm_users |
用户态引用计数,每多一个共享进程则加 1 |
mm_count |
内核引用计数,内核模块或线程持有该结构时加 1 | |
| 进程段信息 | start_code / end_code |
代码段(.text)的起始和结束地址 |
start_data / end_data |
数据段(.data + .bss)的起始和结束地址 | |
| 堆与栈 | start_brk / brk |
堆的起始地址和当前 brk 指针,brk 随 malloc 增长 |
start_stack |
栈的起始地址(高地址向下增长) | |
| 统计计数 | total_vm |
进程虚拟页面总数,对应 VIRT 指标 |
hiwater_rss |
历史最大驻留集大小,用于内存峰值统计 |
设计意图: 红黑树(
mm_rb)与链表(mmap)并存是为了兼顾遍历和查找效率。链表适合顺序遍历所有 VMA,红黑树则提供 O(log n) 的地址查找性能,这在缺页异常处理中至关重要。
3.3 mm_struct 的生命周期
内存描述符的生命周期分为三个阶段:创建、共享与复制、销毁。下面通过 SVG 流程图展示完整生命周期:
3.3.1 创建阶段
当进程调用 fork() 或 clone() 系统调用时,内核通过 copy_mm() 函数处理内存描述符。创建逻辑如下:
- 分配 mm_struct:调用
allocate_mm()从 slab 分配器获取一个mm_struct对象。 - 初始化:
mm_init()设置初始值,包括mm_users = 1、mm_count = 1、pgd = NULL。 - 分配页表:调用
mm_alloc_pgd()分配进程的页全局目录。 - 设置默认段:根据
ELF解释器或exec参数设置代码段、数据段、栈的起始地址。
注意: 内核线程没有独立的地址空间,其
mm_struct 字段 mm 为 NULL,但会借用上一个用户进程的 active_mm。这是内核线程与用户进程在内存管理上的根本区别。
3.3.2 共享与复制
clone() 系统调用的 CLONE_VM 标志决定了内存描述符的处理方式:
| 标志 | 行为 | 典型场景 |
|---|---|---|
CLONE_VM 置位 |
共享父进程的 mm_struct,仅 mm_users++ |
线程创建(pthread_create) |
CLONE_VM 未置位 |
调用 dup_mm() 深度复制整个地址空间 |
进程创建(fork) |
在复制路径中,dup_mm() 执行以下关键步骤:
- 分配新的
mm_struct并复制所有字段。 - 遍历父进程的 VMA 链表,对每个 VMA 调用
dup_vma()创建副本。 - 复制页表:调用
copy_page_range()逐级复制页表项,并设置写时复制(COW)属性。 - 更新统计计数:
total_vm、map_count等继承父进程值。
3.3.3 销毁阶段
内存描述符的销毁由两个引用计数协同控制:
/* 用户态引用释放 */
void mmput(struct mm_struct *mm) {
if (atomic_dec_and_test(&mm->mm_users)) {
// 最后一个用户退出,执行清理
exit_mmap(mm); // 卸载所有 VMA
mmput_async_fn(mm); // 异步清理
set_mm_exe_file(mm, NULL);
}
}
/* 内核引用释放 */
void mmdrop(struct mm_struct *mm) {
if (atomic_dec_and_test(&mm->mm_count)) {
// 内核引用归零,真正释放内存
free_mm(mm); // 归还 slab 缓存
}
}
销毁流程的关键路径:
- 进程退出:调用
exit_mm()将当前进程的mm置为NULL,并调用mmput()。 - mmput():递减
mm_users,若归零则执行exit_mmap()释放所有 VMA 和页表。 - mmdrop():递减
mm_count,若归零则调用free_mm()将mm_struct归还给 slab 分配器。
为什么需要两个引用计数?
mm_users 跟踪共享地址空间的用户态进程数,当最后一个用户退出时,必须立即释放页表和 VMA 以回收物理内存。而 mm_count 跟踪内核中所有持有该结构体指针的模块(如内核线程、驱动),确保在用户态全部退出后,内核仍能安全访问 mm_struct 中的统计信息,直到所有内核引用也释放。
下面通过 SVG 图展示引用计数变化与销毁触发条件:
3.4 关键数据结构关系图
最后,通过一张综合 SVG 图展示 mm_struct 与进程描述符、VMA、页表之间的关系:
通过上图可以清晰看到:task_struct 通过 mm 指针引用 mm_struct;mm_struct 通过 mmap 和 mm_rb 管理所有 VMA,通过 pgd 指向页表层级;页表最终映射到物理页框。这一整套数据结构构成了进程地址空间的完整骨架。