K03 —— 进程创建时的内存复制:copy_page_range 和 COW 细节

金句:fork 的优雅,在于它把”复制”变成了”共享,直到不得不”。


fork 中的内存复制流程

fork 系统调用的核心是 copy_process,其中内存相关的部分是 copy_mm,它调用 copy_page_range 来复制父进程的地址空间。

Bash
fork 内存复制的调用链:

sys_fork() → do_fork() → copy_process()
  → copy_mm() → dup_mm()
      → dup_mmap() → copy_page_range()
          → copy_pud_range() → copy_pmd_range() → copy_pte_range()

copy_page_range 遍历父进程的每个 VMA,为子进程创建对应的页表条目:

Bash
copy_page_range() 的核心逻辑:

int copy_page_range(struct mm_struct *dst, struct mm_struct *src,
                    struct vm_area_struct *vma)
{
    unsigned long addr = vma->vm_start;
    unsigned long end = vma->vm_end;

    for (; addr < end; addr += PAGE_SIZE) {
        // 1. 为子进程分配新的 PTE
        // 2. 复制 PTE 内容
        // 3. 设置 COW 标志(只读)
        // 4. 共享物理页面(引用计数 +1)
    }
}

COW 在 fork 中的具体实现

Bash
fork 时 COW 的关键步骤:

1. 复制页表条目(PTE),但共享物理页面
   - 父 PTE:物理页面 + RW 位
   - 子 PTE:同样的物理页面 + RO 位(只读)
   - 物理页面的引用计数 +1

2. 设置页面为只读
   - 父子都有页表指向同一个物理页
   - 页面被标记为只读
   - 当任一方写入时,触发 page fault

3. Page Fault 处理 COW
   - Page fault handler 发现:
     a. 页面存在
     b. 只读
     c. 引用计数 > 1(共享)
   - 分配新页面
   - 复制旧页面内容
   - 建立新页面的映射
   - 把写入方的 PTE 更新为新页面 + RW

关键数据结构:
  struct page {
      atomic_t _refcount;  // 引用计数
      // ...
      unsigned long flags; // PG_dirty / PG_writeback 等
  };

  PG_referenced 标志:
    - 页面最近被访问
    - COW 时检查这个标志
    - 如果是 clean + referenced,优先不回收

文件映射的 COW 特殊处理

Bash
文件映射的 fork COW 与匿名映射不同:

匿名映射(MAP_ANON):
  - 页面内容是内存(全是零或匿名数据)
  - COW 时直接分配新页,复制内容
  - 不需要写回文件系统

文件映射(MAP_FILE):
  - 页面内容来自文件
  - COW 时需要先读取文件内容到新页
  - 写时复制后变成"私有"(不再同步到文件)

writeback 处理:
  - COW 后的脏页不会写回原文件
  - 只对当前进程可见
  - 进程退出时释放(不回写文件)

MAP_SHARED 的处理:
  - fork 时不复制页表(共享同一个 VMA)
  - 父子共享同一个 page cache
  - 写入会直接修改 page cache(立即对其他进程可见)
  - 不是 COW,是真正的共享

vfork 和 clone 的特殊处理

Bash
vfork(virtual fork)的特殊处理:

vfork() 和 fork() 的区别:

fork():
  - 复制整个地址空间(COW)
  - 父子可以独立运行
  - 写时复制,内存可能翻倍

vfork():
  - 完全共享父进程地址空间(不复制)
  - 子进程先运行,调用 exec 或 _exit 后父进程才继续
  - 效率更高(但有风险)
  - 现代 Linux 实际用 clone(CLONE_VM) 实现

clone(CLONE_VM) 的效果:
  - 父子共享同一个 mm_struct
  - 没有 COW,没有页表复制
  - 共享所有 VMA 和页表
  - 立即 exec 时效率最高(不复制任何内存)

vfork 的风险:
  - 如果子进程不调用 exec/_exit,父进程无法运行
  - 子进程修改内存会影响父进程(完全共享)
  - 现代代码很少用 vfork,fork + COW 更安全

总结

  • fork 内存复制:copy_page_range 遍历 VMA,复制页表但共享物理页,标记只读(COW)
  • COW 实现:页表条目只读 + 物理页引用计数 +1 → 写入时 page fault → 分配新页 → 复制内容 → 更新 PTE
  • 匿名 vs 文件映射:匿名 COW 直接复制,文件 COW 读文件内容到新页(私有化)
  • MAP_SHARED:fork 时不复制,父子共享同一个 page cache,写入立即对其他进程可见
  • vfork:完全共享地址空间(不复制),子进程先运行直到 exec/_exit,现代用 clone(CLONE_VM)

下篇预告(K04):当内存耗尽时,内核是怎么选择杀掉哪个进程的?OOM Killer 的评分机制和 cgroup 内存限制。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年1月8日

作者

评论

发表评论

您的邮箱地址不会被公开。