一个fork的面试题

一个fork的面试题

前两天有人问了个关于Unix的fork()系统调用的面试题,这个题正好是我大约十年前找工作时某公司问我的一个题,我觉得比较有趣,写篇文章与大家分享一下。这个题是这样的:

题目:请问下面的程序一共输出多少个“-”?

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

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

   wait(NULL);
   wait(NULL);

   return 0;
}

如果你对fork()的机制比较熟悉的话,这个题并不难,输出应该是6个“-”,但是,实际上这个程序会很tricky地输出8个“-”。

要讲清这个题,我们首先需要知道fork()系统调用的特性,

  • fork()系统调用是Unix下以自身进程创建子进程的系统调用,一次调用,两次返回,如果返回是0,则是子进程,如果返回值>0,则是父进程(返回值是子进程的pid),这是众为周知的。
  • 还有一个很重要的东西是,在fork()的调用处,整个父进程空间会原模原样地复制到子进程中,包括指令,变量值,程序调用栈,环境变量,缓冲区,等等。

所以,上面的那个程序为什么会输入8个“-”,这是因为printf(“-“);语句有buffer,所以,对于上述程序,printf(“-“);把“-”放到了缓存中,并没有真正的输出(参看《C语言的迷题》中的第一题),在fork的时候,缓存被复制到了子进程空间,所以,就多了两个,就成了8个,而不是6个。

另外,多说一下,我们知道,Unix下的设备有“块设备”和“字符设备”的概念,所谓块设备,就是以一块一块的数据存取的设备,字符设备是一次存取一个字符的设备。磁盘、内存都是块设备,字符设备如键盘和串口。块设备一般都有缓存,而字符设备一般都没有缓存

对于上面的问题,我们如果修改一下上面的printf的那条语句为:

printf("-\n");

或是

 printf("-");
fflush(stdout);

就没有问题了(就是6个“-”了),因为程序遇到“\n”,或是EOF,或是缓中区满,或是文件描述符关闭,或是主动flush,或是程序退出,就会把数据刷出缓冲区。需要注意的是,标准输出是行缓冲,所以遇到“\n”的时候会刷出缓冲区,但对于磁盘这个块设备来说,“\n”并不会引起缓冲区刷出的动作,那是全缓冲,你可以使用setvbuf来设置缓冲区大小,或是用fflush刷缓存。

我估计有些朋友可能对于fork()还不是很了解,那么我们把上面的程序改成下面这样:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
   int i;
   for(i=0; i<2; i++){
      fork();
      //注意:下面的printf有“\n”
      printf("ppid=%d, pid=%d, i=%d \n", getppid(), getpid(), i);
   }
   sleep(10); //让进程停留十秒,这样我们可以用pstree查看一下进程树
   return 0;
}

于是,上面这段程序会输出下面的结果,(注:编译出的可执行的程序名为fork)

ppid=8858, pid=8518, i=0
ppid=8858, pid=8518, i=1
ppid=8518, pid=8519, i=0
ppid=8518, pid=8519, i=1
ppid=8518, pid=8520, i=1
ppid=8519, pid=8521, i=1

