linux系统下 fork()系统调用, 关于父子进程缓存问题的小坑
1. 首先看一个简单示例程序如下
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
int main(int argc, char **argv)
{
printf("before fork: in father...\n");
pid_t child = fork();
assert( child >= 0 );
if ( 0 == child )
printf("in child...\n");
if ( child > 0 )
printf("after fork: in father...\n");
}
在我本机测试, 输出如下:
before fork: in father...
after fork: in father...
in child...
输出和我们预想一致. 一共输出3行. 目前一切正常.
请继续往下看.
2. 这个示例程序和第一个基本相同, 但是输出时, 换行符”\n”略有区别.
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
int main(int argc, char **argv)
{
// 注意这里没有换行, 多加了几个空格, 使得打印容易识别.
printf("before fork: in father... ");
pid_t child = fork();
assert( child >= 0 );
if ( 0 == child )
printf("in child...\n");
if ( child > 0 )
printf("after fork: in father...\n");
}
程序输出如下:
before fork: in father... after fork: in father...
before fork: in father... in child...
这次输出就不在我们意料之中了!!!
“before fork: in father… ”
是在fork()调用之前的父进程中调用输出的.
但是竟然在子进程中也输出了 !!!
感觉好像是在fork之前父进程中的printf打印的字符, 被copy到子进程中了?
没错, 正是如此.原理如下:
我们这次在fork之前printf打印语句时, 没有带”\n”参数( 终端是行缓存的. )
所以这条语句执行后, 打印信息并没有被立刻输出到屏幕.而是缓存起来了.
终端stdout是行缓存, 输出只有在遇到’\n’字符,或者输出超过缓存长度时, 才会刷新缓存–真正写入文件( 这里的写文件就是终端stdout )
这样当调用fork() 函数时, 父进程中的输出缓存还没有刷新到文件.
fork()调用除了会复制父进程的所有已打开文件描述符, 还会复制父进程的缓存到子进程中.
所以父进程的缓存 “before fork: in father… ” 就被copy到子进程中了..
缓存复制到子进程空间后, 就像子进程自己调用了printf的效果一样.
结果程序输出就出现了上面诡异的一幕.
至于程序1为什么没有出现这样的一幕, 因为在fork之前父进程printf 带了’\n’, stdout是行缓存, 遇到’\n’时就会刷新输出到stdout的缓存.
在fork时, 父进程的缓存已刷新到文件.缓存被删除. 所以不会copy到子进程空间.
3. 这次对程序1 输出重定向到文件, 查看结果.
重定向输出到fork.log中, 代码如下:
$ ./fork-out > fork.log
$ cat fork.log
输出如下:
before fork: in father...
after fork: in father...
before fork: in father...
in child...
咦等等, 程序1 不是正常吗, 为什么我仅仅重定向了一次, 结果就成这样了.
不是说’\n’ 在终端stdout是行缓存的吗, 遇到’\n’就会刷新输出缓存!
是的, 之前说的没错. 请注意:”终端stdout是行缓存的”, 但是文件重定向之后, 这时候输出stdout文件已经不是终端了, 而是重定向到了fork.log文件.
fork.log 是一个普通文件, 普通文件的缓存是基于长度的, 也就是说输出到普通文件的数据, 在缓存后, 只有达到了缓存上限, 或者手动调用 flush/sync 等系统调用刷新, 才会清空缓存并刷新到文件中.
而 “行缓存” 是只有终端stdout才有的.
同时注意终端stderr是没有缓存的, 因为输出到stderr的信息都是敏感信息. 所以系统并不缓存.
但是在stderr重定向到文件后, 也会遇到相同的缓存级别改变的情况. 读者感兴趣请自行尝试.
4. 所以正确做法是:
调用fork程序前, 手动调用flush/fflush函数手动刷新输出缓存.
避免重定向后出现父子进程诡异的输出copy问题.