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 中各边界字段如何划分区域:

进程虚拟地址空间布局与 mm_struct 字段映射 进程虚拟地址空间布局与 mm_struct 字段映射 内核空间 (Kernel Space) 0xC0000000 - 0xFFFFFFFF (1GB) 用户栈 (User Stack) start_stack → 高地址向下增长 内存映射段 / 共享库 (mmap 区域) 堆 (Heap) start_brk → brk (向上增长) BSS 段 (未初始化数据) 数据段 (Data Segment) start_data → end_data 代码段 (Text Segment) start_code → end_code 保留区 / 0 页 (不可访问) 高地址 低地址 说明: mm_struct 通过 start_code/end_code、start_data/end_data、start_brk/brk、start_stack 等字段精确定位各区域边界。

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 就不会被释放。

mm_struct 引用计数生命周期 mm_struct 引用计数生命周期 fork() 创建 mm_users=1, mm_count=1 clone(CLONE_VM) mm_users++ 内核线程借用 mm_count++ exit() 退出 mm_users-- → 0 时清理页表 mmdrop() mm_count-- → 0 时释放

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 统计。