COW
Copy on Write | 写时复制
在 Linux 程序中,fork()
会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了写时复制技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。写时复制(Copy-on-write)是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。
也就是说,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享。这种技术使地址空间上页的拷贝被推迟到实际发生写入的时候。有时共享页根本不会被写入,例如,fork()
后立即调用 exec()
,就无须复制父进程的页了。fork()
的实际开销就是复制父进程的页表以及给子进程创建唯一的 PCB。这种优化可以避免拷贝大量根本就不会使用的数据(地址空间里常常包含数十兆的数据)。
fork 实现
传统的 fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux 的 fork() 使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—举例来说,fork()后立即调用 exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于 Unix 强调进程快速执行的能力,所以这个优化是很重要的。这里补充一点:Linux COW 与 exec 没有必然联系。
在 fork 之后 exec 之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为 exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为 exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。不过,fork 之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行 exec 系统调用,因无意义的复制而造成效率的下降。
实际上 COW 技术不仅仅在 Linux 进程上有应用,其他例如 C++的 String 在有的 IDE 环境下也支持 COW 技术,即例如:
string str1 = "hello world";
string str2 = str1;
str1[1]='q';
str2[1]='w';
在开始的两个语句后,str1 和 str2 存放数据的地址是一样的,而在修改内容后,str1 的地址发生了变化,而 str2 的地址还是原来的,这就是 C++中的 COW 技术的应用,不过 VS2005 似乎已经不支持 COW。