写时复制
本章为进阶内容,零基础读者可以跳过,不影响后续阅读。
复习
- 按需加载与交换空间:只把眼下需要的页面放进内存
- 页面置换:内存装满后应该换出哪一页
- 工作集与内存抖动:程序频繁缺页时为什么会忙着换页却不再做事
TL;DR
- 创建进程时常要“复制一份”,可完整复制内存既慢又费内存
- 写时复制(COW):先让父子进程共享同一批页,都设为只读
- 谁真正要写,才给谁复制那一页
- 省内存、加快创建,是 fork 能又快又省的秘诀
正文
回忆一下创建进程:fork 会“复制”出一个和父进程几乎一样的子进程。如果老实地把父进程的内存一字不差地抄一份,代价相当可观——尤其是父进程本身很大时。
可仔细想想:子进程复制完之后,往往紧接着就 exec 换成别的程序,那些抄过来的数据它根本不用。辛辛苦苦抄一遍,然后立刻扔掉,这买卖太亏。
先共享,再按需复制
写时复制(COW,Copy-On-Write)给出了一个聪明的解法:
fork时,不复制内存,而是让父子进程共享同一批物理页- 把这些共享页统统标记为只读
- 之后,谁想写某一页,硬件就会发现“这是只读的,不能写”,触发一个异常
- 内核介入:把这一页真正复制一份给要写的那方,改成可写,再让它继续写
fork 后:父 ─┐
├── 共享同一批页(只读)
子 ─┘
子进程想写某页: → 异常 → 内核复制该页给子进程 → 子进程继续写
注意关键:只有被写的那一页才复制。没被写的页,父子一直共享,一份内存当两份用。
好处
写时复制带来的好处很实在:
- 快:
fork几乎不用搬数据,瞬间完成 - 省:没被改动的页大家共用,内存占用大幅下降
- 贴切:非常契合“复制后马上 exec”这种常见用法——子进程几乎不写,于是几乎不复制
“读的时候共享,写的时候才分开”,这个思路不只用在进程创建上,后面讲文件映射、内存共享时还会再见到它。
思考题
如果子进程 fork 之后立刻把整片内存都改一遍,写时复制还能省内存吗?它能省下的是“创建的开销”,还是“内存的占用”,还是两者都省?
小结
知识点
- 完整复制进程内存既慢又浪费
- 写时复制:先共享只读页,写入时才复制
- 只有被写的页才复制
- 加快创建、节省内存
参考资料
- Wikipedia(zh):写时复制:copy-on-write
- Wikipedia(zh):fork:进程创建
思考题答案(仅供参考)
如果子进程立刻把整片内存都改一遍,那么每一页都会被写、都被复制,最终等同于完整复制了一份——内存上并不省。写时复制真正稳定省下的,是“创建时的开销”:不用一上来就抄一遍。所以在“改遍全部”的极端情况下,它主要省的是启动时间,而不是内存;而在“改了很少”的常见情况下,时间和内存都省。可见同一项技术,收益大小取决于程序怎么用。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