一个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 ,请勿用于任何商业用途)
《一个fork的面试题》的相关评论
沙发!哈哈……
下面的英文摘自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.
了然
我ubuntu 10.04 64bit 的机器上验证了下,真是这样。
@vingc
哈哈,根据我之前的经验。文章中确实有点写反了,不过不影响整篇文章的主题理解。
@deanzhang 不小心写反了,已改正。
很有意思~呵呵,这里应该加上 pstree -p | grep fork,不然不会显示pid
谢了!已改正。
我觉着可以出个:一个fork的面试题(2)
请问下边的程序在以./a.out > tmp 的方式运行时,输出几个 ‘-‘。
a.c
在写daemon忘了setvbuf时,也很可能遇到。
对于错误个人喜欢: 1+1=
32 这样的修改方式.留下的痕迹.自省也可以表示真诚. 仅供参考.读了楼主的文章,很有收获,从一个很小的问题引出原理性的东西,这样的效果是最好的。我有一点不是很明白,请教一下
在使用printf(“-“);时,父进程在fork时将自身的缓冲区里的’-‘复制给了子进程,那么是不是说父进程到了程序结束时才把缓冲区里面的‘-’都打印出来?或者是将缓冲区复制给子进程之后,在下一次接收到’-‘才将上一次的’-‘输出?
谢谢。
你可以试一试下面的程序:
哈,这真是考基本功啊~
有点像平行世界的感觉,从fork开始,世界出现了两个分支,但是这两个分支开始的时候,状态什么的一模一样,后面发展却不一样了
只是想说printf( “-” )不会输出”_”
上学期的操作系统课题及实验遇到过类似的问题,当时挺清楚,现在有点忘了,看了你这篇文章,又让我思绪清楚了。。。
@陈皓
谢谢楼主的示例程序,现在清楚了。不过你最后一个printf(“\n”);那个’\’被你写成了’/’,哈哈。
我有时候觉得,程序没这么难,只是因为很多知识暂时不知道。多点去了解就好了。
我也知道fork的那些 只是不晓得printf的内部原理
感觉这个问题似乎也要求了解printf的内部原理才能正确作出题目来 所以如果真要出题考fork 不如加个 ‘\n’
不知道我理解的对不对,假如您所说的题目中,fork();和 printf(“-“)调换一下次序,是不是输出的应该是4个?
假如您说的题目,还是fork();和 printf(“-“)调换一下次序, printf(“-“)变换成 printf(“-\n”),是不是输出是3个?
@Bruce
我的理解:不加 \n 时因为‘_’在缓存里没有实际输出,所以 fork 和 printf 调换顺序没有关系。加上 \n 后因为要实际输出,所以 fork 和 printf 的顺序就有关系了。
@GamerH2o
我理解的是fork的位置不同,分叉的地方会不一样,这样会造成重复执行的区域会不同,所以才有之前的想法,调换次序输出会不一样吧!
@Bruce
我试验了一下,printf 里没 \n 时 fork 和 printf 的顺序对结果没有影响,都是八个‘_’;有 \n 时先 fork 再 printf 输出六个‘_’,先 printf 再 fork 输出三个’_’。用 GCC 默认选项编译的。
@tricky
我试了一下这段代码,重定向到文件时输出8个-,而直接输出在屏幕时输出6个-,重定向到文件时,如果不用setvbuf的话,默认的缓冲区类型是什么呢?遇到newline不会flush么?
@wyhao31
输出到文件时,标准I/O的stdout是完全缓冲的。也就是说换行不会引起flush。只有在显示调用fflush或exit时,可能flush缓冲。
输出到终端时,标准I/O的stdout默认是行缓冲。
不过如果你用stderr输出(默认不缓冲),或者setvbuf令stdout变成不缓冲,或者write调用,在任何情况下加不加\n是否重定向到文件,得到的结果都一样了。
相关问题在《Unix环境高级编程》都有很详细的介绍。
太绕了。
不太懂,顶一下。
@陈皓
如果子进程先运行呢?会不会依赖进程调度?
其实我觉得这种题就像高考题一样,故意把一些知识点揉在一起形成很复杂的情况,而实际工作中绝对不会写成这种样子。
如果来面试的人没看过的话,有10个人1个回答上来就不错了,实际情况很可能是100个人有1个。。。
PS:写了这么多年程序,APUE看了很多遍,但其实我工作上连fork一次都没用过,不知道我是少数还是多数?
@hackee
我的理解是:不依赖,因为在fork的时候完全复制了一份直接父进程,父进程或父父进程再怎么改也没关系。
讲得不错。
@kk
这个我觉得似乎是资源抢占造成的,父子进程都有标准输入设备块缓冲区的指针,只不过是后面运行的进程覆盖了前面进程在缓冲区输出的值。
Reading《现代操作系统》,今天刚看到fork()调用,书上的翻译实在是不敢恭维,搞得我一头雾水!没想到陈老师刚好发表这篇文章,解决疑惑了!真是万分感谢!
确实不错,很容易看懂。
写的简单易懂,这是最让人佩服的~
有些奥林匹克数学的意思。
是的,所以我只引用了一下man页的原文@deanzhang
不能同意更多!!!!
项目遇到过,换行后也不一定立即输出的。。。。。。必须flush才行
从dos编程初次接触*nix编程时,最不理解的就是fork了:为什么把自己整个都复制了?
我需要的子进程往往只需要做很少或完全不同的事情
可能线程才符合最直观的想法
本例,应该算是语言、运行机制的一个悲剧、失败:太多隐含的东西,不是好事
精读APUE中关于缓冲区类型及其缓冲方式的讲述,理解无障碍。
如果对输出有要求的话还是要在程序里面主动设置缓冲类型,就算例子中用的是printf(“-\n”);只要执行程序的时候把输出重定向到文件,还是会打出8个”-“
@haitao 做网络服务器编程,fork很重要。父进程负责监听,和建立连接。子进程负责响应客户端的请求。书上看来的,没实际经验。
@candochen
我的意思是:父进程为了。。。而需要子进程做单一的事情,很对。但是,子进程只需要它工作涉及的数据,而系统不应该把父进程所有的数据都复制给子进程。这样显然会加大内存开销。因为当时机器的内存还是很少的。
当然,现在想想,只复制部分数据,会导致子进程对变量的寻址复杂很多。不过,编译器如果能对子进程另外编译,地址计算时减掉那些不在子进程出现的数据空间,可能也没问题。
我当时也遇到了这个问题。
当时的问题是在for循环前,加了一句:printf(“hello”)
结果也是输出了两次 hello。没搞清除fork时,系统都做了哪些事情,还真难理解为啥是输出了两次hello。
看看understanding linux kernel第三章关于进程的东西就会更清楚内核是怎么实现的了,其实是否复制父进程地址空间应该是可以作选择的
嗯,这个在APUE里也有描述,是一个代码例子,不过作者只是文字叙述了一番,没有耗子这么详细深入浅出
提问:为什么下面程序,打印出的减号(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]
@wks
printf有缓冲,write无缓冲。
@jie
那个是clone函数吧
为什么在我的gcc 4.7.1版本下,运行第一个代码实例,多次,输出结果有2、4或6个-,就是没有8个-,唯独重定向到文件时才有8个,运行第二个,多次,都是6个,但是重定向后又是8个.没明白。(系统环境:arch发行版,3.4.4-2的内核)
我用了一个本办法理解了一下
希望浩哥指点,谢谢!