Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

写时复制

本章为进阶内容,零基础读者可以跳过,不影响后续阅读。

复习

  • 按需加载与交换空间:只把眼下需要的页面放进内存
  • 页面置换:内存装满后应该换出哪一页
  • 工作集与内存抖动:程序频繁缺页时为什么会忙着换页却不再做事

TL;DR

  • 创建进程时常要“复制一份”,可完整复制内存既慢又费内存
  • 写时复制(COW):先让父子进程共享同一批页,都设为只读
  • 谁真正要写,才给谁复制那一页
  • 省内存、加快创建,是 fork 能又快又省的秘诀

正文

  回忆一下创建进程:fork 会“复制”出一个和父进程几乎一样的子进程。如果老实地把父进程的内存一字不差地抄一份,代价相当可观——尤其是父进程本身很大时。

  可仔细想想:子进程复制完之后,往往紧接着就 exec 换成别的程序,那些抄过来的数据它根本不用。辛辛苦苦抄一遍,然后立刻扔掉,这买卖太亏。

先共享,再按需复制

  写时复制(COW,Copy-On-Write)给出了一个聪明的解法:

  • fork 时,不复制内存,而是让父子进程共享同一批物理页
  • 把这些共享页统统标记为只读
  • 之后,谁想某一页,硬件就会发现“这是只读的,不能写”,触发一个异常
  • 内核介入:把这一页真正复制一份给要写的那方,改成可写,再让它继续写
fork 后:父 ─┐
             ├── 共享同一批页(只读)
        子 ─┘

子进程想写某页: → 异常 → 内核复制该页给子进程 → 子进程继续写

  注意关键:只有被写的那一页才复制。没被写的页,父子一直共享,一份内存当两份用。

好处

  写时复制带来的好处很实在:

  • fork 几乎不用搬数据,瞬间完成
  • :没被改动的页大家共用,内存占用大幅下降
  • 贴切:非常契合“复制后马上 exec”这种常见用法——子进程几乎不写,于是几乎不复制

  “读的时候共享,写的时候才分开”,这个思路不只用在进程创建上,后面讲文件映射、内存共享时还会再见到它。

思考题

  如果子进程 fork 之后立刻把整片内存都改一遍,写时复制还能省内存吗?它能省下的是“创建的开销”,还是“内存的占用”,还是两者都省?

小结

知识点

  • 完整复制进程内存既慢又浪费
  • 写时复制:先共享只读页,写入时才复制
  • 只有被写的页才复制
  • 加快创建、节省内存

参考资料

  1. Wikipedia(zh):写时复制:copy-on-write
  2. Wikipedia(zh):fork:进程创建

思考题答案(仅供参考)

  如果子进程立刻把整片内存都改一遍,那么每一页都会被写、都被复制,最终等同于完整复制了一份——内存上并不省。写时复制真正稳定省下的,是“创建时的开销”:不用一上来就抄一遍。所以在“改遍全部”的极端情况下,它主要省的是启动时间,而不是内存;而在“改了很少”的常见情况下,时间和内存都省。可见同一项技术,收益大小取决于程序怎么用。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