【操作系统】写时复制 Copy-on-write
更多面试题总结请看:【面试题】技术面试题汇总
Copy-on-write 简介
写时复制(Copy-on-write,COW),有时也称为隐式共享(implicit sharing)。COW 将复制操作推迟到第一次写入时进行:在创建一个新副本时,不会立即复制资源,而是共享原始副本的资源;当修改时再执行复制操作。通过这种方式共享资源,可以显著减少创建副本时的开销,以及节省资源;同时,资源修改操作会增加少量开销。
为什么需要 Copy-on-write?
当通过 fork()
来创建一个子进程时,操作系统需要将父进程虚拟内存空间中的大部分内容全部复制到子进程中(主要是数据段、堆、栈;代码段共享)。这个操作不仅非常耗时,而且会浪费大量物理内存。特别是如果程序在进程复制后立刻使用 exec
加载新程序,那么负面效应会更严重,相当于之前进行的复制操作是完全多余的。
因此引入了写时复制技术。内核不会复制进程的整个地址空间,而是只复制其页表,fork
之后的父子进程的地址空间指向同样的物理内存页。
但是不同进程的内存空间应当是私有的。假如所有进程都只读取其内存页,那么就可以继续共享物理内存中的同一个副本;然而只要有一个进程试图写入共享区域的某个页面,那么就会为这个进程创建该页面的一个新副本。
写时复制技术将内存页的复制延迟到第一次写入时,更重要的是,在很多情况下不需要复制。这节省了大量时间,充分使用了稀有的物理内存。
Copy-on-write 实现原理
fork()
之后,内核会把父进程的所有内存页都标记为只读。一旦其中一个进程尝试写入某个内存页,就会触发一个保护故障(缺页异常),此时会陷入内核。
内核将拦截写入,并为尝试写入的进程创建这个页面的一个新副本,恢复这个页面的可写权限,然后重新执行这个写操作,这时就可以正常执行了。
内核会保留每个内存页面的引用数。每次复制某个页面后,该页面的引用数减少一;如果该页面只有一个引用,就可以跳过分配,直接修改。
这种分配过程对于进程来说是透明的,能够确保一个进程的内存更改在另一进程中不可见。
优缺点
优点:减少不必要的资源分配,节省宝贵的物理内存。
缺点:如果在子进程存在期间发生了大量写操作,那么会频繁地产生页面错误,不断陷入内核,复制页面。这反而会降低效率。
实际应用
Redis 的持久化机制中,如果采用 bgsave
或者 bgrewriteaof
命令,那么会 fork 一个子进程来将数据存到磁盘中。Redis 的读取操作多,因此这种情况下使用 COW 可以减少 fork()
操作的阻塞时间。
写时复制的思想在很多语言中也有应用,相比于传统的深层复制,能带来很大性能提升。比如 C++ 98 标准下的 std::string
就采用了写时复制的实现:
std::string x("Hello");
std::string y = x; // x、y 共享相同的 buffer
y += ", World!"; // 写时复制,此时 y 使用一个新的 buffer
// x 依然使用旧的 buffer
Golang、PHP 中的 string、array 也是写时复制。在修改这些类型时,如果其引用计数非零,则会复制一个副本。因此我们在 golang、php 中可以将字符串、数组当作值类型(values type)进行传递,即不会有传值复制的开销,也能保证其 immutable 的特性。
技术面试题汇总
参考资料:
- Copy-on-write - Wikipedia
- Copy On Write 机制 - Java3y
- 《深入 Linux 内核架构》2.4.1