写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程建立:当发出fork( )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为是很是耗时的,由于它须要:node
· 为子进程的页表分配页面linux
· 为子进程的页分配页面xcode
· 初始化子进程的页表缓存
· 把父进程的页复制到子进程相应的页中数据结构
建立一个地址空间的这种方法涉及许多内存访问,消耗许多CPU周期,而且彻底破坏了高速缓存中的内容。在大多数状况下,这样作经常是毫无心义的,由于许多子进程经过装入一个新的程序开始它们的执行,这样就彻底丢弃了所继承的地址空间。app
如今的Unix内核(包括Linux),采用一种更为有效的方法称之为写时复制(或COW)。这种思想至关简单:父进程和子进程共享页面而不是复制页面。然而,只要页面被共享,它们就不能被修改。不管父进程和子进程什么时候试图写一个共享的页面,就产生一个错误,这时内核就把这个页复制到一个新的页面中并标记为可写。原来的页面仍然是写保护的:当其它进程试图写入时,内核检查写进程是不是这个页面的惟一属主;若是是,它把这个页面标记为对这个进程是可写的。函数
传统的fork()系统调用直接把全部的资源复制给新建立的进程。这种实现过于简单而且效率低下,由于它拷贝的数据或许能够共享(This approach is significantly naïve and inefficient in that it copies much data that might otherwise be shared.)。更糟糕的是,若是新进程打算当即执行一个新的映像,那么全部的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种能够推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在须要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在须要写入的时候才会进行,在此以前,只有以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的状况下---例如,fork()后当即执行exec(),地址空间就无需被复制了。fork()的实际开销就是复制父进程的页表以及给子进程建立一个进程描述符。在通常状况下,进程建立后都为立刻运行一个可执行的文件,这种优化,能够避免拷贝大量根本就不会被使用的数据(地址空间里经常包含数十兆的数据)。因为Unix强调进程快速执行的能力,因此这个优化是很重要的。测试
COW技术初窥:优化
在Linux程序中,fork()会产生一个和父进程彻底相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。spa
那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?
在fork以后exec以前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,二者的虚拟空间不一样,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,若是不是由于exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此二者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(二者的代码彻底相同)。而若是是由于exec,因为二者执行的代码不一样,子进程的代码段也会分配单独的物理空间。
在网上看到还有个细节问题就是,fork以后内核会经过将子进程放在队列的前面,以让子进程先执行,以避免父进程执行致使写时复制,然后子进程执行exec系统调用,因无心义的复制而形成效率的降低。
COW详述:
如今有一个父进程P1,这是一个主体,那么它是有灵魂也就身体的。如今在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部 分,相应的,内核要为这四个部分分配各自的物理块。即:正文段块,数据段块,堆块,栈块。至于如何分配,这是内核去作的事,在此不详述。
1. 如今P1用fork()函数为进程建立一个子进程P2,
内核:
(1)复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。
(2)为这四个部分分配物理块,P2的:正文段->PI的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段->P2本身的数据段块(为其分配对应的块),堆->P2本身的堆块,栈->P2本身的栈块。以下图所示:同左到右大的方向箭头表示复制内容。
2. 写时复制技术:内核只为新生成的子进程建立虚拟空间结构,它们来复制于父进程的虚拟究竟结构,可是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
3. vfork():这个作法更加火爆,内核连子进程的虚拟地址空间结构也不建立了,直接共享了父进程的虚拟空间,固然了,这种作法就顺水推舟的共享了父进程的物理空间
经过以上的分析,相信你们对进程有个深刻的认识,它是怎么一层层体现出本身来的,进程是一个主体,那么它就有灵魂与身体,系统必须为实现它建立相应的实体, 灵魂实体与物理实体。这二者在系统中都有相应的数据结构表示,物理实体更是体现了它的物理意义。
补充一点:Linux COW与exec没有必然联系
PS:实际上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。
2. fork()函数
头文件
函数原型
(pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中)
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;不然,出错返回-1
口诀: 父返子,子返0,fork出错返-1
示例代码
注意!样例代码仅供参考,样例代码存在着父进程在子进程结束前结束的可能性。必要的时候可使用wait或 waitpid函数让父进程等待子进程的结束并获取子进程的返回状态。
fork的另外一个特性是全部由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增长。
3. Linux的fork()使用写时复制(详)
fork函数用于建立子进程,典型的调用一次,返回两次的函数,其中返回子进程的PID和0,其中调用进程返回了子进程的PID,而子进程则返回了0,这是一个比较有意思的函数,可是两个进程的执行顺序是不定的。fork()函数调用完成之后父进程的虚拟存储空间被拷贝给了子进程的虚拟存储空间,所以也就实现了共享文件等操做。可是虚拟的存储空间映射到物理存储空间的过程当中采用了写时拷贝技术(具体的操做大小是按着页控制的),该技术主要是将多进程中一样的对象(数据)在物理存储其中只有一个物理存储空间,而当其中的某一个进程试图对该区域进行写操做时,内核就会在物理存储器中开辟一个新的物理页面,将须要写的区域内容复制到新的物理页面中,而后对新的物理页面进行写操做。这时就是实现了对不一样进程的操做而不会产生影响其余的进程,同时也节省了不少的物理存储器。
缘由分析:
因为存在企图进行写操做的部分,所以会发生写时拷贝过程,子进程中对数据的修改,内核就会建立一个新的物理内存空间。而后再次将数据写入到新的物理内存空间中。可知,对新的区域的修改不会改变原有的区域,这样不一样的空间就区分开来。可是没有修改的区域仍然是多个进程之间共享。
fork()函数的代码段基本是只读类型的,并且在运行阶段也只是复制,并不会对内容进行修改,所以父子进程是共享代码段,而数据段、Bss段、堆栈段等会在运行的过程当中发生写过程,这样就致使了不一样的段发生相应的写时拷贝过程,实现了不一样进程的独立空间。
可是须要注意的是文件操做,因为文件的操做是经过文件描述符表、文件表、v-node表三个联系起来控制的,其中文件表、v-node表是全部的进程共享,而每一个进程都存在一个独立的文件描述符表。父子进程虚拟存储空间的内容是大体相同的,父子进程是经过同一个物理区域存储文件描述符表,但若是修改文件描述符表,也会发生写时拷贝操做,只有这样才能保证子进程中对文件描述符的修改,不会影响到父进程的文件描述符表。例如close操做,由于close会致使文件的描述符的值发生变化,至关于发生了写操做,这是产生了写时拷贝过程,实现新的物理空间,而后再次发生close操做,这样就不会产生子进程中文件描述符的关闭而致使父进程不能访问文件。
测试函数:
编译运行:
缘由分析:因为父子进程的文件描述符表是相同的,可是在子进程中对fd(文件描述符表中的项)进行了修改,这时会发生写时拷贝过程,内核在物理内存中分配一个新的页面存储子进程原文件描述符fd存在页面的内容,而后再进修写操做,实现将fd修改成1,也就是标准输出。可是父进程的fd并无发生改变,仍是与其余的子进程共享文件描述符表,所以仍然是对文件foobar.txt进行操做。 所以须要注意fork()函数实质上是按着写时拷贝的方式实现文件的映射,并非共享,写时拷贝操做使得内存的需求量大大的减小了,具体的写时拷贝实现,请参看很是经典的“深刻理解计算机系统”的第622页。