一个很有趣的fork面试程序,和大家分享下经验

682 查看

为了暑期的腾讯实习,拿到offer之后一直在linux的道路上不断学习。
这两天逛了下酷壳大神的blog(http://coolshell.cn/articles/7965.html),偶然看到一个关于fork小问题,虽然之前想通了,不过还是值得回味并且和大家分享下的。

大家有兴趣可以想想,下面输出了多少个“g”?

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
   int i;
   for(i=0; i<2; i++){
      fork();
      printf("g");
   }

   wait(NULL);
   wait(NULL);

   return 0;
}

也许你很快就说,那么简单,第一次循环,fork后2个进程,第二次再fork出4个,一共6个进程,肯定是6个“g”

恭喜你,如果我是面试官,那么你已经跪了 = =

答案:会输出8个“g”

有一个很重要的东西是,在fork()的调用处,整个父进程空间会原模原样地复制到子进程中,包括指令,变量值,程序调用栈,环境变量,缓冲区,等等。

等等!!什么,你说全部复制过去?那么我可以解释为:
上面的那个程序为什么会输入8个“-”,这是因为printf(“-“);语句有buffer

我们来试试printf的缓冲区:

#include <stdio.h>
#include <unistd.h>    //for sleep()

int main(void)
{
    printf("/nstart the dead loop/n");
    while(1)
    {    
        printf("/b->");
        fflush(stdout);//刷新输出缓冲区
        usleep(100000);
    }
    return 1;
}

哦哦!!如果不做fflush这个动作,上边的输出便不会显示到屏幕上咯?
除非其中有换行操作或者缓冲区,这也许就是所谓的“到终端行规程”!

呃呃,不要脱离了重点,我们还是回到刚才的fork问题上~

既然我们知道了printf有缓冲区,那么,又如何?

容我引用下酷壳大神美丽的图解:

你可以清楚的看到,用阴影框标注出来的两个“最终版子进程(i=2)”

他们在fork拷贝的时候,就将 parent的缓冲区一并拷贝过去,这时“g”还在缓冲区
那么,在输出的时候,两个子进程才一并把“g”从缓冲区输出来,那么是不是两个子进程没人多输出了一个?
1+1+1+1+12+12=8个

OK,我知道你和我一样可能有点懵懵的,我改下程序咯~

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
   int i;
   for(i=0; i<2; i++){
      fork();
      printf("g  ppid=%d, pid=%d, i=%d ", getppid(), getpid(), i);
   }

   wait(NULL);
   wait(NULL);

   return 0;

你可以gcc下这个程序,在我的电脑里输出是这样的:

g  ppid=3082, pid=3128, i=0 g  ppid=3128, pid=3130, i=1 
g  ppid=3128, pid=3129, i=0 g  ppid=3129, pid=3131, i=1 
g  ppid=3128, pid=3129, i=0 g  ppid=3128, pid=3129, i=1 
g  ppid=3082, pid=3128, i=0 g  ppid=3082, pid=3128, i=1

可以看到ppid为3082 pid为3128的输出有3次
同样的,ppid为3128 pid为3129的输出有3次

你将显然发现,原本i=0时应该只会有两个“g”被输出,但是现在居然诡异地多出了两个?到底是哪两个呢?
我索性标注下这个输出结果吧~

(1)g  ppid=3082, pid=3128, i=0 (5)g  ppid=3128, pid=3130, i=1 
(2)g  ppid=3128, pid=3129, i=0 (6)g  ppid=3129, pid=3131, i=1 
(3)g  ppid=3128, pid=3129, i=0 (7)g  ppid=3128, pid=3129, i=1 
(4)g  ppid=3082, pid=3128, i=0 (8)g  ppid=3082, pid=3128, i=1

明显的,(1)和(4)是“不可能”有两个的,(2)和(3)也是,现在你知道了吧,i=0时的两个进程,在下一次fork的时候各自成为了下一个父进程,在这一次fork时,他们的信息被完全复制到了子进程,那么最后一步每一个子进程缓冲区里还存有多余的“g”(见上图)

你还可以用pstree -p | grep fork命令试下
可以得出一个树状的结构:

$ pstree -p | grep fork
|-bash(3082)-+-fork(3128)-+-fork(3129)---fork(3131)
|            |            `-fork(3130)

那么,最后我们再试试,在printf一个g的后面加上一个换行符(或者fflush)

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
   int i;
   for(i=0; i<2; i++){
      fork();
      printf("g\n");
   }

   wait(NULL);
   wait(NULL);

   return 0;
}

输出的结果将是换行的6个“g
原因就在于,每一次换行,缓冲区的“g”,就会把数据“刷”出缓冲区

(其实或是EOF,或是缓中区满,或是文件描述符关闭,或是主动flush,或是程序退出)

下次大家在面试的时候如果遇到这个问题,相信都可以迎刃而解了吧 ~~~
(再次感谢酷壳大神,大家对我的看法如果有问题欢迎交流!)