`

Linux进程管理(二)——fork()和写时复制【转】

 
阅读更多

1. Linux的fork()使用写时复制(略)

 

      传统的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强调进程快速执行的能力,所以这个优化是很重要的。

 

参见:http://hi.baidu.com/zengzhaonong/blog/item/90ce8d5802d044de9d82043f.html

 

COW技术初窥:

      在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程

      那么子进程的物理空间没有代码,怎么去取指令执行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';

在开始的两个语句后,str1str2存放数据的地址是一样的,而在修改内容后,str1的地址发生了变化,而str2的地址还是原来的,这就是C++中的COW技术的应用,不过VS2005似乎已经不支持COW。

 

参见:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html

 

2. fork()函数

头文件

#include<unistd.h>
#include<sys/types.h>

函数原型

pid_t fork( void);

 (pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中)
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1

口诀: 父返子,子返0,fork出错返-1
示例代码

#include<sys/types.h> //对于此程序而言此头文件用不到
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(int argc, char ** argv ){

      //由于会返回两次,下面的代码会被执行两遍
      //如果成功创建子进程:
      //1. 父进程返回子进程ID,因此(父进程)会走一遍“分支3”
      //2. 子进程返回0,因此(子进程)会走一遍“分支2”
      pid_t pid = fork();

      if (pid < 0){ //分支1
            fprintf(stderr, "error!");
      }else if( 0 == pid ){//分支2
            printf("This is the child process!");
            _exit(0);
      }else{//分支3
            printf("This is the parent process! child process id = %d", pid);
      }
      //可能需要时候wait或waitpid函数等待子进程的结束并获取结束状态
      exit(0);
}

      注意!样例代码仅供参考,样例代码存在着父进程在子进程结束前结束的可能性。必要的时候可以使用wait或 waitpid函数让父进程等待子进程的结束并获取子进程的返回状态。
      fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。

 

3. Linux的fork()使用写时复制(详)

      fork函数用于创建子进程,典型的调用一次,返回两次的函数,其中返回子进程的PID和0,其中调用进程返回了子进程的PID,而子进程则返回了0,这是一个比较有意思的函数,但是两个进程的执行顺序是不定的。fork()函数调用完成以后父进程的虚拟存储空间被拷贝给了子进程的虚拟存储空间,因此也就实现了共享文件等操作。但是虚拟的存储空间映射到物理存储空间的过程中采用了写时拷贝技术(具体的操作大小是按着页控制的),该技术主要是将多进程中同样的对象(数据)在物理存储其中只有一个物理存储空间,而当其中的某一个进程试图对该区域进行写操作时,内核就会在物理存储器中开辟一个新的物理页面,将需要写的区域内容复制到新的物理页面中,然后对新的物理页面进行写操作。这时就是实现了对不同进程的操作而不会产生影响其他的进程,同时也节省了很多的物理存储器。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>

int main(){
        char p = 'p';
        int number = 11;

        if(fork()==0)      /*子进程*/
        {
                p = 'c';      /*子进程对数据的修改*/
                printf("p = %c , number = %d \n ",p,number);
                exit(0);
        }
       /*父进程*/
        number = 14;  /*父进程对数据修改*/
        printf("p = %c , number = %d \n ",p,number);
        exit(0);
}

编译运行:

$ gcc -g TestWriteCopyTech.c -o TestWriteCopyTech
$ ./TestWriteCopyTech
p = p , number = 14    -----父进程打印内容
$ p = c , number = 11    -----子进程打印内容 

原因分析:
       由于存在企图进行写操作的部分,因此会发生写时拷贝过程,子进程中对数据的修改,内核就会创建一个新的物理内存空间。然后再次将数据写入到新的物理内存空间中。可知,对新的区域的修改不会改变原有的区域,这样不同的空间就区分开来。但是没有修改的区域仍然是多个进程之间共享。
       fork()函数的代码段基本是只读类型的,而且在运行阶段也只是复制,并不会对内容进行修改,因此父子进程是共享代码段,而数据段、Bss段、堆栈段等会在运行的过程中发生写过程,这样就导致了不同的段发生相应的写时拷贝过程,实现了不同进程的独立空间。
       但是需要注意的是文件操作,由于文件的操作是通过文件描述符表、文件表、v-node表三个联系起来控制的,其中文件表、v-node表是所有的进程共享,而每个进程都存在一个独立的文件描述符表。父子进程虚拟存储空间的内容是大致相同的,父子进程是通过同一个物理区域存储文件描述符表,但如果修改文件描述符表,也会发生写时拷贝操作,只有这样才能保证子进程中对文件描述符的修改,不会影响到父进程的文件描述符表。例如close操作,因为close会导致文件的描述符的值发生变化,相当于发生了写操作,这是产生了写时拷贝过程,实现新的物理空间,然后再次发生close操作,这样就不会产生子进程中文件描述符的关闭而导致父进程不能访问文件。

测试函数:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>

int main(){
        int fd;
        char c[3];
        char *s = "TestFs";

        fd = open("foobar.txt",O_RDWR,0);

        if(fork()==0)   //子进程
        {
                fd = 1;//stdout
                write(fd,s,7);
                exit(0);
        }
       //父进程
        read(fd,c,2);
        c[2]='\0';
        printf("c = %s\n",c);
        exit(0);
}

编译运行:

$ gcc -g fileshare2.c -o fileshare2
$ ./fileshare2
c = fo    ----foobar.txt中的内容
$ TestFs   ---标准输出 


       原因分析:由于父子进程的文件描述符表是相同的,但是在子进程中对fd(文件描述符表中的项)进行了修改,这时会发生写时拷贝过程,内核在物理内存中分配一个新的页面存储子进程原文件描述符fd存在页面的内容,然后再进修写操作,实现将fd修改为1,也就是标准输出。但是父进程的fd并没有发生改变,还是与其他的子进程共享文件描述符表,因此仍然是对文件foobar.txt进行操作。
       因此需要注意fork()函数实质上是按着写时拷贝的方式实现文件的映射,并不是共享,写时拷贝操作使得内存的需求量大大的减少了,具体的写时拷贝实现,请参看非常经典的“深入理解计算机系统”的第622页。

分享到:
评论

相关推荐

    高效与精细的结合——Linux的进程管理.pdf

    本文将深入探讨Linux进程的创建、切换和终止,揭示其中的高效性和精细性。 首先,进程的创建是Linux系统中一个基础且关键的过程。在Linux中,进程的创建通常通过fork()系统调用来实现。这个调用会复制当前进程的...

    LINUX实验程序——————初学者

    4. **系统调用**:描述中提到的“相关的系统编程的函数理解”可能是指Linux中的系统调用,如open()用于打开文件,read()和write()用于文件读写,close()用于关闭文件,还有fork()和exec()用于进程管理。 5. **网络...

    Linux安全攻略——僵尸进程.pdf

    Linux安全攻略——僵尸进程.pdf中讨论了Linux操作系统中的进程管理机制,包括进程的概念、进程调度、进程树、进程标识符、进程生命周期等方面的知识点。 进程概念 在Linux操作系统中,进程是动态的,程序是静态的...

    linux高级编程知识点总结——进程线程

    ### Linux高级编程知识点总结——进程线程 #### 进程与程序的区别及PCB - **进程与程序的区别**...以上就是关于Linux进程和线程的高级编程知识点总结,涉及到了进程的基础概念、创建、管理以及特殊状态等方面的内容。

    疯狂内核之——进程管理子系统

    - **资源复制时间**:`fork`在创建新进程时立即复制所有资源;而`vfork`则延迟复制,直到新进程执行`exec`或返回时才真正复制资源。 - **资源共享**:由于`vfork`不立即复制资源,因此在新进程执行`exec`或返回前,...

    实验一 进程通信——管道和信号实验报告.doc

    【实验报告概述】 本次实验是关于操作系统中的进程通信,主要涉及了进程的创建、控制、以及多种通信方式,包括信号通信和管道通信。...通过这些练习,学生能够更好地掌握实际操作系统中进程管理的原理和应用。

    linux进程之fork创建新进程.zip

    `fork()`函数的调用会产生一个与当前进程(父进程)几乎完全一样的新进程——子进程。这两个进程拥有相同的代码段、数据段、堆和栈,但它们各自拥有独立的进程ID(PID)和资源分配。`fork()`调用成功后,会在父进程...

    实验一——Linux环境下的进程管理.pdf

    "实验一——Linux环境下的进程管理" 该实验报告主要涉及到 Linux 环境下的进程管理,旨在深入理解进程概念、进程与程序的区别、并发执行的实质、进程争用资源的现象、进程互斥的方法、Linux 系统中进程通信的基本...

    Linux进程-zhangwl.rar_linux_linux 进程_linux进程_源码分析_进程 linux

    总的来说,Linux进程是系统运行的基础,涉及的知识点广泛且深入,包括进程的生命周期管理、内存管理和资源分配、进程间通信、调度策略等。通过阅读提供的PDF文档和相关文本资料,开发者可以进一步深化对Linux内核的...

    linux面试题看FORK()

    在深入了解本面试题之前,我们需要掌握一些关于Linux进程管理的基础概念: 1. **进程的概念**:在Linux环境下,进程可以被视为一个正在执行的程序实例。每一个进程都有一个唯一的标识符——进程ID(PID),它是一个...

    Linux中进程控制与管理

    一个父进程可以通过**Fork**机制复制出一个或多个子进程,这一特性在编写网络程序时尤其重要,如Web服务器通过父进程接收客户端请求,并Fork子进程来处理请求,使父进程能继续监听新请求,实现高效并发处理。...

    Linux 多进程及其通信

    文档"Linux进程间通信——使用信号.docx"可能会详细解释如何发送和处理不同类型的信号,以及如何自定义信号处理函数。 2. **信号量(Semaphores)**:信号量是同步原语,用于控制多个进程对共享资源的访问。它们...

    进程管理实验报告

    5. **Linux进程通信**:Linux系统提供了多种进程间通信(IPC)机制,包括管道、信号量、消息队列、共享内存等。实验中涉及的是管道通信,通过管道,进程之间可以实现单向的数据传输。 实验内容包括: 1. **进程的...

    linux进程编程介绍

    当我们谈论Linux进程编程时,`fork()`是最基础的操作。这个系统调用会创建一个与父进程几乎完全一样的副本——子进程。两者共享父进程的代码段,但拥有各自的栈和数据段。然而,由于复制开销,`fork()`并不适合频繁...

    实验一——Linux环境下的进程管理.doc

    "实验一——Linux环境下的进程管理" 本实验的目的是为了加深对进程概念的理解,明确进程和程序的区别,进一步认识并发执行的实质,分析进程争用资源的现象,学习解决进程互斥的方法,了解 Linux 系统中进程通信的...

    fork 两个子进程 及其进程控制

    `fork()`是Unix/Linux系统中的一个系统调用,用于创建一个新的进程——子进程。当父进程调用`fork()`时,操作系统会复制父进程的所有资源(如内存空间、文件描述符等)给新创建的子进程,形成一份完全独立的副本。...

    实验四:Linux下进程管道通信.docx

    通过实验,我们了解到管道通信的核心在于管道文件描述符的管理和进程间的同步。父进程创建管道后,子进程会继承这些描述符,从而可以进行通信。管道提供了一种简单且有效的方法来传递数据,特别是在父子进程或兄弟...

    LINUX教程——进程PPT

    Linux教程中的进程概念是操作系统核心的重要组成部分,它涵盖了操作系统如何管理和调度执行中的程序。进程是正在执行的程序的一个实例,包含了程序的执行上下文、相关数据以及程序本身。与静态的程序不同,进程具有...

Global site tag (gtag.js) - Google Analytics