原创
fork函数
2011-6-8 19:04
2028
9
10
分类:
工程师职场
原文件地址: //http://www.boluor.com/summary-of-fork-in-linux.html //http://blog.sina.com.cn/s/blog_4b9216f50100cwwu.html### //http://blog.csdn.net/wei801004/archive/2009/06/12/4263571.aspx //http://blog.csdn.net/boy8239/archive/2007/10/29/1854471.aspx 信息摘录: 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 –> main begin() 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去执行不同的程序的. 前几天,论坛上有人问了这样一个问题: #include <sys/types.h> #include <unistd.h> int main() { for(int i = 0; i < 3; i ++) { int pid = fork(); if(pid == 0) { printf("child\n"); } else { printf("father\n"); } } return 0; } 请问输出结果是什么? 初看,想当然认为结果是3对child-father,只是顺序不确定,而且按照Unix环境高级编程中的说法,极端的情况下可能还会出现两个输出的内容相互夹杂的情况。 但是,在Unix测试了一下发现输出竟然有7对child-father。为什么会这样呢?看了半天程序终于明白了这个简单的问题。其实,这个问题在写/懂汇编的人看来是再清楚不过了,问题就出在这个for循环。 1.i=0时,父进程进入for循环,此时由于fork的作用,产生父子两个进程(分别记为F0/S0),分别输出father和child,然后,二者分别执行后续的代码,那后续的代码是什么呢?return 0?当然不是,由于for循环的存在,后续的代码是add指令和一条jump指令,因此,父子进程都将进入i=1的情况; 2.i=1时,父进程继续分成父子两个进程(分别记为F1/S1),而i=0时fork出的子进程也将分成两个进程(分别记为FS01/SS01),然后所有这些进程进入i=2; 3.....过程于上面类似,已经不用多说了,相信一切都已经明了了,依照上面的标记方法,i=2时将产生F2/S2,FS12/SS12,FFS012/SFS012,FSS012/SSS012. 因此,最终的结果是输出7对child/father。其对应的数学公式为: 1 + 2 + 4 + ... + 2^(n - 1) = 2^n - 1 不过话说回来,这种在for循环中使用fork的作法实在不值得推荐,研究研究尚可,实际应用恐怕会引来很多麻烦,需小心谨慎才是。 main() { pid_t pid; if(pid=fork()<0) { printf("error!"); } else { if(pid==0) printf("a\n"); else printf("b\n"); } } 结果是返回a,b或者b,a 因为fork调用将执行两次返回分别从子进程和父进程返回 由于父进程和子进程无关,父进程与子进程都可能先返回 在看一个程序 main() { pid_t a_pid,b_fork; if(a_pid=fork()<0) { printf("error!"); } else { if(a_pid==0) printf("b\n"); else printf("a\n"); } if(b_pid=fork()<0) { printf("error!"); } else { if(b_pid==0) printf("c\n"); else printf("a\n"); } } 如果是创建两个进程则出现结果 b c a a c a 事实上,理解fork()的关键在于它的返回点在哪里。fork最特殊的地方就在于他有两个甚至三个返回值,注意是同时返回两个值。其中pid=0的这个返回值用来执行子进程的代码,而大于0的一个返回值为父进程的代码块。第一次fork调用的时候生叉分为两个进程,不妨设为a父进程和b子进程。他们分别各自在第二次fork调用之前打印了b和a各一次;在第一次叉分的这两个进程中都含有 if(b_pid=fork()<0) { printf("error!"); } else { if(b_pid==0) printf("c\n"); else printf("a\n"); } } 这段代码。很明显,a父进程和b子进程在这段代码中又各自独立的被叉分为两个进程。这两个进程每个进程又都打印了a,c各一次。到此,在程序中总共打印三次a两次c和一次b。总共6个字母。 注:在第一次叉分为两个进程的时候父子进程含有完全相同的代码(第二次仍然相同),只是因为在父子进程中返回的PID的值不同,父进程代码中的PID的值大于0,子进程代码中的值等于0,从而通过if这样的分支选择语句来执行各自的任务。 我做如下修改 #include <unistd.h>; #include <sys/types.h>; main () { pid_t pid; printf("fork!"); // printf("fork!n"); pid=fork(); if (pid < 0) printf("error in fork!"); else if (pid == 0) printf("i am the child process, my process id is %dn",getpid()); else printf("i am the parent process, my process id is %dn",getpid()); } 结果是 [root@localhost c]# ./a.out fork!i am the child process, my process id is 4286 fork!i am the parent process, my process id is 4285 但我改成printf("fork!n");后,结果是 [root@localhost c]# ./a.out fork! i am the child process, my process id is 4286 i am the parent process, my process id is 4285 为什么只有一个fork!打印出来了?上一个为什么有2个? 我也来一下: wujiajia 的理解有些错误, printf("AAAAAAAA");//print 一次; 这里会print 2次 如果你将 printf("AAAAAA") 换成 printf("AAAAAAn") 那么就是只打印一次了. 主要的区别是因为有了一个 n 回车符号 这就跟Printf的缓冲机制有关了,printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上 但是,只要看到有 n 则会立即刷新stdout,因此就马上能够打印了. 运行了printf("AAAAAA") 后, AAAAAA 仅仅被放到了缓冲里,再运行到fork时,缓冲里面的 AAAAAA 被子进程继承了 因此在子进程度stdout缓冲里面就也有了 AAAAAA. 所以,你最终看到的会是 AAAAAA 被printf了2次!!!! 而运行 printf("AAAAAAn")后, AAAAAA 被立即打印到了屏幕上,之后fork到的子进程里的stdout缓冲里不会有 AAAAAA 内容 因此你看到的结果会是 AAAAAA 被printf了1次!!!!
用户377235 2013-7-2 14:25