如果对中间的零碎的讲解没有兴趣,可以直接跳到文章末尾,看总结。
一、fork基础知识
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
fork()函数得到的子进程,继承父进程的所有系统资源,包括,代码段、数据区、常量区等等。父子进程的地址空间开始是共享的,只有当父子进程中的任何一个企图修改其中的内容时才进行复制,即copy on write。(写时复制技术我会在另外一篇博客中进行详细讲解)。而从表明上看二者是互不相干的。
看一个简单例子:
#include <unistd.h> #include <stdio.h> int main () { pid_t fpid; //fpid表示fork函数返回的值 int count=0; fpid=fork(); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) { printf("i am the child process, my process id is %d\n",getpid()); count++; } else { printf("i am the parent process, my process id is %d\n",getpid()); count++; } printf("count is: %d\n",count); return 0; }
结果:
i am the parent process, my process id is 5573
count: 1 i am the child process, my process id is 5574count: 1
为什么两个进程的fpid不同呢,这与fork函数的特性有关。
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1.在父进程中,fork返回新创建子进程的进程ID;2.在子进程中,fork返回0;3.如果出现错误,fork返回一个负值;
fork出错可能有两种原因:1.当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。2.系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。
二、fork进阶
先看代码:
#include <unistd.h> #include <stdio.h> int main () { int i=0; printf("i son/pa ppid pid fpid\n"); //ppid指当前进程的父进程pid //pid指当前进程的pid, //fpid指fork返回给当前进程的值 for(i=0;i<2;i++){ pid_t fpid=fork(); if(fpid==0) printf("%d child %4d %4d %4d\n",i,getppid(),getpid(),fpid); else printf("%d parent %4d %4d %4d\n",i,getppid(),getpid(),fpid); } return 0; }
结果:
i son/pa ppid pid fpid 0 parent 2043 3224 3225 0 child 3224 3225 0 1 parent 2043 3224 3226 1 parent 3224 3225 3227 1 child 1 3227 0 1 child 1 3226 0
第一步:在父进程中,指令执行到for循环中,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是p3224和p3225(后面我都用pxxxx表示进程id为xxxx的进程)。可以看到父进程p3224的父进程是p2043,子进程p3225的父进程正好是p3224。我们用一个链表来表示这个关系:
p2043->p3224->p3225
读者可能注意到p3226,p3227的父进程难道不该是p3224和p3225吗,怎么会是1呢?这里得讲到进程的创建和死亡的过程,在p3224和p3225执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。p3224和p3225死亡后,p3226,p3227就没有父进程了,这在操作系统是不被允许的,所以p3226,p3227的父进程就被置为p1了,p1是永远不会死亡的。
总结一下,这个程序执行的流程如下:
还有,如果去掉”\n“后每次程序运行的结果都不一样了,这是跟printf的缓冲机制有关,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上。但是,只要看到有\n 则会立即刷新stdout,因此就马上能够打印了。
运行了printf("fork!")后,“fork!”仅仅被放到了缓冲里,程序运行到fork时缓冲里面的“fork!” 被子进程复制过去了。因此在子进程的stdout缓冲里面就也有了fork! 。所以,你最终看到的会是fork! 被printf了2次!!!!
而运行printf("fork!\n")后,“fork!”被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有fork! 内容。因此你看到的结果会是fork! 被printf了1次!!!!
最后,看看下面程序一共创建了多少个进程:
#include <stdio.h> #include <unistd.h> int main(int argc, char* argv[]) { fork(); fork() && fork() || fork(); fork(); return 0; }
答案是总共20个进程。fork() && fork() || fork();的解释如下图:
fork() && fork() || fork();额外fork出了4个进程。子进程的返回值是0。
三、总结
- fork函数通过系统调用创建一个与原来进程几乎完全相同的进程,从此可以将这两个父子进程看做完全不相干的两个进程。
- fork函数返回值,父进程返回子进程的ID,子进程返回0,错误返回负值。
- 现在Linux系统中fork子进程是利用写时复制技术实现的,也就说内核只为子进程创建虚拟空间,不为其分配实际内存,共享父进程的物理空间。且内核通常将子进程放置于父进程前面执行。
- 当父子进程中任何一个发生更改相应段的数据时,系统才会为子进程分配相应段的物理空间。
- 通常fork的子进程都会立即调用exec(),此时fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。
- 若fork后并不是立即执行exec(),而是其他指令,那么此时子进程首先会创建一个栈。
- 为子进程的页表分配页帧
- 为子进程的页分配页帧
- 初始化子进程的页表
- 把父进程的页复制到子进程相应的页中
- fork实现写时复制,图形描述,如下图
- vfork实现更加暴力,连虚拟空间都共享父进程的。