4、mm_struct的初始化与创建:fork()过程中mm_struct的复制机制(copy_mm函数)

在Linux内核中,进程的创建通过fork()、vfork()或clone()系统调用完成,最终都会调用内核函数_do_fork()(或kernel_clone())。进程创建的核心任务之一就是为子进程建立独立的内存地址空间,或者与父进程共享地址空间。这一关键步骤由copy_mm()函数负责执行。

核心要点: copy_mm() 是 fork 过程中内存描述符 mm_struct 复制的枢纽函数,它决定了父子进程是共享地址空间(线程)还是拥有独立的地址空间(进程)。

4.1 fork() 流程中的内存复制入口

当用户态调用 fork() 后,系统调用陷入内核,经过一系列封装函数,最终到达 _do_fork()。该函数会调用 copy_process(),而 copy_process() 内部会按顺序复制进程的各个子系统,其中就包括内存描述符的复制。

调用链如下:

fork() / vfork() / clone()
    └─> _do_fork() (或 kernel_clone())
        └─> copy_process()
            ├─> dup_task_struct()      // 复制 task_struct
            ├─> copy_semundo()
            ├─> copy_files()
            ├─> copy_fs()
            ├─> copy_sighand()
            ├─> copy_signal()
            ├─> copy_mm()              // <-- 本章核心:复制 mm_struct
            ├─> copy_namespaces()
            ├─> copy_io()
            └─> copy_thread()          // 复制线程上下文
关键观察: copy_mm() 在 copy_process() 中处于中间位置,它依赖于之前已经复制好的 task_struct,同时为后续的 copy_thread() 提供内存环境。

4.2 copy_mm() 函数核心逻辑

copy_mm() 函数定义在 kernel/fork.c 中,其核心逻辑可以分为三个阶段:初始化检查、策略判断、深度复制或共享。

以下为 copy_mm() 的简化核心代码(基于 Linux 5.x 内核):

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
    struct mm_struct *mm, *oldmm;
    int retval;

    tsk->min_flt = tsk->maj_flt = 0;
    tsk->nvcsw = tsk->nivcsw = 0;

    // 1. 获取父进程的 mm_struct
    oldmm = current->mm;
    if (!oldmm)
        return 0;  // 内核线程没有 mm_struct,直接返回

    // 2. 判断是否共享地址空间(CLONE_VM 标志)
    if (clone_flags & CLONE_VM) {
        // 共享模式:线程创建
        mm_get(oldmm);          // 增加引用计数
        mm = oldmm;
        goto good_mm;
    }

    // 3. 独立模式:进程创建,需要深度复制
    retval = -ENOMEM;
    mm = dup_mm(tsk, oldmm);   // 核心复制函数
    if (!mm)
        goto fail_nomem;

good_mm:
    // 4. 将新的 mm_struct 赋值给子进程
    tsk->mm = mm;
    tsk->active_mm = mm;
    return 0;

fail_nomem:
    return retval;
}
避坑指南: 初学者容易混淆 mm 和 active_mm。对于普通进程,两者指向同一个 mm_struct;对于内核线程,mm 为 NULL,active_mm 指向它借用的地址空间。在 copy_mm() 中,子进程的 active_mm 被设置为与 mm 相同。

4.3 分支决策:共享 vs 独立

copy_mm() 根据 clone_flags 中的 CLONE_VM 标志做出关键决策:

标志位 系统调用 行为 mm_struct 操作 典型场景
CLONE_VM 置位 clone() 创建线程 共享地址空间 引用计数 +1,不复制 pthread_create()
CLONE_VM 未置位 fork() / vfork() 独立地址空间 调用 dup_mm() 深度复制 普通进程创建
性能差异: 线程创建(共享模式)仅需增加一个引用计数,开销极小;进程创建(独立模式)需要调用 dup_mm(),进而触发 dup_mmap() 遍历所有 VMA 并复制页表,开销较大。这也是线程创建比进程创建快得多的根本原因之一。

4.4 dup_mm():深度复制的核心

当需要独立地址空间时,copy_mm() 调用 dup_mm() 执行真正的复制工作。dup_mm() 的流程如下:

  1. 分配新的 mm_struct:调用 allocate_mm() 从 slab 缓存中分配一个新的 mm_struct 实例。
  2. 初始化新 mm_struct:调用 mm_init() 设置基本字段(如页表锁、MMU 通知器、引用计数等)。
  3. 复制内存映射:调用 dup_mmap(),遍历父进程的 VMA 红黑树,为每个 VMA 创建副本,并复制页表。
  4. 复制上下文数据:复制与架构相关的内存上下文(如 cpu_vm_mask_var、context.id 等)。
  5. 更新统计信息:复制 mm->total_vm、mm->locked_vm 等统计计数器。
static struct mm_struct *dup_mm(struct task_struct *tsk,
                                struct mm_struct *oldmm)
{
    struct mm_struct *mm;
    int err;

    // 1. 分配新的 mm_struct
    mm = allocate_mm();
    if (!mm)
        goto fail_nomem;

