写这篇博客的原因是在考试中遇到一个很 细 的题目。

题目

题目长这样

问最后的运行结果,当然有 fork 存在肯定是多种情况的,当时乍一看这一题,就很自信地写了

xbay ,xaby,xayb

结果却是一个大红叉子,我细数也就四次输出,为什么答案长度为 5??

我运行一遍之后,发现结果:

纳尼,真的是 5 个,哪来的五个呢。

细细品味一下,原来有个天坑。

分析

首先 Linux 的 C,它的缓冲区默认不初始化的,如果不初始化缓冲区,就会造成每隔一行才打印一次,或者是等到程序 exit 之后才打印。

如果在 Linux 下面有这样的语句

1
2
printf("input");
scanf("%s");

那么你大概率是看不到开头这个 input 的,只有程序结束了才能看到,原因就是调用的 IO 函数它会先把数据存到对应的 FILE 结构体上。

调试

我们启动 gdb 调试一下,断在第一个 putchar 下面,在进去之前我们可以看到,stdout 没有初始化。

然后 putchar 内部调用一个 __overflow 函数

最后进入到 _IO_file_overflow

在里面有一个分配缓冲区的动作,如果是程序自己设置的缓冲区,那么这个缓冲区会被分配到堆上面。

然后我们看看此时的 stdout 的结构

可以看到 _IO_write_ptr 在 _IO_write_base 的 +1 位置,那么这里面应该是存了一个 x,并且直到 putchar 调用结束,都不会输出出来。

也可以在缓冲区上看到这个字符。

这里选择追踪子进程,然后在 fork 结束之后看看结果。

程序在退出之后输出了 xay,很正常,因为仅仅跟踪父进程,确实就是输出这三个字符,但是此时我们可以打开 stdout 的结构体看看,发现缓冲区的数据也被复制了过来没有输出出来。

也就是说等会还会调用一个 putchar 再存到 缓冲区中,最后结束打印出两个字符就得到了正确答案。

可以看到缓冲区中有两个字符,然后我们进入 exit 看一看它会触发什么行为,可以发现 exit 当中,有一个函数为 _IO_cleanup,望文生义就是清理 IO 缓冲区用的嘛,主动调用 exit 或者是 main return 0 的时候都会触发。

最后当然就是调用 _IO_do_write 去打印缓冲区的内容。

因此这个题目其实只有两个情况,因为只有程序结束一瞬间才会打印,并且打印完,要么父进程先,要么子进程先。

这也就衍生出了一个问题,就是 Linux 的 c 程序默认不是实时打印,所以一般情况下我们需要初始化 stdout 的结构体。

1
setbuf(stdout,0);

初始化的 stdout 结构体会把缓冲区分配到 glibc 当中,就可以允许我们实时打印了,实时打印之后,答案就如我们所想的一样了。

或者每次输出之后,调用 fflush(stdout) 去刷新缓冲区达到实时打印的效果。

或者是使用 write 去打印,因为 write 函数直接走系统调用输出,不会经过 stdout 结构体,所以这是唯一一个不需要初始化缓冲区可以实时打印的函数。

总结

人有失手,马有失蹄,这个知识点其实以前知道的,但是万万没想到考试能出到这个知识点。

也算记忆更加深刻了吧,以后需要时刻注意。