4、mm_struct的创建与销毁:fork()时的mm复制、exec()时的mm替换、exit()时的mm释放
进程地址空间的生命周期与进程本身紧密绑定。在Linux内核中,mm_struct(内存描述符)的创建、复制、替换和销毁分别对应着进程生命周期中的关键系统调用:fork()、exec() 和 exit()。理解这些操作背后的内核机制,是掌握进程内存管理的基础。
核心要点:
mm_struct 的引用计数 mm_users 和 mm_count 是管理其生命周期的关键。创建时计数递增,销毁时计数递减,只有当所有引用都被释放时,内核才会真正回收该结构体。
4.1 fork() 时的 mm 复制:写时复制的起点
当进程调用 fork() 创建子进程时,内核并不会立即为子进程分配全新的物理内存页来复制父进程的整个地址空间。相反,它采用了一种高效的策略——写时复制(Copy-on-Write, COW)。在 fork() 瞬间,父子进程共享相同的物理内存页,但内核会将所有页表项标记为只读。
在 fork() 系统调用的核心函数 copy_process() 中,会调用 copy_mm() 函数来处理内存描述符的复制。其核心流程如下:
- 复制或共享
mm_struct: 内核检查父进程的mm_struct是否为内核线程(mm == NULL)。对于普通用户进程,copy_mm()会调用dup_mm()。 - 分配新的
mm_struct:dup_mm()首先通过allocate_mm()从 slab 分配器中分配一个新的mm_struct结构体。 - 复制核心字段: 将父进程
mm_struct中的大部分字段(如pgd、mmap、mm_rb、start_code、end_data等)复制到子进程的新结构体中。 - 复制页表与VMA: 调用
dup_mmap()函数,遍历父进程的 VMA 链表,为每个 VMA 创建副本。关键的是,dup_mmap()会调用copy_page_range()来复制页表,但此时物理页框并未复制,而是共享,且页表项被设置为只读。 - 设置写时保护: 在复制页表的过程中,内核会清除所有页表项的写权限位,并设置
PAGE_COW标志。这样,当父子进程中任何一个尝试写入共享页面时,都会触发缺页异常,内核在异常处理中完成真正的物理页复制。
性能优化: 写时复制机制避免了
fork() 时大量不必要的物理内存拷贝。如果子进程立即执行 exec(),则无需复制父进程的地址空间,从而节省了大量时间和内存。
4.2 exec() 时的 mm 替换:加载新程序的地址空间
当进程调用 exec() 族函数(如 execve)加载并执行一个新程序时,内核需要丢弃旧的地址空间,并为新程序创建全新的内存布局。这个过程称为 mm 替换。
exec() 的核心实现函数是 exec_mmap(),它执行以下关键步骤:
| 步骤 | 函数/操作 | 描述 |
|---|---|---|
| 1 | unmap_old_mm() |
释放旧进程地址空间中的所有 VMA 和页表,但保留 mm_struct 结构体本身。 |
| 2 | mm_alloc() |
分配一个新的 mm_struct 结构体,并初始化其核心字段(如 pgd 指向新页全局目录)。 |
| 3 | init_new_context() |
初始化新地址空间的架构特定上下文(如 x86 的 LDT 段)。 |
| 4 | arch_pick_mmap_layout() |
根据新程序的二进制格式和架构,选择内存映射布局(如栈、堆、mmap 区域的起始地址)。 |
| 5 | load_elf_binary() |
解析 ELF 可执行文件,根据程序头表创建新的 VMA,并将代码段、数据段等映射到新地址空间。 |
| 6 | set_mm_exe_file() |
将新 mm_struct 的 exe_file 字段指向可执行文件的 file 结构体。 |
| 7 | switch_mm() |
将新 mm_struct 分配给当前进程,并刷新 TLB(转换后备缓冲器)。 |
注意: 在
exec() 过程中,旧 mm_struct 的引用计数会递减。如果它是最后一个引用(例如,没有其他线程共享该地址空间),则旧 mm_struct 会被释放。新 mm_struct 的引用计数初始化为 1。
4.3 exit() 时的 mm 释放:进程地址空间的销毁
当进程调用 exit() 系统调用终止时,内核需要回收该进程占用的所有内存资源,包括 mm_struct 本身。这个过程由 exit_mm() 函数触发。
exit_mm() 的执行路径如下:
- 解除关联: 将当前进程的
mm字段设置为NULL,使其不再指向任何地址空间。此时,进程变为一个“匿名”状态,不再拥有用户空间。 - 递减引用计数: 调用
mmput()函数,递减mm_struct中的mm_users字段。这个字段表示有多少个进程(或线程)正在使用该地址空间。 - 条件销毁: 如果
mm_users降为 0,说明没有其他进程共享该地址空间,mmput()会继续调用mmdrop()。 - 最终释放:
mmdrop()递减mm_count字段。当mm_count也降为 0 时,表示内核中所有对该mm_struct的引用都已释放,此时调用free_mm()将mm_struct结构体归还给 slab 分配器。
关键数据结构:
mm_struct 中有两个引用计数:atomic_t mm_users 和 atomic_t mm_count。mm_users 是“用户态引用计数”,每多一个进程或线程共享该地址空间就加 1;mm_count 是“内核态引用计数”,只要有内核代码路径持有指向该 mm_struct 的指针(如通过 get_task_mm() 获取),就加 1。这种双层计数机制确保了在用户态引用全部释放后,内核仍能安全地完成清理工作。
4.4 关键函数与数据结构总结
下表总结了 mm_struct 生命周期中涉及的核心函数及其作用:
| 函数 | 调用场景 | 主要作用 |
|---|---|---|
copy_mm() |
fork() |
复制父进程的 mm_struct 和 VMA,设置写时复制。 |
dup_mm() |
fork() |
分配新的 mm_struct 并复制核心字段。 |
dup_mmap() |
fork() |
复制 VMA 链表和页表。 |
exec_mmap() |
exec() |
释放旧地址空间,分配并初始化新 mm_struct。 |
exit_mm() |
exit() |
解除进程与 mm_struct 的关联,触发释放流程。 |
mmput() |
exit() / 内核线程退出 |
递减 mm_users,若为 0 则调用 mmdrop()。 |
mmdrop() |
mmput() 内部 |
递减 mm_count,若为 0 则调用 free_mm()。 |
free_mm() |
mmdrop() 内部 |
将 mm_struct 归还给 slab 分配器。 |
内核线程的特殊性: 内核线程没有用户地址空间,其
task_struct->mm 字段为 NULL。当内核线程调用 exit() 时,exit_mm() 会直接返回,不会执行任何释放操作。内核线程在需要访问用户空间时,会临时借用其他进程的 mm_struct(通过 use_mm() / unuse_mm())。