B10 — Why fork is Fast: The Copy-On-Write Mechanism
Key Insight: Don’t copy, just share. Until it’s actually needed.
The Traditional Problem with fork: Massive Memory Copying
The fork system call creates a new process. The traditional implementation copies the entire memory space of the parent process:
传统 fork 的问题:
父进程内存:假设 1GB
fork() 执行时:
- 复制所有页表(PTE)
- 复制所有物理页面(1GB)
- 创建子进程的页表和页面
问题:
1. 时间:复制 1GB 内存 → 耗时 100~500ms
2. 空间:fork 后内存翻倍(2GB),即使子进程只用 10MB
3. 浪费:fork + exec 的经典 Unix 模式,子进程会立即丢弃复制的内存
这就是为什么传统 Unix 的 fork 是"昂贵"的操作。The Core Idea of Copy-On-Write (COW)
The core of COW is: Do not copy content, only copy page table reference counts.
COW 的基本原则:
1. fork 时:
- 不复制物理页面
- 只复制父进程的页表(指向同样的物理页面)
- 把页面标记为"只读"(Read-Only)
- 父子共享同一物理页面
2. 父或子写入时:
- 触发 page fault(因为页面是只读的)
- OS 分配新物理页
- 复制内容到新页
- 更新页表,指向新页
- 把新页标记为"读写"
3. 结果:
- fork 很快(只复制页表,不复制页面)
- 实际复制延迟发生在第一次写入时
- 如果子进程只读内存(如 exec 后),永远不复制COW Implementation Details
fork 中的 COW 流程:
sys_fork() → copy_process() → copy_page_range()
copy_page_range() 的实现:
int copy_page_range(struct mm_struct *dst, struct mm_struct *src,
struct vm_area_struct *vma)
{
for each page in vma:
if (is_cow_page(page)) {
// COW 页面,共享但标记为只读
get_page(page); // 引用计数 +1
set_pte(pte, readonly); // 设置只读位
flush_tlb(); // 刷新 TLB
}
}
}
关键:
- 引用计数 > 1(共享),页面只读
- 写入触发 #PF(Page Fault)
- page fault handler 分配新页,复制内容,继续执行写入时的 Page Fault 处理:
do_page_fault():
// Page Fault 处理
if (!(vma->vm_flags & VM_WRITE)) {
// 写入只读页面 → COW!
// 1. 分配新物理页
new_page = alloc_page(GFP_KERNEL);
// 2. 复制旧页内容到新页
copy_page(new_page, old_page);
// 3. 更新页表,指向新页
set_pte(addr, new_page | R/W); // 可读写
// 4. 释放旧页的引用
put_page(old_page);
// 5. 继续执行写入指令
return;
}
// 不是 COW,可能是真正的错误
// 发送 SIGSEGVBenefits of COW
COW 的性能和空间优势:
场景 1:fork + exec(最常见)
父进程 500MB(代码+数据)
fork 后共享 500MB(只读)
exec() 丢弃所有页面,重新加载可执行文件
无 COW:500MB 复制 → 耗时 200ms,浪费 500MB
有 COW:0 复制(exec 前都是只读共享)→ 耗时 <1ms
场景 2:fork 后子进程只读
父进程 1GB
fork 后子进程只读(不做任何写入)
无 COW:1GB 复制 → 1GB 内存占用
有 COW:共享 1GB(只读)→ 0 额外占用
场景 3:fork 后子进程写入少量内存
父进程 1GB
fork 后子进程写入 4KB
无 COW:1GB 复制 → 1GB 额外占用
有 COW:只复制被写入的页(如 4KB)→ 额外 4KB
大量未修改的页面仍然共享
实际测试(fork + exec):
无 COW:~300ms(复制 1GB)
有 COW:<1ms(只复制页表)COW Side Effects and Considerations
COW 的副作用:
1. 内存峰值:
如果父进程 100% 使用内存(1GB),
fork 后的子进程如果立即大量写入,
会立即分配 1GB 新内存(COW 触发)
→ 内存峰值可能达到 2GB
2. 页面共享的限制:
- 只读页面(代码段)可以长期共享
- 写入后 COW,共享结束
- 内存碎片化(不同进程的页面在不同位置)
3. 虚拟内存统计的复杂性:
- /proc/PID/status 的 VmRSS 可能不反映实际内存
- COW 页面的引用计数 > 1 时,实际占用少
$ cat /proc/PID/smaps | grep -A 5 "Shared"
0000000000400000 4KB r-xp ... // 可执行代码,共享
... Shared_Clean: 4KB, Shared_Dirty: 0KB
$ cat /proc/PID/status | grep -i vm
VmRSS: 512000 kB // 包括 COW 共享的页(只计一份)
VmSwap: 0 kB // 没有 swapCopy-on-Write and Demand Paging
fork 之后的内存分配有多种模式:
1. COW(Copy-On-Write):
- fork 时共享,只读
- 写入时复制
- 延迟复制,直到真正需要
2. 写时复制 vs 按需分配(Demand Allocation):
- fork + exec:COW 效果好(exec 前只读)
- fork + 立即写入:COW + 延迟复制
3. exec() 的内存处理:
- exec() 丢弃所有页面
- 按需加载可执行文件(mmap)
- 代码段按需分页(demand paging)
- 数据段按需分配(demand allocation)
fork + exec 的最优化:
fork 共享所有读页面
exec 丢弃所有页面,重新加载
结果:fork 几乎免费,exec 按需加载Summary
- Traditional fork Problem: Copies entire memory space; takes 100-500ms; memory doubles
- COW Core: fork does not copy pages, only copies page table references; marks them read-only; shares pages
- Write Trigger: Write to read-only page → Page Fault → allocate new page → copy content → continue execution
- COW Advantages: fork is fast (only page tables copied), memory shared (zero copy), on-demand copying (lazy)
- Practical Effect: fork + exec (without COW 300ms → with COW <1ms); read-only after fork (zero additional memory)
- Side Effects: Memory peak may double; page sharing relies on read-only attribute
Next (B11): What happens when a child process modifies data after fork? Complete Page Fault handling flow and COW page allocator call path.
Comments