    // 2. 复制基本字段并初始化
    memcpy(mm, oldmm, sizeof(*mm));
    mm_init(mm, tsk);  // 重新初始化锁、引用计数等

    // 3. 复制 VMA 和页表
    err = dup_mmap(mm, oldmm);
    if (err)
        goto fail_dupmmap;

    // 4. 复制架构相关上下文
    mm->context = oldmm->context;

    // 5. 更新统计信息
    mm->hiwater_rss = get_mm_rss(mm);
    mm->hiwater_vm = mm->total_vm;

    return mm;

fail_dupmmap:
    mm_free(mm);
fail_nomem:
    return NULL;
}

4.5 dup_mmap():VMA 与页表的逐项复制

dup_mmap() 是内存复制中最繁重的部分,它负责遍历父进程的 VMA 链表,并为每个 VMA 创建副本。其核心步骤包括:

  • 遍历 VMA 红黑树:使用 rb_prev()/rb_next() 或 vma_link() 机制遍历所有 VMA。
  • 分配新 VMA:调用 vm_area_dup() 从 slab 缓存分配新的 vm_area_struct。
  • 复制 VMA 属性:复制 vm_start、vm_end、vm_flags、vm_page_prot 等字段。
  • 处理文件映射:如果 VMA 是文件映射,调用 get_file() 增加文件引用计数。
  • 复制匿名页:对于匿名映射,设置 anon_vma 链接。
  • 复制页表:调用 copy_page_range() 逐级复制页表条目。
  • 插入新 VMA:将新 VMA 插入子进程的 VMA 红黑树和链表中。
写时复制(COW)的入口: 在 dup_mmap() 中,copy_page_range() 复制页表时,会将父进程的物理页设置为只读(清除写权限),并将父子进程的页表项都标记为只读。当任一进程尝试写入时,触发缺页异常,内核才真正复制物理页。这就是写时复制(Copy-On-Write)机制的实现起点。

4.6 整体复制流程 SVG 架构图

下图展示了 fork() 过程中 mm_struct 从 copy_mm() 到 dup_mmap() 的完整复制流程:

fork() 中 mm_struct 复制流程架构图 fork() / clone() 系统调用 copy_process() copy_mm() 入口 检查 CLONE_VM 标志 共享模式:引用计数 +1 独立模式:调用 dup_mm() tsk->mm = oldmm allocate_mm() + mm_init() dup_mmap() 复制 VMA 和页表 tsk->mm = mm (新 mm_struct) 子进程就绪,返回用户态 说明 左分支:线程创建 右分支:进程创建

4.7 关键数据结构变化对比

在 copy_mm() 执行前后,父子进程的关键数据结构关系发生如下变化:

数据结构 fork 前(父进程) fork 后(子进程 - 共享模式) fork 后(子进程 - 独立模式)
task_struct->mm 指向父 mm_struct 指向同一个 mm_struct 指向新的 mm_struct
mm_struct->mm_count 1 2(引用计数增加) 1(新对象)
mm_struct->mm_users 1 2 1
vm_area_struct 父进程 VMA 链表 共享同一组 VMA 独立的新 VMA 链表(内容相同)
页表 父进程页表 共享同一套页表 独立页表(COW 机制)

4.8 vfork() 的特殊处理

vfork() 是一种特殊的 fork 变体,它在 copy_mm() 中的行为与普通 fork() 不同:

  • 不复制页表:vfork() 在 copy_mm() 中设置 CLONE_VM 标志,因此子进程共享父进程的地址空间。
  • 父进程阻塞:子进程在 exec() 或 exit() 之前,父进程被阻塞在 wait_for_completion() 上。
  • 安全保证:由于父子进程共享地址空间,子进程不能修改内存(内核保证子进程在 exec() 前不会写入数据)。
历史演进: 在早期 Linux 内核中,fork() 会立即复制所有物理内存,性能较差。现代内核通过 copy_mm() + dup_mmap() + COW 机制,将物理页的复制推迟到实际写入时发生,大幅提升了 fork 性能。vfork() 则进一步优化了“fork + exec”场景,完全避免了页表复制。

4.9 总结:copy_mm() 的关键设计哲学

copy_mm() 的设计体现了 Linux 内核在内存管理上的几个核心原则:

  • 延迟复制(Lazy Copy):通过 COW 机制,将物理页的复制推迟到写入时,避免不必要的复制开销。
  • 共享优先(Share by Default):线程创建时默认共享地址空间,仅增加引用计数,实现轻量级创建。
  • 分层抽象:copy_mm() 负责高层决策,dup_mm() 负责结构复制,dup_mmap() 负责 VMA 复制,copy_page_range() 负责页表复制,每一层职责清晰。
  • 引用计数管理:通过 mm_count 和 mm_users 两个引用计数器,精确控制 mm_struct 的生命周期。
学习建议: 阅读 copy_mm() 源码时,建议同时打开 dup_mm()、dup_mmap() 和 copy_page_range() 三个函数,沿着调用链逐层深入理解。重点关注 CLONE_VM 标志如何影响整个复制路径的选择。