$ pstree -p | grep fork
|-bash(8858)-+-fork(8518)-+-fork(8519)---fork(8521)
|            |            `-fork(8520)

面对这样的图你可能还是看不懂,没事,我好事做到底,画个图给你看看:

注意:上图中的我用了几个色彩,相同颜色的是同一个进程。于是,我们的pstree的图示就可以成为下面这个样子:(下图中的颜色与上图对应)

这样,对于printf(“-“);这个语句,我们就可以很清楚的知道,哪个子进程复制了父进程标准输出缓中区里的的内容,而导致了多次输出了。(如下图所示,就是我阴影并双边框了那两个子进程)

现在你明白了吧。(另,对于图中的我本人拙劣的配色,请见谅!)

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——
好烂啊有点差凑合看看还不错很精彩 (51 人打了分,平均分: 4.88 )
Loading...

一个fork的面试题》的相关评论

  1. 下面的英文摘自fork的man page
    Return Value

    On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.

  2. 我觉着可以出个:一个fork的面试题(2)
    请问下边的程序在以./a.out > tmp 的方式运行时,输出几个 ‘-‘。

    a.c

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h> 
     
    int main(void)
    {
       int i;
       for(i=0; i<2; i++){
          fork();
          printf("-\n");
       }
     
       return 0;
    }

    在写daemon忘了setvbuf时,也很可能遇到。

  3. 对于错误个人喜欢: 1+1=32 这样的修改方式.留下的痕迹.自省也可以表示真诚. 仅供参考.

  4. 读了楼主的文章,很有收获,从一个很小的问题引出原理性的东西,这样的效果是最好的。我有一点不是很明白,请教一下

    在使用printf(“-“);时,父进程在fork时将自身的缓冲区里的’-‘复制给了子进程,那么是不是说父进程到了程序结束时才把缓冲区里面的‘-’都打印出来?或者是将缓冲区复制给子进程之后,在下一次接收到’-‘才将上一次的’-‘输出?

    谢谢。

  5. 有点像平行世界的感觉,从fork开始,世界出现了两个分支,但是这两个分支开始的时候,状态什么的一模一样,后面发展却不一样了

  6. 上学期的操作系统课题及实验遇到过类似的问题,当时挺清楚,现在有点忘了,看了你这篇文章,又让我思绪清楚了。。。

  7. 我有时候觉得,程序没这么难,只是因为很多知识暂时不知道。多点去了解就好了。

  8. 我也知道fork的那些 只是不晓得printf的内部原理

    感觉这个问题似乎也要求了解printf的内部原理才能正确作出题目来 所以如果真要出题考fork 不如加个 ‘\n’

  9. 不知道我理解的对不对,假如您所说的题目中,fork();和 printf(“-“)调换一下次序,是不是输出的应该是4个?
    假如您说的题目,还是fork();和 printf(“-“)调换一下次序, printf(“-“)变换成 printf(“-\n”),是不是输出是3个?

  10. @Bruce
    我的理解:不加 \n 时因为‘_’在缓存里没有实际输出,所以 fork 和 printf 调换顺序没有关系。加上 \n 后因为要实际输出,所以 fork 和 printf 的顺序就有关系了。

  11. @GamerH2o
    我理解的是fork的位置不同,分叉的地方会不一样,这样会造成重复执行的区域会不同,所以才有之前的想法,调换次序输出会不一样吧!

  12. @Bruce
    我试验了一下,printf 里没 \n 时 fork 和 printf 的顺序对结果没有影响,都是八个‘_’;有 \n 时先 fork 再 printf 输出六个‘_’,先 printf 再 fork 输出三个’_’。用 GCC 默认选项编译的。

  13. @tricky
    我试了一下这段代码,重定向到文件时输出8个-,而直接输出在屏幕时输出6个-,重定向到文件时,如果不用setvbuf的话,默认的缓冲区类型是什么呢?遇到newline不会flush么?

  14. @wyhao31
    输出到文件时,标准I/O的stdout是完全缓冲的。也就是说换行不会引起flush。只有在显示调用fflush或exit时,可能flush缓冲。
    输出到终端时,标准I/O的stdout默认是行缓冲。

    不过如果你用stderr输出(默认不缓冲),或者setvbuf令stdout变成不缓冲,或者write调用,在任何情况下加不加\n是否重定向到文件,得到的结果都一样了。

    相关问题在《Unix环境高级编程》都有很详细的介绍。

  15. 其实我觉得这种题就像高考题一样,故意把一些知识点揉在一起形成很复杂的情况,而实际工作中绝对不会写成这种样子。
    如果来面试的人没看过的话,有10个人1个回答上来就不错了,实际情况很可能是100个人有1个。。。
    PS:写了这么多年程序,APUE看了很多遍,但其实我工作上连fork一次都没用过,不知道我是少数还是多数?

  16. @hackee
    我的理解是:不依赖,因为在fork的时候完全复制了一份直接父进程,父进程或父父进程再怎么改也没关系。

  17. @kk
    这个我觉得似乎是资源抢占造成的,父子进程都有标准输入设备块缓冲区的指针,只不过是后面运行的进程覆盖了前面进程在缓冲区输出的值。

  18. Reading《现代操作系统》,今天刚看到fork()调用,书上的翻译实在是不敢恭维,搞得我一头雾水!没想到陈老师刚好发表这篇文章,解决疑惑了!真是万分感谢!

  19. Dvwei :
    Reading《现代操作系统》,今天刚看到fork()调用,书上的翻译实在是不敢恭维,搞得我一头雾水!没想到陈老师刚好发表这篇文章,解决疑惑了!真是万分感谢!

    不能同意更多!!!!

  20. tricky :
    @wyhao31
    输出到文件时,标准I/O的stdout是完全缓冲的。也就是说换行不会引起flush。只有在显示调用fflush或exit时,可能flush缓冲。
    输出到终端时,标准I/O的stdout默认是行缓冲。
    不过如果你用stderr输出(默认不缓冲),或者setvbuf令stdout变成不缓冲,或者write调用,在任何情况下加不加\n是否重定向到文件,得到的结果都一样了。
    相关问题在《Unix环境高级编程》都有很详细的介绍。

    项目遇到过,换行后也不一定立即输出的。。。。。。必须flush才行

  21. 从dos编程初次接触*nix编程时,最不理解的就是fork了:为什么把自己整个都复制了?
    我需要的子进程往往只需要做很少或完全不同的事情

    可能线程才符合最直观的想法

    本例,应该算是语言、运行机制的一个悲剧、失败:太多隐含的东西,不是好事

  22. 如果对输出有要求的话还是要在程序里面主动设置缓冲类型,就算例子中用的是printf(“-\n”);只要执行程序的时候把输出重定向到文件,还是会打出8个”-“

  23. @haitao 做网络服务器编程,fork很重要。父进程负责监听,和建立连接。子进程负责响应客户端的请求。书上看来的,没实际经验。

  24. @candochen
    我的意思是:父进程为了。。。而需要子进程做单一的事情,很对。但是,子进程只需要它工作涉及的数据,而系统不应该把父进程所有的数据都复制给子进程。这样显然会加大内存开销。因为当时机器的内存还是很少的。
    当然,现在想想,只复制部分数据,会导致子进程对变量的寻址复杂很多。不过,编译器如果能对子进程另外编译,地址计算时减掉那些不在子进程出现的数据空间,可能也没问题。

  25. 我当时也遇到了这个问题。
    当时的问题是在for循环前,加了一句:printf(“hello”)
    结果也是输出了两次 hello。没搞清除fork时,系统都做了哪些事情,还真难理解为啥是输出了两次hello。

  26. 看看understanding linux kernel第三章关于进程的东西就会更清楚内核是怎么实现的了,其实是否复制父进程地址空间应该是可以作选择的

  27. 嗯,这个在APUE里也有描述,是一个代码例子,不过作者只是文字叙述了一番,没有耗子这么详细深入浅出

  28. 提问:为什么下面程序,打印出的减号(8个)和加号数量(6个)不一样?而且加号比减号先出现?

    [code language=”c”]
    #include <unistd.h>
    #include <stdio.h>

    int main(int argc, char *argv[]) {
    int i;

    for (i=0; i<2; i++) {
    fork();
    printf("-");
    write(1, "+", 1);
    }

    return 0;
    }
    [/code]

  29. 为什么在我的gcc 4.7.1版本下,运行第一个代码实例,多次,输出结果有2、4或6个-,就是没有8个-,唯独重定向到文件时才有8个,运行第二个,多次,都是6个,但是重定向后又是8个.没明白。(系统环境:arch发行版,3.4.4-2的内核)

  30. 我用了一个本办法理解了一下

    int main(void)
    {
            fork();
            printf("-");
            fork(); 
            printf("-");
            return 0;
    }

    希望浩哥指点,谢谢!

发表评论

电子邮件地址不会被公开。 必填项已用*标注