写时复制COW的第一性理解

写时复制(Copy-on-Write,COW)是 Linux 进程创建和内存管理的核心机制之一,理解它需要从进程创建的本质问题出发。(Zygote的底层核心原理, 包括也是flutter单SO的底层优化方案依仗)


第一性:进程创建面临什么根本矛盾

问题 描述
创建进程需要复制内存 fork() 创建子进程时,理论上需要把父进程的整个地址空间复制一份
内存复制太慢 一个进程可能有几百 MB 甚至几 GB 内存,完整复制需要几十毫秒到几秒
复制的内容可能根本用不上 很多内存只是读取,不会被修改,复制是浪费
内存浪费 父子进程各自持有一份相同的数据,物理内存翻倍

如果没有 COW:

scss 复制代码
父进程(1GB 内存)
    │
    ▼ fork()
子进程(1GB 内存)── 完全独立的 1GB 物理内存

结果:2GB 物理内存被占用,但内容一模一样

本质:延迟复制 + 共享只读

COW 的思想极其简单:

fork 时只复制页表(虚拟地址到物理地址的映射),不复制物理内存页。父子进程共享同一块物理内存。只有当某一方试图修改某页时,才复制那一页。

css 复制代码
┌─────────────────────────────────────────┐
│           父进程虚拟地址空间              │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
│  │  页 A   │  │  页 B   │  │  页 C   │ │
│  └────┬────┘  └────┬────┘  └────┬────┘ │
└───────┼────────────┼────────────┼──────┘
        │            │            │
        ▼            ▼            ▼
┌─────────────────────────────────────────┐
│              页表(复制)                │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
│  │ 映射关系 │  │ 映射关系 │  │ 映射关系 │ │
│  └────┬────┘  └────┬────┘  └────┬────┘ │
└───────┼────────────┼────────────┼──────┘
        │            │            │
        └────────────┴────────────┘
                      │
                      ▼
        ┌─────────────────────────┐
        │    物理内存(共享)      │
        │  ┌─────┐┌─────┐┌─────┐ │
        │  │ 页A ││ 页B ││ 页C │ │ ◄── 只读,标记为共享
        │  └─────┘└─────┘└─────┘ │
        └─────────────────────────┘

fork 瞬间:

  • 虚拟地址空间:复制了(页表复制)
  • 物理内存:没有复制,父子指向同一块
  • 页表标记:只读权限

写时复制触发过程

css 复制代码
步骤 1:fork 完成
父进程 ──► 共享物理页 ◄── 子进程
            (只读标记)

步骤 2:子进程尝试修改页 B
        │
        ▼ 触发页错误(Page Fault)
        因为页 B 是只读的,但子进程要写入

        │
        ▼ 操作系统介入
        1. 分配新的物理页 B'
        2. 把原页 B 的内容复制到 B'
        3. 子进程的页表指向 B'(可写)
        4. 父进程的页表仍指向 B(可写)

步骤 3:结果
父进程 ──► 页A ──► 物理页A(共享)
        ──► 页B ──► 物理页B(独有)

子进程 ──► 页A ──► 物理页A(共享)
        ──► 页B ──► 物理页B'(独有,刚复制)

解决的问题

问题 COW 如何解决
fork 太慢 不复制物理内存,fork 瞬间完成(微秒级)
内存浪费 只读数据共享,物理内存只存一份
不必要的复制 没被修改的页永远不会复制
父子进程隔离 修改时自动复制,保证各自独立

核心场景

1. fork() 创建进程(最典型的场景)

c 复制代码
// Linux 中 fork 的 COW 实现
pid_t pid = fork();
if (pid == 0) {
    // 子进程
    // 此时和父进程共享所有物理页
    *ptr = 100;  // 修改这一页时才触发复制
}

效果:

  • 一个 1GB 的进程 fork 后,如果不修改任何数据,物理内存只增加几 KB(页表开销)
  • 修改 10% 的数据,物理内存只增加 100MB

2. 文件映射(mmap)

c 复制代码
// 多个进程 mmap 同一个文件
void* addr1 = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
void* addr2 = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);

效果:

  • 两个进程共享同一份文件内容的物理页
  • 都不修改:内存中只有一份
  • 某个进程修改(MAP_PRIVATE):触发 COW,复制一份给修改者

3. Android Zygote(我们刚聊的)

markdown 复制代码
Zygote 预加载 Framework(只读)
    │
    ▼ fork 出微信
    共享 Framework 物理页
    │
    ▼ 微信加载自己的代码
    修改自己的页,不影响 Zygote 和其他 App

效果:

  • 100 个 App 共享同一份 Framework 内存
  • 物理内存节省数百 MB

4. 字符串/数据结构 copy(编程语言层面)

cpp 复制代码
// C++ std::string 的 COW 实现(C++11 前常见)
std::string s1 = "hello world";  // 分配内存
std::string s2 = s1;             // 不复制,共享引用计数

s2[0] = 'H';  // 此时才触发 COW,复制一份给 s2

注意: C++11 后 string 不再用 COW(因为多线程问题),但其他数据结构仍可用。


为什么 COW 能工作:基于一个关键假设

大部分被共享的内存不会被修改,或者只有少量被修改

如果这个假设不成立(比如 fork 后子进程立即修改所有页),COW 反而更慢:

  • 正常复制:一次性 memcpy,缓存友好
  • COW:逐页触发页错误,逐页复制,缺页中断开销

所以 COW 不是万能优化,它的收益取决于"共享后修改的比例"。


一句话总结

写时复制的本质:用"共享只读 + 延迟复制"替代"立即全量复制",在进程隔离性和内存效率之间取得平衡。核心假设是:共享的数据大多不会被修改。


具体总结

写时复制是 Linux 的一种内存优化机制。传统 fork 创建子进程时需要完整复制父进程的物理内存,既慢又浪费内存。COW 的做法是:fork 时只复制页表,让父子进程共享同一块物理内存,并标记为只读。当任何一方试图修改某页时,操作系统才分配新的物理页并复制内容。这样,没被修改的页永远共享,大幅节省内存和创建时间。Android Zygote 就是典型应用,所有 App 共享同一份预加载的 Framework 内存。

相关推荐
sTone873751 小时前
Zygote的第一性理解
android
sTone873751 小时前
类RN框架通过Service暴露卡片渲染能力给AI Chat
android·react native
灯火不休1 小时前
Android Studio & Flutter 构建命令完全指南
android
杉氧1 小时前
Ktor 全栈之路 (5):JWT 认证全流程实战 —— 打造安全通信闭环
android·架构·kotlin
CHANG_THE_WORLD1 小时前
逐层拆解:C++ 虚函数从对象内存到手工调用的完整过程
java·开发语言·c++
Scott9999HH1 小时前
2026 避坑实录:国产品牌压力变送器什么牌子好?从硬件抗扰到 C++ Qt 实时曲线绘制源码剖析
开发语言·c++·qt
nicepainkiller1 小时前
codex 逆向 flutter
flutter·flutter 逆向
ch0sen1pm1 小时前
800 行代码手写 Reactor 网络库:从 epoll 到聊天室
c++
️学习的小王2 小时前
Python + MySQL 学生信息管理系统实战教程
android·python·mysql