当前位置:首页>Linux>深挖 Linux 内存:为什么 fork 后的父子进程写同一个页,却没人能复用它?

深挖 Linux 内存:为什么 fork 后的父子进程写同一个页,却没人能复用它?

  • 2026-09-10 07:31:00
深挖 Linux 内存:为什么 fork 后的父子进程写同一个页,却没人能复用它?

一、 那个深深刻在脑海里的直觉模型

在学习 Linux 内核时,我们对 fork + COW(写时复制)的直觉通常是这样的:

  1. 父进程拥有一个物理页 P0,mapcount = 1。

  2. fork() 发生:父子进程共享 P0,页面被设为只读,mapcount = 2。

  3. 关键点: 谁先写,谁就触发 COW。比如父进程先写,它发现页面被共享,于是 copy 出一个新页 P1。

  4. 结局: 此时 P0 的 mapcount 降回 1。等子进程再写时,发现 P0 已经是它独占了,直接执行 Reuse(复用)。

一句话总结:一个 COW,一个 Reuse。 听起来逻辑严密,教科书也是这么写的。但在 Linux 6.17 的真实环境下,实验结果却“打”了我的脸。



二、 诡异的实验:谁也没占到便宜

环境环境:Qemu + Linux 6.17 ,参考‘深入理解 Linux 内核:匿名页缺页中断处理机制(do_anonymous_page)’。

应用测试代码:
 char *p = mmap(NULL, PAGE,                   PROT_READ | PROT_WRITE,                   MAP_PRIVATE | MAP_ANONYMOUS,                   -1, 0);p[0] = 1;pid_t pid = fork();p[0] = 2;
在内核源码mm/memory.c中加入打印调试:
实际log输出情况:
PID 1712: mmap done, p=0x7fc1faa1a000...[  525.906058] [do_wp_page 3930]: 1712 do_wp_page flag 1a55, addr 7fc1faa1a000[  525.909179] [do_wp_page 4020]: 1712 call wp_page_copy with mapcount 2, addr 7fc1faa1a000[  525.913167] [do_wp_page 3930]: 1713 do_wp_page flag 1a55, addr 7fc1faa1a000[  525.916198] [do_wp_page 4020]: 1713 call wp_page_copy with mapcount 2, addr 7fc1faa1a000[  525.920047] [do_wp_page 3930]: 1712 do_wp_page flag 1a55, addr 7fc1fa836000

 PID 1712(父进程)和 1713(子进程)全部触发了 copy。原本以为能被复用的原始页 P0,在 mapcount 归零后被系统回收了。

为什么? 既然是单核,难道没有先后顺序吗?


三、内核深度解密:消失的“瞬间”

我们要去 mm/memory.c 的 do_wp_page 函数里找答案。内核判断是“拷贝”还是“复用”的逻辑核心如下:

1. 竞争的本质

即使是单核,父进程在触发写异常进入内核态后,如果在完成 P0→P1 的拷贝并更新 PTE(页表项)之前,由于某种原因(如时间片耗尽或抢占)切换到了子进程,子进程也触发写异常。

此时,关键时刻到了:

  • 父进程的 COW 还没走完,PTE 还没改。

  • 子进程去看 P0 的 mapcount,依然是 2。

  • 内核心想:“既然还有别人在用,那我也 copy 一个吧。”

结果就是:大家都觉得自己不是最后一个使用者,大家都选择了“背锅”拷贝。

2. 时序决定命运

随后做了一组对比实验:在子进程写操作前加一个 sleep(1),强制拉开两者的距离。

 char *p = mmap(NULL, PAGE,                   PROT_READ | PROT_WRITE,                   MAP_PRIVATE | MAP_ANONYMOUS,                   -1, 0);p[0] = 1;pid_t pid = fork();if (pid == 0) {        /* child */        sleep(1);        p[0] = 2;        exit(0);} else {        /* parent */        p[0] = 3;        wait(NULL);}
内核输出结果
PID 1906: mmap done, p=0x7f1ed83ac000......[ 5826.114947] [do_wp_page 3930]: 1906 do_wp_page flag 1a55, addr 7f1ed83ac000[ 5826.115752] [do_wp_page 4020]: 1906 call wp_page_copy with mapcount 2, addr 7f1ed83ac000[ 5827.120510] [do_wp_page 3930]: 1907 do_wp_page flag 1a55, addr 7f1ed83ac000[ 5827.123148] [do_wp_page 4004]: 1907 call wp_page_reuse with mapcount 1, addr 7f1ed83ac000

结果:父进程 COW,子进程 Reuse。 实验验证了我的猜想:Reuse 成功的前提是,第一个进程的 COW 流程必须完全走完,把旧页的 mapcount 彻底降下来。


四、总结
  1. linux 的 COW 是“基于瞬时状态判断的竞争式实现(racy but correct)”。fork 后是否发生 1 次还是 2 次 COW,是实现细节,与调度有关。
  2. 内核的正确性vs性能:内核首先保证的是数据的独立(Correctness),至于能不能 Reuse 节省那一页内存,取决于调度器脸色。
有时候我们请教前辈/大牛问题,他们有时候会说“hmm,it's hard to say..."。当时不清楚为什么这么难说,现在想想,有些问题有时候确实还挺难说的...

最新文章

随机文章