http://www.boluor.com/summary-of-fork-in-linux.html
fork函数在linux中非常重要,因为进程大多是通过它来创建的,比如linux系统在启动时首先创建了进程0,之后的很多进程借助do_fork得到创建.这两天在看匿名管道时了解了下fork,其应用毕竟广,这里只说些我才学到的吧.
首先来看例1.
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
int main(){
int i;
printf("hello world %d\n",getpid());
i=3;
fork();
printf("var %d in %d\n",i,getpid());
return 0;
}
输出是什么呢?
这是在我的机器上一次执行的结果:
hello world 8168
var 3 in 8169
var 3 in 8168
为什么会有两次输出var 3 一行呢?看似不可思议吧…要解释原因,就牵涉到了我们要讨论的fork,它到底做了什么?
fork英文是叉的意思.在这里的意思是进程从这里开始分叉,分成了两个进程,一个是父进程,一个子进程.子进程拷贝了父进程的绝大部分.栈阿,缓冲区阿等等.系统为子进程创建一个新的进程表项,其中进程id与父进程是不相同的,这也就是说父子进程是两个独立的进程,虽然父子进程共享代码空间.但是在牵涉到写数据时子进程有自己的数据空间,这是因为copy on write机制,在有数据修改时,系统会为子进程申请新的页面.
再来复习下进程的有关知识.系统通过进程控制块PCB来管理进程.进程的执行,可以看作是在它的上下文中执行.一个进程的上下文(context)由三部分组成:用户级上下文,寄存器上下文和系统级上下文.用户级上下文中有正文,数据,用户栈和共享存储区;寄存器上下文中有个非常重要的程序计数器(传说中的)PC,还有栈指针和通用寄存器等;系统级上下文分静态和动态,PCB中进程表项,U区,还有本进程的表项,页表,系统区表项等都属于静态部分,而核心栈等则属于动态部分.
回到fork上来.fork在内核中对应的是do_fork函数,本来想自己写下函数说明的,发现已经有了.详见:内核 do_fork 函数源代码浅析 . 上面已经提到,fork后,子进程拷贝了父进程的进程表项,还有栈阿,缓冲区,U区等等.当然在这之前会去检查系统有没有可用的资源,取一个空闲的进程表项和唯一的PID号等工作.(后面的例子会体现子进程到底拷贝了父进程的哪些东西.)需要指出的是,这里所说的拷贝,并不是说子进程再申请页面,将父进程中的全部拷贝过来.而是,他们共享一个空间,子进程只是作一层映射而已,这个时候进程页面标记为只读.在有数据修改时,才会申请新的页面,拷贝过来,并标记为可写.
fork执行后,对父进程和子进程不同的地方还有,对父进程返回子进程的pid号,对子进程返回的是0.大致的算法描述为:
if (当前正在执行的是父进程){
将子进程的状态设置为”就绪状态”;
return (子进程的pid号);
}else{ /*正在执行的是子进程*/
初始化U区等工作;
return 0;
}
现在来看例1,是不是已经清晰了很多? 在执行了fork之后,父子进程分别都执行了下一步printf语句.由于fork拷贝走了pc,所以在子进程中不会再从main入口重新执行,而是执行fork后的下一条指令.而i是保存在进程栈空间中的,所以子进程中也存在.
有了前面的基础,再看下面一个例2:
#include <stdio.h>
#include <unistd.h>
int main()
{
int i=0;
pid_t fork_result;
printf("pid : %d --> main begin()\n",getpid());
fork_result = fork();
if(fork_result < 0) {
printf("Fork Failure\n");
return 0;
}
for(i=0;i<3;i++){
if(fork_result == 0){ //在子进程中.
printf("child process : %d\n",i);
}else{
printf("Father process : %d\n",i);
}
}
return 0;
}
这次输出可以更明确的显示出子进程到底拷贝了些什么.我机器上的两次执行结果:
boluor@boluor-laptop:~/programs/pipe/fork$./a.out
pid : 16567 –> mainbegin()
child process : 0
child process : 1
child process : 2
Father process : 0
Father process : 1
Father process : 2
boluor@boluor-laptop:~/programs/pipe/fork$ ./a.out
pid : 16569 –> main begin()
Father process : 0
Father process : 1
Father process : 2
child process : 0
child process : 1
child process : 2
同时也可以说明,父子进程到底哪个先执行,是跟cpu调度有关系的.如果想固定顺序,那么就要用wait或vfork函数.
继续看例3:
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
int main()
{
printf("hello world %d",getpid());
//fflush(0);
fork();
return 0;
}
执行上面的程序,可以发现输出了两遍hello world.而且两次的pid号都是一样的.这是为什么呢? 这其实是因为printf的行缓冲的问题,printf语句执行后,系统将字符串放在了缓冲区内,并没有输出到stdout.不明白的话看下面的例子:
#include "stdio.h"
int main(){
printf("hello world");
while(1);
return 0;
}
执行上面的程序你会发现,程序陷入死循环,并没有输出”hello world”.这就是因为把”hello world”放入了缓冲区.我们平常加’\n’的话,就会刷新缓冲区,那样就会直接输出到stdout了.
因为子进程将这些缓冲也拷贝走了,所以子进程也打印了一遍.父进程直到最后才输出.他们的输出是一样的,输出的pid是一致的,因为子进程拷贝走的是printf语句执行后的结果.如果利用setbuf设置下,或者在printf语句后调用fflush(0);强制刷新缓冲区,就不会有这个问题了.这个例子从侧面显示出子进程也拷贝了父进程的缓冲区.
关于fork的应用还很多很多,在实际项目中需要了再去深入研究.关于fork和exec的区别,exec是将本进程的映像给替换掉了,跟fork差别还是很大的,其实fork创建子进程后,大部分情况下,子进程会调用exec去执行不同的程序的.
先说到这里了.如果需要更多fork的知识就google一下^.^.