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 流程图展示完整生命周期:

mm_struct 生命周期流程图 fork() / mm_init() CLONE_VM? 共享 mm_struct(线程) 复制 mm_struct(进程) mm_users++,pgd 指向同一页表 dup_mm():复制 VMA 和页表 mmput() / mmdrop() mm_users 归零 → mmput() → mm_count 归零 → mmdrop() → 释放

3.3.1 创建阶段

当进程调用 fork() 或 clone() 系统调用时,内核通过 copy_mm() 函数处理内存描述符。创建逻辑如下:

  1. 分配 mm_struct:调用 allocate_mm() 从 slab 分配器获取一个 mm_struct 对象。
  2. 初始化:mm_init() 设置初始值,包括 mm_users = 1、mm_count = 1、pgd = NULL。
  3. 分配页表:调用 mm_alloc_pgd() 分配进程的页全局目录。
  4. 设置默认段:根据 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 缓存
    }
}

销毁流程的关键路径:

  1. 进程退出:调用 exit_mm() 将当前进程的 mm 置为 NULL,并调用 mmput()。
  2. mmput():递减 mm_users,若归零则执行 exit_mmap() 释放所有 VMA 和页表。
  3. mmdrop():递减 mm_count,若归零则调用 free_mm() 将 mm_struct 归还给 slab 分配器。
为什么需要两个引用计数? mm_users 跟踪共享地址空间的用户态进程数,当最后一个用户退出时,必须立即释放页表和 VMA 以回收物理内存。而 mm_count 跟踪内核中所有持有该结构体指针的模块(如内核线程、驱动),确保在用户态全部退出后,内核仍能安全访问 mm_struct 中的统计信息,直到所有内核引用也释放。

下面通过 SVG 图展示引用计数变化与销毁触发条件:

mm_users 与 mm_count 协同销毁机制 mm_users = 1 mm_count = 1 mm_users = N mm_count = 1 mm_users = 0 mm_count = K mm_users 归零? mm_count 归零? exit_mmap() 释放 VMA + 页表 free_mm() 归还 slab 缓存 阶段1:创建 阶段2:共享 阶段3:销毁

3.4 关键数据结构关系图

最后,通过一张综合 SVG 图展示 mm_struct 与进程描述符、VMA、页表之间的关系:

mm_struct 与周边数据结构关系 task_struct mm / active_mm mm_struct mmap / mm_rb / pgd vm_area_struct 链表 + 红黑树节点 页表层级 PGD → P4D → PUD → PMD → PTE 物理页框 struct page mmap 链表头 / mm_rb 根 pgd 指针 页表项指向物理页框

通过上图可以清晰看到:task_struct 通过 mm 指针引用 mm_struct;mm_struct 通过 mmap 和 mm_rb 管理所有 VMA,通过 pgd 指向页表层级;页表最终映射到物理页框。这一整套数据结构构成了进程地址空间的完整骨架。