这是一道关于 Unix fork() 系统调用的经典面试题:每一轮 fork 产生的新进程数量等于当前正在运行的进程数量。题目问——下面这个程序一共输出多少个 -?
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i;
for (i = 0; i < 2; i++) {
fork();
printf("-");
}
return 0;
}
如果你对 fork() 的机制比较熟悉,会认为答案是 6 个 -,但实际上这个程序会”tricky”地输出 8 个 -。
fork() 的两个关键特性
要讲清这道题,首先要知道 fork() 系统调用的两个特性:
fork()是 Unix 下以自身进程创建子进程的系统调用,一次调用、两次返回:返回值是 0 则是子进程,返回值 > 0 则是父进程(返回值是子进程的 pid)。- 在
fork()调用处,整个父进程空间会原模原样地复制到子进程中,包括指令、变量值、调用栈、环境变量、缓冲区等等。
为什么是 8 个 -
关键在于 printf("-") 有缓冲区:printf("-") 把 - 放到了缓存里,并没有真正输出。在 fork 的时候,缓存被复制到了子进程空间,于是多出了两个 -,结果就成了 8 个而不是 6 个。
顺带一提,Unix 下的设备有”块设备”和”字符设备”的概念:
- 块设备:以一块一块的数据存取,如磁盘、内存,一般有缓存;
- 字符设备:一次存取一个字符,如键盘、串口,一般没有缓存。
对于上面的问题,把 printf 那条语句改成下面任意一种,就没问题了(即输出 6 个 -):
printf("-\n");
printf("-");
fflush(stdout);
因为程序遇到 \n、EOF、缓冲区满、文件描述符关闭、主动 flush 或程序退出时,都会把数据刷出缓冲区。
需要注意:标准输出是行缓冲,所以遇到 \n 会刷出缓冲区;但磁盘这种块设备是全缓冲,\n 不会引起刷出动作。你可以用 setvbuf 设置缓冲区大小,或用 fflush 刷缓存。
进一步观察进程树
如果对 fork() 还不熟,可以把程序改成下面这样,加 \n 便于观察:
#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 查看进程树:
$ pstree -p | grep fork
|-bash(8858)-+-fork(8518)-+-fork(8519)---fork(8521)
| | `-fork(8520)