进程标识
深入理解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
内核做了以下两件事:
- 复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。
- 为这四个部分分配物理块,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()函数实质上是按着写时拷贝的方式实现文件的映射,并不是共享,写时拷贝操作使得内存的需求量大大的减少了,具体的写时拷贝实现