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:

Bash
传统 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.

Bash
COW 的基本原则:

1. fork 时:
   - 不复制物理页面
   - 只复制父进程的页表(指向同样的物理页面)
   - 把页面标记为"只读"(Read-Only)
   - 父子共享同一物理页面

2. 父或子写入时:
   - 触发 page fault(因为页面是只读的)
   - OS 分配新物理页
   - 复制内容到新页
   - 更新页表,指向新页
   - 把新页标记为"读写"

3. 结果:
   - fork 很快(只复制页表,不复制页面)
   - 实际复制延迟发生在第一次写入时
   - 如果子进程只读内存(如 exec 后),永远不复制

COW Implementation Details

Bash
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 分配新页,复制内容,继续执行

Bash
写入时的 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,可能是真正的错误
  // 发送 SIGSEGV

Benefits of COW

Bash
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

Bash
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         // 没有 swap

Copy-on-Write and Demand Paging

Bash
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.


Last modified: 2024年7月15日

Author

Comments

Write a Reply or Comment

Your email address will not be published.