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() 的流程如下:
- 分配新的 mm_struct:调用
allocate_mm()从 slab 缓存中分配一个新的mm_struct实例。 - 初始化新 mm_struct:调用
mm_init()设置基本字段(如页表锁、MMU 通知器、引用计数等)。 - 复制内存映射:调用
dup_mmap(),遍历父进程的 VMA 红黑树,为每个 VMA 创建副本,并复制页表。 - 复制上下文数据:复制与架构相关的内存上下文(如
cpu_vm_mask_var、context.id等)。 - 更新统计信息:复制
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() 的完整复制流程:
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 标志如何影响整个复制路径的选择。