原创 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次!!!!

PARTNER CONTENT

文章评论1条评论)

登录后参与讨论

用户377235 2013-7-2 14:25

例子很好
相关推荐阅读
用户116683 2014-01-29 15:17
c语言注释删除器v1.0
1.删除c语言的注释(//和/**/)。 2.操作前先备份好源代码。 3.可执行文件见附件。...
用户116683 2013-11-07 13:13
大端模式小端模式
以下程序是在同一台式机上vc6测试: ----------------------------------------- //输出不同类型所占内存空间长度 #include "stdio....
用户116683 2013-10-29 17:06
#ifdef __cplusplus深度剖析
注:本博文为转载博文 原作者: yundu 原地址:http://bbs.ednchina.com/BLOG_ARTICLE_251752.HTM   时常在cpp的代码之中看到...
用户116683 2013-10-19 22:41
用Diskgenius对电脑硬盘坏道进行隔离
故障发生:   电脑在使用过程中,突然关机了,再次开机,一直起不来。   一种情况是显示win7进度条不动。   另一种情况是win7启动进度条后,进入上次关机不妥F盘需要...
用户116683 2011-10-08 11:50
assert()在release中没有任何作用(转)
最近在调试别人写的代码。被assert()狠狠的涮了一把。现在将涮我的代码片段摘录如下:  else if( m_radarLevel > c_maxRadarLevel )  {//可以在预计...
用户116683 2011-09-26 10:07
AIX线程支持可调参数(转)
AIX线程支持可调参数(转自:http://ebsblog.blog.163.com/blog/static/127949789200981533034710/)有很多可调的线程支持参数。ACT_TI...
我要评论
1
9
关闭 站长推荐上一条 /3 下一条