3. 内存描述符 mm_struct:进程地址空间描述
在 Linux 内核中,每个进程(包括线程)都拥有一个独立的虚拟地址空间。内核使用 mm_struct 结构体来描述和管理这个地址空间。它是内存管理子系统中最重要的数据结构之一,连接了进程、虚拟内存区域(VMA)和页表。
核心概念:
mm_struct 是进程地址空间的“总指挥”,它记录了进程的代码段、数据段、堆、栈、内存映射等所有虚拟内存区域的布局和属性。
3.1 进程地址空间布局
一个典型的用户态进程的虚拟地址空间布局如下(以 x86-64 架构为例,地址空间分为用户空间和内核空间):
3.2 mm_struct 关键字段解析
mm_struct 定义在 include/linux/mm_types.h 中。以下是其核心字段的解析:
| 字段名 | 类型 | 描述 |
|---|---|---|
mmap |
struct vm_area_struct * |
指向虚拟内存区域(VMA)链表的头节点,用于遍历进程的所有内存区域。 |
mm_rb |
struct rb_root |
VMA 的红黑树根节点,用于快速查找特定地址所在的 VMA(时间复杂度 O(log n))。 |
pgd |
pgd_t * |
指向进程页全局目录(Page Global Directory)的指针,是页表层次结构的根。 |
mm_users |
atomic_t |
共享该 mm_struct 的用户进程/线程数量(包括内核线程借用的情况)。 |
mm_count |
atomic_t |
mm_struct 结构体本身的内存引用计数,用于决定何时释放该结构体。 |
map_count |
int |
当前进程中 VMA 的数量。 |
start_code, end_code |
unsigned long |
代码段的起始和结束虚拟地址。 |
start_data, end_data |
unsigned long |
数据段的起始和结束虚拟地址。 |
start_brk, brk |
unsigned long |
堆的起始地址(start_brk)和当前堆的结束地址(brk,即堆顶)。 |
start_stack |
unsigned long |
栈的起始地址。 |
arg_start, arg_end |
unsigned long |
命令行参数(argv)在栈中的起始和结束地址。 |
env_start, env_end |
unsigned long |
环境变量(envp)在栈中的起始和结束地址。 |
total_vm |
unsigned long |
进程总共使用的虚拟内存页数(所有 VMA 的页数总和)。 |
locked_vm |
unsigned long |
被锁定在物理内存中、不允许换出的页数(mlock)。 |
rss_stat |
struct mm_rss_stat |
记录进程的常驻内存(Resident Set Size)统计信息。 |
设计要点:
mmap 链表和 mm_rb 红黑树同时维护了 VMA 的两种组织方式。链表用于遍历所有 VMA(如 /proc/pid/maps 的读取),红黑树用于按地址快速查找(如缺页异常处理)。
3.3 mm_struct 与进程、线程的关系
在 Linux 中,进程和线程都使用 task_struct 表示。它们与 mm_struct 的关系如下:
- 进程: 每个进程拥有一个独立的
mm_struct,即独立的地址空间。 - 线程: 同一进程内的所有线程共享同一个
mm_struct,即共享地址空间。线程的task_struct中的mm指针指向同一个mm_struct。 - 内核线程: 内核线程没有用户地址空间,其
task_struct中的mm字段为NULL。
3.4 内核线程与用户进程的 mm 区别
内核线程(kthread)和用户进程在 mm_struct 的使用上存在根本区别:
| 特性 | 用户进程 | 内核线程 |
|---|---|---|
| mm 字段 | 指向自己独立的 mm_struct |
通常为 NULL |
| 地址空间 | 拥有完整的用户空间 + 内核空间 | 只拥有内核空间,没有用户空间 |
| 页表 | 使用自己的页表(通过 pgd) |
直接使用内核页表(init_mm.pgd) |
| 缺页异常 | 可以触发用户空间缺页,内核会为其分配物理页 | 不会触发用户空间缺页;内核空间缺页通常是 BUG |
| VMA | 有完整的 VMA 链表和红黑树 | 没有 VMA(因为不需要管理用户空间) |
| 内存回收 | 参与用户页面回收和交换 | 不参与用户页面回收,但内核内存可回收 |
| 上下文切换 | 切换 mm 和 pgd,刷新 TLB |
不切换 mm,但会借用上一个进程的 mm(active_mm) |
关键机制:active_mm
当内核线程被调度运行时,它的
当内核线程被调度运行时,它的
mm 为 NULL。为了访问内核空间,内核线程会“借用”上一个用户进程的 mm_struct,并将其指针保存在 task_struct 的 active_mm 字段中。这避免了为内核线程单独维护页表,同时保证了 TLB 的正确性。
3.5 mm_struct 的生命周期与操作
mm_struct 的生命周期由引用计数管理:
- 创建: 通过
fork()系统调用创建进程时,内核调用copy_mm()复制父进程的mm_struct(写时复制)。 - 共享: 通过
clone()创建线程时,CLONE_VM标志使子线程共享父线程的mm_struct(mm_users递增)。 - 借用: 内核线程调度时,设置
active_mm借用其他进程的mm_struct(mm_count递增)。 - 释放: 当
mm_users降为 0 时,内核开始清理地址空间(释放所有 VMA 和页表)。当mm_count也降为 0 时,mm_struct本身被释放。
关键函数调用链:
fork() → copy_process() → copy_mm() → dup_mm() → dup_mmap()(复制 VMA)exit() → exit_mm() → mmput() → mmdrop() → free_mm()