1316 字
7 分钟
[programming] 状态机、页表与写时复制:深度解析 fork() 的内存实现机制

引言#

根据系统调用手册(man page)的定义,fork() 的核心功能是创建一个新进程(creates a new process)。该调用在子进程中返回 0,而在父进程中返回子进程的进程 ID(PID)。从逻辑语义上看,子进程获得了父进程状态机的完整副本。

然而,在物理实现层面,如果进程占用大量内存(如 1GB 以上),直接进行物理拷贝将导致巨大的延迟与资源浪费。Linux 通过 MMU、页表与写时复制(COW) 机制,在维持逻辑一致性的前提下,实现了高效的进程创建。

1. MMU:虚拟地址空间抽象#

每个进程都拥有独立的虚拟地址空间,这是通过 MMU (Memory Management Unit) 实现的硬件抽象。

  • 虚拟地址与物理地址:程序操作的是虚拟地址,MMU 负责将其翻译为真实的物理地址。
  • 页表 (Page Table):由内核维护的映射关系表,记录了虚拟页(Virtual Page)到物理页框(Physical Frame)的对应关系。
  • 进程隔离:不同进程拥有各自独立的页表,指向不同的物理地址空间,确保了进程间的内存安全。

2. COW (Copy-on-Write):写时复制机制#

在执行 fork() 时,内核采用延迟处理策略,避免不必要的物理拷贝:

  1. 页表复制:内核仅复制父进程的页表项(Page Table Entries)给子进程。此时,父子进程的虚拟地址映射到相同的物理页面
  2. 读写权限限制:内核将这些共享的物理页面在页表中标记为 ReadOnly(只读)。
  3. 延迟开销:由于仅涉及页表结构的复制而无需搬运数据,进程创建操作可在微秒级完成。

3. Page Fault:硬件与内核的协同处理#

当任一进程尝试对标记为只读的共享页面进行写操作时,将触发以下硬件与内核协作流程:

3.1 异常触发#

CPU 检测到写指令违反了页表的只读权限,立即触发一个 Page Fault (缺页异常) 并陷入内核态。

3.2 内核介入#

操作系统内核捕获异常后,检查报错原因:

  • 若属于非法内存访问,则发送 SIGSEGV
  • 若属于 COW 导致的正常写入尝试,则启动写时复制逻辑。

3.3 物理页拷贝#

内核在物理内存中分配一个新的页框(通常为 4KB),将原页面数据完整拷贝至新页框,并更新当前进程的页表,使其指向这个私有的新物理地址。

3.4 指令重执行#

内核将新页面的权限修改为可写,然后让 CPU 重新执行刚才失败的写指令。此时,两个进程在物理层面上完成了数据的正式分离。

4. 实验观察:fork() 后的内存行为#

通过 C 程序可以验证:fork() 后变量的虚拟地址保持一致,但修改操作会触发底层物理页的分离。

#include <stdio.h>
#include <unistd.h>
int global_var = 100;
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程尝试修改变量,触发硬件 Page Fault 和内核 COW 机制
global_var = 200;
printf("Child: var = %d, addr = %p\n", global_var, &global_var);
} else {
// 父进程保持不变,观察其物理隔离性
sleep(1);
printf("Parent: var = %d, addr = %p\n", global_var, &global_var);
}
return 0;
}

5. 硬件层面的底层实现细节#

5.1 寄存器层面的状态区分 (RAX 的作用)#

在 x86-64 架构下,fork() 通过 syscall 进入内核。当调用返回时,父子进程的指令指针(RIP)均指向相同的下一条指令,其逻辑分歧点在于 rax 寄存器:

  • 父进程:内核将子进程的 PID 存入 rax
  • 子进程:内核强制将子进程状态机中的 rax 置为 0
  • 本质:C 语言中的 if (pid == 0) 本质上是在检测 rax 寄存器的状态值。

5.2 多级页表结构#

Linux 采用 4 级(或 5 级)页表结构来管理 64 位地址空间:

  • 层级关系:PGD (Page Global Directory) -> PUD (Page Upper Directory) -> PMD (Page Middle Directory) -> PTE (Page Table Entry)。
  • 稀疏存储:仅为实际使用的内存区域分配底层页表,极大节省了元数据开销。
  • 分层复制fork() 时内核主要复制顶级页表目录(PGD),底层页表可随 COW 机制按需生成。

5.3 CR3 寄存器:地址空间切换#

在 x86 架构中,CPU 使用 CR3 寄存器 存储当前进程顶级页表(PGD)的物理基地址:

  • 上下文切换:当 OS 调度器切换进程时,通过修改 CR3 的值,即可瞬间将 CPU 寻址环境切换到目标进程的地址空间。

总结#

fork() 的实现体现了计算机系统中经典的 “延迟处理 (Lazy Evaluation)” 思想。通过引入页表这一间接层,Linux 将沉重的物理内存拷贝操作推迟到“确需修改”的时刻,从而在保证进程隔离性的前提下实现了极高的系统性能。

参考资料#

  • 《Operating Systems: Three Easy Pieces》 (OSTEP)
  • 南京大学《操作系统原理》- 蒋炎岩
  • Intel® 64 and IA-32 Architectures Software Developer’s Manual
[programming] 状态机、页表与写时复制:深度解析 fork() 的内存实现机制
https://www.eustia-astraea.top/posts/programming/linux-memory-fork-cow/
作者
mcsl
发布于
2025-03-27
许可协议
CC BY-NC-SA 4.0