Linux进程控制之深入理解fork

进程标识

深入理解fork

创建新的进程,调用一次返回两次。父进程返回子进程ID,子进程返回0,之后子进程和父进程继续执行fork之后的指令。子进程获取父进程的数据空间、堆、栈的副本。父子进程不共享这些存储空间,但是代码段(正文段,指令)是父子进程共享的,这点可以参考C程序内存空间布局,这个很重要哦。

传统fork vs 写时拷贝fork

首先每个进程都是由实体的,有实际的数据结构支撑,这个可以参考前面博客,一个C程序的内存空间布局。

  • 传统fork:直接把当前线程数据直接全部复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据或许可以共享。更糟糕的是,如果新进程打算立即执行一个新的映像(执行exec),那么所有的拷贝都将前功尽弃。
  • 如今fork的写时拷贝技术:所以Linux的fork()使用写时拷贝(copy-on-write COW)页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—例如,fork()后立即执行exec(),地址空间就无需被复制了。fork()的实际开销就是复制父进程的页表(使子进程虚拟空间和父进程一样,物理空间共用一个)以及给子进程创建一个进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化,可以避免拷贝大量根本就不会被使用的数据,导致写时复制技术很牛逼,这就回答了进程是如何被创建的。
    这里写图片描述

详解写时fork

现在有一个父进程P1,这是一个主体,那么它有自己虚拟内存空间。现在在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部分,相应的,内核要为这四个部分分配各自的物理块。即:正文段块,数据段块,堆块,栈块。至于如何分配,这是内核去做的事,在此不详述。

1、传统P1用fork()函数为进程创建一个子进程P2

内核做了以下两件事:

  1. 复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。
  2. 为这四个部分分配物理块,P2的:正文段->P1的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段->P2自己的数据段块(为其分配对应的块),堆->P2自己的堆块,栈->P2自己的栈块。如下图所示:同左到右大的方向箭头表示复制内容。

这里写图片描述
注意传统中代码段也是共享的,因为代码段是使用不变的内容不需要在复制一份。

2、写时复制技术P1用fork()函数为进程创建一个子进程P2
内核只为新生成的子进程创建虚拟空间结构也就是页表啥的,它们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
这里写图片描述

3、vfork函数调用序列和返回值与fork相同,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间。vfork创建的子进程就是为了exec一个新的程序,所以在子进程中修改变量,其实是修改了父进程中的变量,并且vfork保证子进程在调用exec之前,父进程处于休眠状态。
这里写图片描述

#include<sys/types.h> //对于此程序而言此头文件用不到  
#include<unistd.h>  
#include<stdio.h>  
#include<stdlib.h>  
int main(int argc, charchar ** argv ){  
      pid_t pid = vfork();  
      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);  
}  

测试例子

#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];  
        charchar *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);  
}  
//输出
c = fo    ----foobar.txt中的内容  
$ TestFs   ---标准输出   

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有时需要偏执狂

请我喝咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值