在多进程编程中,我们都知道fork()会产生一个子进程,而且子进程就是父进程的一个副本。按照传统fork的方式,子进程获得父进程数据空间、堆和栈的副本。这种实现方式实在过于简单,粗暴,效率低下。为什么这么说呢?因为在fork之后,往往紧接着就会跟随exec。exec之前拷贝完全是无意义的,而且会极大的限制创建进程的速度。
所以linux引入了写时拷贝技术(copy-on-write),简称COW。它是一种可以推迟甚至可以免除拷贝数据的技术。fork时,内核此时并不复制整个进程的地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说在此之前都是以只读的方式访问。这种技术使地址空间上的页的拷贝被推迟到真正需要写入的时候。例如我之前说的情况,fork之后立即调用exec,他们就不要拷贝。
现在fork之后的实际开销就是复制进程的页表和子进程创建唯一的进程描述符。这是一种极大的优化,避免了大量的无意义的拷贝。对于Linux这种强调快速切换的操作系统来说,这个优化有着重大的意义。
注意:fork之后内核会通过将子进程放入到运行队列前面,以让子进程先运行。以避免父进程先写入,产生拷贝,而后子进程执行exec,导致因而无意义的拷贝。
在我之前的【Linux内存管理】中我已经讲到了,每个进程都有独立的进程地址空间。我们现在考虑一种实际情况,有一个父进程PID1。它的虚拟地址空间大致有:代码段,数据段,堆,栈。内核通过页表为他们映射了虚拟地址到物理地址,为了方便表示,就用物理地址块表示吧。(4G地址空间)。
原始fork()
PID1 通过fork()系统调用创建了一个子进程PID2。下图简单的直接的表示,可以看出直接为PID2复制了PID1的地址空间数据。
COW技术
内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
通过上面的分析,相信大家对写时拷贝技术有了一个比较深入的认识了。如果在本文中对这些专业术语不是很清楚,建议去看我之前的文章【Linux内存管理】。