“fork()”之后的 printf 异常
Posted
技术标签:
【中文标题】“fork()”之后的 printf 异常【英文标题】:printf anomaly after "fork()" 【发布时间】:2011-02-01 14:33:09 【问题描述】:操作系统:Linux,语言:纯 C
我正在学习一般的 C 编程,以及特殊情况下的 UNIX 下的 C 编程。
在使用fork()
调用后,我检测到printf()
函数的奇怪行为(对我而言)。
代码
#include <stdio.h>
#include <system.h>
int main()
int pid;
printf( "Hello, my pid is %d", getpid() );
pid = fork();
if( pid == 0 )
printf( "\nI was forked! :D" );
sleep( 3 );
else
waitpid( pid, NULL, 0 );
printf( "\n%d was forked!", pid );
return 0;
输出
Hello, my pid is 1111
I was forked! :DHello, my pid is 1111
2222 was forked!
为什么子输出中出现第二个“Hello”字符串?
是的,这正是父级启动时打印的内容,父级的pid
。
但是!如果我们在每个字符串的末尾放置一个\n
字符,我们会得到预期的输出:
#include <stdio.h>
#include <system.h>
int main()
int pid;
printf( "Hello, my pid is %d\n", getpid() ); // SIC!!
pid = fork();
if( pid == 0 )
printf( "I was forked! :D" ); // removed the '\n', no matter
sleep( 3 );
else
waitpid( pid, NULL, 0 );
printf( "\n%d was forked!", pid );
return 0;
输出:
Hello, my pid is 1111
I was forked! :D
2222 was forked!
为什么会这样?这是正确的行为,还是错误?
【问题讨论】:
【参考方案1】:原因是格式字符串末尾没有\n
,值不会立即打印到屏幕上。相反,它在进程中缓冲。这意味着它实际上是在 fork 操作之后才被打印出来的,因此你会打印两次。
添加\n
会强制刷新缓冲区并输出到屏幕。这发生在分叉之前,因此只打印一次。
您可以使用fflush
方法强制执行此操作。例如
printf( "Hello, my pid is %d", getpid() );
fflush(stdout);
【讨论】:
fflush(stdout);
似乎是这里更正确的答案。【参考方案2】:
我注意到<system.h>
是一个非标准的标头;我将其替换为<unistd.h>
,代码编译干净。
当你的程序输出到终端(屏幕)时,它是行缓冲的。当程序的输出进入管道时,它是完全缓冲的。您可以通过标准 C 函数setvbuf()
和_IOFBF
(全缓冲)、_IOLBF
(行缓冲)和_IONBF
(无缓冲)模式来控制缓冲模式。
您可以在修改后的程序中证明这一点,方法是将程序的输出通过管道传输到例如cat
。即使printf()
字符串末尾有换行符,您也会看到双重信息。如果您直接将其发送到终端,那么您只会看到一大堆信息。
故事的寓意是在分叉之前要小心调用fflush(0);
以清空所有 I/O 缓冲区。
按要求逐行分析(大括号等被删除 - 前导空格被标记编辑器删除):
printf( "Hello, my pid is %d", getpid() );
pid = fork();
if( pid == 0 )
printf( "\nI was forked! :D" );
sleep( 3 );
else
waitpid( pid, NULL, 0 );
printf( "\n%d was forked!", pid );
分析:
-
将“你好,我的 pid 是 1234”复制到标准输出缓冲区。因为末尾没有换行符,并且输出在行缓冲模式(或全缓冲模式)下运行,所以终端上不会出现任何内容。
为我们提供了两个独立的进程,在标准输出缓冲区中使用完全相同的材料。
孩子有
pid == 0
并执行第4行和第5行;父级的 pid
具有非零值(这两个进程之间的少数区别之一 - 来自 getpid()
和 getppid()
的返回值是另外两个)。
将换行符和“I was forked! :D”添加到子级的输出缓冲区。第一行输出出现在终端上;其余部分保存在缓冲区中,因为输出是行缓冲的。
一切停止 3 秒。在此之后,孩子通过main结束时的return正常退出。此时,标准输出缓冲区中的剩余数据被刷新。由于没有换行符,这会将输出位置留在行尾。
父母来了。
父母等待孩子完成死亡。
父级添加了一个换行符并且“1345 被分叉了!”到输出缓冲区。在子级生成的不完整行之后,换行符将“Hello”消息刷新到输出。
parent现在通过main结束时的return正常退出,残留数据被flush;由于末尾仍然没有换行符,所以光标位置在感叹号之后,并且shell提示符出现在同一行。
我看到的是:
Osiris-2 JL: ./xx
Hello, my pid is 37290
I was forked! :DHello, my pid is 37290
37291 was forked!Osiris-2 JL:
Osiris-2 JL:
PID 编号不同 - 但整体外观清晰。在printf()
语句的末尾添加换行符(这很快成为标准做法)会大大改变输出:
#include <stdio.h>
#include <unistd.h>
int main()
int pid;
printf( "Hello, my pid is %d\n", getpid() );
pid = fork();
if( pid == 0 )
printf( "I was forked! :D %d\n", getpid() );
else
waitpid( pid, NULL, 0 );
printf( "%d was forked!\n", pid );
return 0;
我现在明白了:
Osiris-2 JL: ./xx
Hello, my pid is 37589
I was forked! :D 37590
37590 was forked!
Osiris-2 JL: ./xx | cat
Hello, my pid is 37594
I was forked! :D 37596
Hello, my pid is 37594
37596 was forked!
Osiris-2 JL:
请注意,当输出到终端时,它是行缓冲的,因此“Hello”行出现在 fork()
之前,并且只有一个副本。当输出通过管道传送到cat
时,它是完全缓冲的,因此在fork()
之前不会出现任何内容,并且两个进程在缓冲区中都有要刷新的“Hello”行。
【讨论】:
好的,我知道了。但是我仍然无法向自己解释为什么“缓冲区垃圾”出现在子输出中新打印行的末尾?但是等等,现在我怀疑它真的是 CHILD 的输出。哦,你能解释一下为什么输出看起来完全一样(旧字符串之前的新字符串),一步一步,所以我将非常感激。还是谢谢你! 非常令人印象深刻的解释!非常感谢,终于明白了! P.S.:我之前给你投了票,现在又傻傻的点了“向上箭头”,所以投票就消失了。但我不能再给你一次,因为“答案太老了”:( P.P.S.:我在其他问题上给了你一票。再次感谢你!【参考方案3】:fork()
有效地创建了进程的副本。如果在调用fork()
之前,它有缓冲的数据,则父级和子级都将拥有相同的缓冲数据。下一次他们每个人都做了一些事情来刷新它的缓冲区(例如在终端输出的情况下打印一个换行符),除了该进程产生的任何新输出之外,您还将看到缓冲的输出。所以如果你要在父子节点都使用stdio,那么你应该在fork之前fflush
,以确保没有缓冲数据。
通常,子级仅用于调用exec*
函数。由于它替换了完整的子进程映像(包括任何缓冲区),因此从技术上讲不需要fflush
,如果这真的是您要在孩子身上做的所有事情。但是,如果可能存在缓冲数据,那么您应该小心处理执行失败的方式。特别是,避免使用任何 stdio 函数(write
可以)将错误打印到 stdout 或 stderr,然后调用 _exit
(或 _Exit
)而不是调用 exit
或只是返回(这将刷新任何缓冲输出)。或者通过在分叉前冲洗来完全避免这个问题。
【讨论】:
以上是关于“fork()”之后的 printf 异常的主要内容,如果未能解决你的问题,请参考以下文章
计算机系统篇之异常控制流:利用 fork 和 execve 实现一个简易的 shell 程序