mm_struct 是 Linux 内核描述进程地址空间 的核心数据结构,它管理着进程的虚拟内存映射、页表、内存访问权限、地址空间布局等关键信息。每个用户态进程(或内核线程共享的地址空间)都会关联一个 mm_struct,内核通过它实现对进程内存的分配、回收、保护等操作。
- 进程地址空间的唯一标识 :每个独立的用户态进程都有自己的
mm_struct,内核线程则默认共享内核的init_mm(无独立地址空间)。 - 页表的顶层管理入口 :
mm_struct指向进程的顶级页表(如pgd指针),是用户虚拟地址到物理地址映射的总入口。 - 内存区域的组织容器 :通过内部的
vm_area_struct链表,管理进程的所有虚拟内存区域(如代码段、数据段、堆、栈、内存映射文件等)。 - 内存资源的统计与限制 :记录进程的内存使用量、页表项数量、锁定内存大小等,配合
rlimit实现内存资源限制。
核心字段分类说明
| 字段类型 | 核心字段 | 作用 |
|---|---|---|
| 虚拟内存区域管理 | /* 虚拟内存区域(VMA)的链表头 */ struct vm_area_struct *mmap; /* VMA 的红黑树根节点(加速查找) */ struct rb_root mm_rb; /* VMA 缓存序列号,优化查找 */ u32 vmacache_seqnum; | mmap 是 VMA 链表头,mm_rb 是红黑树根(加速 VMA 查找),map_count 记录 VMA 数量 |
| 页表管理 | /* 指向进程的页全局目录(PGD),页表的顶层入口 */ pgd_t *pgd; /* 保护页表的自旋锁 */ spinlock_t page_table_lock; | pgd 指向顶级页表,page_table_lock 保护页表的并发修改 |
| 地址空间边界 | /* 进程地址空间的最大大小(如 32 位进程为 3GB) */ unsigned long task_size; /* 进程内存映射区域的起始地址(默认 TASK_UNMAPPED_BASE) */ unsigned long mmap_base; /* 代码段、数据段的起止地址 */ 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; | 定义进程地址空间的最大范围、映射基址、代码 / 数据 / 堆 / 栈的边界 |
| 内存统计 | /* 进程总虚拟内存页数(单位:PAGE_SIZE) */ unsigned long total_vm; /* 被 mlock() 锁定的内存页数 */ unsigned long locked_vm; /* 进程的 RSS(驻留集大小)历史峰值 */ unsigned long hiwater_rss; /* 进程的虚拟内存大小历史峰值 */ unsigned long hiwater_vm; | 统计进程的虚拟内存、锁定内存、RSS 峰值等资源使用情况 |
| 共享与引用计数 | /* 使用该地址空间的进程数(共享计数,如线程共享) */ atomic_t mm_users; /* 地址空间的引用计数(销毁时减到 0 才释放) */ atomic_t mm_count; | mm_users 是共享该地址空间的进程数(如线程),mm_count 是内核内部引用计数 |
| 架构相关 | /* 架构相关的上下文信息(如 ARM64 的 ASID) */ mm_context_t context; | 存储架构特定信息(如 ARM64 的地址空间标识符 ASID,x86 的 CR3 寄存器值) |
核心关联数据结构
vm_area_struct(虚拟内存区域)
mm_struct 采用 双数据结构 管理 VMA,兼顾遍历效率和查找效率:
- 链表(
mmap字段) :所有 VMA 以vm_area_struct的vm_next指针串联成单向链表,适合全量遍历(如进程退出时释放所有 VMA)。 - 红黑树(
mm_rb字段) :所有 VMA 按虚拟地址范围挂载到红黑树中,适合精准查找(如缺页异常时,快速定位虚拟地址所属的 VMA)。
每个 VMA 对应进程地址空间中的一段连续虚拟地址区间,且具有相同的访问权限(如可读、可写、可执行)。
- 例如:进程的代码段对应一个 VMA(
PROT_READ|PROT_EXEC),堆对应一个 VMA(PROT_READ|PROT_WRITE)。 - 内核通过
find_vma()函数,根据虚拟地址快速查找对应的 VMA(红黑树查找时间复杂度 O (log n))。- 先尝试从 VMA 缓存 (
vmacache)中查找,缓存命中则直接返回(O (1) 开销); - 缓存未命中时,遍历红黑树查找对应 VMA(O (log n) 开销);
- 更新 VMA 缓存,提升下一次查找效率。
- 先尝试从 VMA 缓存 (
pgd_t(页全局目录)
pgd 是 mm_struct 指向的顶级页表项,是虚拟地址翻译的入口:
- 对于 64 位系统(如 ARM64),页表通常分为 4 级(PGD → PUD → PMD → PTE);
- 当进程切换时,内核会将
task_struct->mm->pgd加载到 CPU 的页表基址寄存器(如 ARM64 的TTBR0_EL1),完成地址空间切换。
task_struct(进程控制块)
每个进程的 task_struct 中包含一个 mm 指针,指向该进程的 mm_struct:
struct task_struct {
// ... 其他字段
struct mm_struct *mm; /* 进程的地址空间 */
struct mm_struct *active_mm; /* 运行时的地址空间(内核线程借用) */
// ... 其他字段
};
mm 和 active_mm 是两个与进程地址空间相关的核心指针,它们的设计目标是区分进程的「自有地址空间」和「运行时使用的地址空间」,尤其为内核线程的地址空间管理提供了高效的解决方案。
- 用户态进程:
mm和active_mm指向同一个mm_struct; - 内核线程:无独立地址空间,
mm=NULL,active_mm借用前一个运行进程的mm_struct。
mm 指针
- 作用 :指向进程私有的地址空间结构体,是用户态进程的「自有地址空间」标识。
- 适用对象 :
- 用户态进程 :拥有独立的虚拟地址空间,
mm指向其专属的mm_struct,包含页表、VMA、内存统计等信息。 - 内核线程 :没有独立的用户态地址空间,
mm = NULL。
- 用户态进程 :拥有独立的虚拟地址空间,
active_mm 指针
- 作用 :指向进程运行时实际使用的地址空间结构体 ,是 CPU 页表基址寄存器(如 ARM64 的
TTBR0_EL1)对应的mm_struct。 - 适用对象 :
- 用户态进程 :运行时使用的就是自有地址空间,因此
active_mm = mm。 - 内核线程 :无自有地址空间,需要借用前一个运行在该 CPU 上的用户态进程的
mm_struct,此时active_mm指向被借用的mm_struct。
- 用户态进程 :运行时使用的就是自有地址空间,因此
内核线程的地址空间管理
内核线程本身没有独立的用户态地址空间 (task_struct->mm = NULL),它通过 active_mm 指针借用前一个运行在该 CPU 上的用户态进程的 mm_struct ,从而复用其页表完成内核地址空间的访问,同时避免为内核线程创建独立 mm_struct 的内存开销。
Linux 内核的地址空间是全局共享的,具体体现在:
- 所有进程的页表(包括用户态进程的
mm_struct->pgd)都包含内核地址空间的映射 (如 ARM64 的TTBR1_EL1对应内核页表,x86_64 的高 1GB 地址)。 - 内核线程仅运行在内核态,只需要访问内核地址空间,不需要访问用户态地址空间。
因此,内核线程可以直接复用任意用户态进程的 mm_struct 中的内核页表映射,无需创建自己的 mm_struct。
因此内核设计了 active_mm 机制:
- 内核线程本身不拥有
mm_struct(mm = NULL)。 - 当内核线程在某 CPU 上运行时,复用该 CPU 上前一个用户态进程的
mm_struct作为active_mm。 - 内核线程运行时,CPU 的页表基址寄存器仍指向被借用的
mm_struct->pgd,但由于内核线程只访问内核地址空间,不会触碰到用户态页表的映射,因此不会产生地址越界。 - 当内核线程执行完毕,切换回用户态进程时,
active_mm会自动切回该进程的mm,无需额外操作。 - 每个 CPU 上的内核线程会借用该 CPU 上最近运行的用户态进程的
mm_struct,不同 CPU 上的内核线程的active_mm可以指向不同的mm_struct,互不干扰。
通过 active_mm 访问内核地址空间
内核线程运行期间,active_mm 指向被借用的 mm_struct,核心特性如下:
- 页表复用 :CPU 的页表基址寄存器仍指向
active_mm->pgd,内核线程通过该页表访问内核地址空间(无需关心用户态页表映射)。 - 用户态地址访问限制 :内核线程运行在内核态,CPU 的特权级(如 ARM64 的
EL1、x86 的Ring 0)会阻止其访问用户态地址,因此不会触发地址越界错误。 - 无需修改 VMA / 页表 :内核线程不会操作
active_mm中的 VMA 链表或用户态页表项,仅利用其内核页表映射。
归还 active_mm
- 归还引用 :内核线程的
active_mm被置为NULL,同时递减被借用mm_struct的mm_count。 - 页表切换 :CPU 的页表基址寄存器更新为目标用户态进程的
mm->pgd,恢复其完整的地址空间映射。
主动借用指定进程的 mm_struct
除了进程切换时的自动借用,内核线程还可以通过 use_mm()/unuse_mm() 函数主动借用指定用户态进程的 mm_struct ,适用于需要访问该进程用户态地址空间的场景(如内核态数据拷贝 copy_to_user()/copy_from_user())。
内核通过 use_mm() 和 unuse_mm() 两个函数完成 active_mm 的借用与归还,核心流程如下:
- 内核线程通过
use_mm()显式借用某个用户态进程的mm_struct。 - 借用时会递增
mm->mm_count(内核态引用计数),防止该mm_struct被提前销毁。 - 内核线程退出时,通过
unuse_mm()归还借用的mm_struct。 - 递减
mm_count,若计数归 0,则可以释放mm_struct。
核心操作 API
mm_struct 的创建与销毁
| 函数 | 作用 |
|---|---|
alloc_mm() |
分配 mm_struct 内存(从 slab 分配器中分配) |
mm_init() |
初始化 mm_struct 的字段,设置默认值 |
copy_mm() |
进程 fork 时,复制父进程的 mm_struct 和页表(写时复制) |
exit_mm() |
进程退出时,销毁 mm_struct 并释放关联的页表和 VMA |
虚拟内存区域操作
| 函数 | 作用 |
|---|---|
do_mmap() |
核心函数,创建新的 VMA 并映射到虚拟地址空间(用户态 mmap() 系统调用的底层实现) |
do_munmap() |
释放指定的虚拟地址区间,删除对应的 VMA(用户态 munmap() 系统调用的底层实现) |
find_vma() |
根据虚拟地址查找对应的 VMA |
find_vma_intersection() |
查找与指定地址区间重叠的 VMA |
地址空间切换
| 函数 | 作用 |
|---|---|
switch_mm() |
进程切换时,切换 mm_struct,加载新进程的页表基址到 CPU 寄存器 |
activate_mm() |
激活 mm_struct,用于内核线程切换到用户态进程时的地址空间加载 |
进程切换时的页表切换流程:
- 内核从
task_struct中取出新进程的mm_struct; - 调用
switch_mm()函数,将mm_struct->pgd加载到 CPU 的页表基址寄存器; - 刷新 TLB(Translation Lookaside Buffer),避免旧页表缓存的干扰;
- 新进程的虚拟地址空间生效。
引用计数与地址空间共享
mm_struct 包含两个核心引用计数,用于管理地址空间的生命周期和共享:
mm_users:用户态引用计数 ,表示共享该地址空间的进程数量(如线程组内的所有线程共享同一个mm_struct,mm_users等于线程数)。fork()时,子进程与父进程共享mm_struct,mm_users++;- 线程退出时,
mm_users--,当mm_users减至 0 时,触发页表和 VMA 的释放。
mm_count:内核态引用计数 ,表示内核对该mm_struct的引用次数(如内核线程借用地址空间时,mm_count++)。只有当mm_count减至 0 时,才会调用free_mm()释放mm_struct本身的内存。
写时复制(Copy-On-Write, COW)
进程 fork() 时,内核不会直接复制父进程的物理页,其实现完全依赖 mm_struct 和页表的配合:
fork()时,内核复制父进程的mm_struct(浅拷贝),共享页表和 VMA,mm_users++;- 将父、子进程的所有页表项标记为 只读 ,并将
mm_struct中的 VMA 权限也标记为只读; - 当任一进程尝试写入 某页面时,触发缺页异常 (
page_fault); - 内核在缺页异常处理函数中,为该进程分配新的物理页,复制原页面数据,更新页表项为可写,解除 COW 限制。
内核版本差异
| 差异点 | Linux 4.4 | Linux 5.4 |
|---|---|---|
| 页表缓存 | pgt_cache 字段直接在 mm_struct 中 |
页表缓存逻辑迁移到 mm_context_t,更贴合架构设计 |
| NUMA 相关字段 | numa_scan_seq、numa_masks 直接定义 |
新增 numa_state 结构体,统一管理 NUMA 扫描状态 |
| 内存控制组 | mem_cgroup 字段为可选(CONFIG_MEMCG) |
默认启用 mem_cgroup,新增 memcg_batch 优化批量统计 |
| VMA 查找优化 | vmacache_seqnum 基础实现 |
新增 vmacache_fault() 函数,进一步优化缺页异常时的 VMA 查找 |
ARM64 架构适配
ARM64 架构的 mm_struct 中,context 字段(mm_context_t)包含关键的地址空间标识符 ASID:
- ASID 用于区分不同进程的页表,避免进程切换时刷新整个 TLB;
- 内核通过 ASID 实现 TLB 条目共享,提升地址翻译性能。
mm_struct 的创建
以 execve() 系统调用为例,新进程 mm_struct 的创建流程:
- 调用
alloc_mm()从slab分配器中分配mm_struct内存; - 调用
mm_init()初始化mm_struct的默认字段(如mmap_base、task_size、信号量等); - 解析可执行文件的段表(如 ELF 的
.text、.data段),调用do_mmap()创建对应的 VMA; - 初始化页表(
pgd),建立代码段、数据段的虚拟地址到物理地址的映射; - 设置栈的 VMA(
start_stack),将命令行参数和环境变量拷贝到栈中; - 将
mm_struct挂载到task_struct的mm指针上。
mm_struct 的销毁
进程调用 exit() 时,mm_struct 的销毁流程:
- 执行
exit_mm()函数,获取task_struct->mm指针; - 调用
mmput()递减mm_users计数; - 若
mm_users减至 0,触发mm_release():- 遍历
mmap链表,调用do_munmap()释放所有 VMA; - 调用
free_pgtables()释放所有页表(PGD/PUD/PMD/PTE); - 递减
mm_count计数,若mm_count减至 0,调用free_mm()释放mm_struct内存;
- 遍历
- 将
task_struct->mm置为NULL,完成地址空间的销毁。
虚拟内存映射
用户态调用 mmap() 时,内核的底层实现流程:
- 系统调用陷入内核,执行
sys_mmap(),最终调用do_mmap(); - 根据传入的参数(映射地址、长度、权限),在
mm_struct的地址空间中分配一段空闲虚拟地址; - 创建新的
vm_area_struct,设置其权限、映射类型(文件映射 / 匿名映射)等属性; - 将新 VMA 挂载到
mm_struct的mmap链表和mm_rb红黑树中; - 若为文件映射 ,建立 VMA 与文件
struct address_space的关联;若为匿名映射,暂不分配物理页(延迟到缺页异常时分配)。