一.程序代码
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char** argv)
{
int i;
for ( i = 0; i < 2; i++)
{
fork();
printf("hello\n");
}
for ( i = 0; i < 2; i++)
{
wait(NULL);
}
exit(0);
}
二.问题挑战
问题一:以上程序的输出有几个hello?
问题二:wc -l命令可以统计文本有多少行,用该命令统计以上程序的输出,结果是多少?
三.答案公布
四.程序解释
第一问解析
第一问不难,只要仔细一点,在纸上画进程图或状态机图就能得到答案。
进程图如下:
共执行了六次printf(“hello\n”);语句,所以就会输出六个hello。
第二问解析
刚开始我也很疑惑,为什么输出6个hello但是wc命令却统计出8行数据。
别急,用strace命令来追踪一下系统调用,看一些关键的系统调用就行。
当我看到倒数第四行时好像发现了什么。
write(1, "hello\nhello\n", 12) = 12
printf函数调用了write这个系统调用,第一个参数是1,表示输出到标准输出stdout。但是第二个参数就出乎我意料了,输出的为什么是hello\nhello\n,不应该是hello\n吗?后来经过一番资料查找再总结后得出以下两条结论:
- 标准输出如果指向的是终端(tty),那么使用的缓冲区就是行缓冲区(line bufffer),缓冲区读取到一个换行符(\n)或者缓冲区满了就会把缓冲区的内容输出到终端。
- 标准输出如果指向的是文件(file)、管道(pipe)、网络套接字(socket)时,使用的缓冲区就是全缓冲区(full buffer),当缓冲区满了的时候才会把缓冲区的内容输出到指定位置。
有了上面的两条结论我们离答案又前进了一大步。
现在先把注意力转移到fork()函数上面。fork()函数的作用就是创建进程,fork()产生的子进程是父进程的一份拷贝,也就是说fork()完成后产生的子进程和父进程有两点区别,一是子进程和父进程的进程号(PID)不同,二是子进程和父进程的返回值不同(子进程的返回值是0,父进程的返回值是子进程的PID)。其他的内容(进程的内存,缓存,寄存器状态,程序计数器(PC),库函数内部状态等等)父子进程完全一样,没有任何区别。
那么现在结合./fork_hello | wc -l这条命令的原理(原理就是把./fork_hello的输出放在一个管道中,用wc-l来统计管道中文本的行数),答案已经出来了。
全部连贯起来就是:在i=0的时候父子进程虽然执行了printf(“hello\n”);这个语句,但是printf这个函数调用底层的write系统调用,write把hello\n这个字符串放在了全缓冲区(full buffer)里面,并没有立即输出。当进行i++后,i=1,父子进程分别fork()了一次,此时系统里面有四个进程,新产生的两个子进程由于是父进程的拷贝,所以这两个新子进程的全缓冲区里面也有hello\n这个字符串。现在再执行printf(“hello\n”)语句,printf调用write,write向全缓冲区里面又一次写入hello\n。所以现在系统里面的四个进程的全缓冲区的内容就是hello\nhello\n。最后再main函数返回后操作系统把这四个进程的缓冲区写入管道中,此时管道的内容就是hello\nhello\nhello\nhello\nhello\nhello\nhello\nhello\n。wc -l 命令在管道中统计到8个换行符,于是最后的结果就是8。
五.总结
这个c程序虽然简短,但是要回答第二问并不简单。解决第二问需要知道printf底层的系统调用,line buffer和full buffer的区别,fork系统调用底层的拷贝原理,Linux操作系统中./fork_hello | wc -l命令的原理等等。只有对底层原理了如指掌才能对上层的运行结果做出正确合理的解释。