为什么带有fork()的程序有时会多次输出其输出?


50

在程序1中Hello world仅被打印一次,但是当我删除 \n并运行它(程序2)时,输出被打印8次。有人可以给我解释一下\n这里的意义以及它如何影响fork()吗?

程序1

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...\n");
    fork();
    fork();
    fork();
}

输出1:

hello world... 

程序2

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...");
    fork();
    fork();
    fork();
}

输出2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...

10
尝试运行程序1,将其输出到文件(./prog1 > prog1.out)或管道(./prog1 | cat)。准备振作起来。:-)⁠
G-人说'恢复莫妮卡

相关的Q + A涵盖了此问题的另一个变体:C系统(“ bash”)忽略了stdin
Michael Homer

13
这已经获得了一些密切的支持,因此对此发表了评论:明确允许 “ UNIX C API和系统接口”问题。缓冲问题也是shell脚本中的常见问题,并且fork()在某些方面也是unix特定的,因此,对于unix.SE来说,这似乎是很常见的问题。
ilkkachu

@ilkkachu实际上,如果您阅读了该链接,然后单击它所指向的meta问题,则可以很清楚地表明这与主题无关。仅仅因为某些东西是C,而Unix则是C,就不能使它成为主题。
帕特里克

@帕特里克,实际上,我做到了。而且我仍然认为它适合“合理范围内”的条款,但是当然那只是我自己。
ilkkachu

Answers:


93

使用C库的printf()函数输出到标准输出时,通常会缓冲输出。在输出换行符,调用fflush(stdout)或退出程序之前,不会刷新缓冲区(_exit()尽管不能通过调用)。当标准输出流连接到TTY时,默认情况下以这种方式进行行缓冲。

当您在“程序2”中派生该进程时,子进程将继承父进程的每个部分,包括未刷新的输出缓冲区。这有效地将未刷新的缓冲区复制到每个子进程。

当进程终止时,将刷新缓冲区。您总共启动了八个进程(包括原始进程),并且未刷新的缓冲区将在每个单个进程终止时刷新。

之所以是八,是因为每一次fork()您获得的处理数量是之前的两倍fork()(因为它们是无条件的),并且您拥有三个(2 3 = 8)。


14
相关阅读:你可以结束main_exit(0)刚刚做出退出系统调用,而不刷新缓冲区,然后它会不换行打印零次。(exit()的Syscall实现_exit(0)(通过syscall 退出)如何阻止我接收任何stdout内容?)。或者,您可以通过管道将Program1插入cat或重定向到文件中,并看到它被打印了8次。(stdout不是TTY时,默认情况下为全缓冲)。或fflush(stdout)在第二行之前在无换行的情况下添加一个fork()...
Peter Cordes

17

它不会以任何方式影响前叉。

在第一种情况下,您最终要完成8个进程而无需编写任何内容,因为输出缓冲区已被清空(由于\n)。

在第二种情况下,您仍然有8个进程,每个进程都有一个包含“ Hello world ...”的缓冲区,并且该缓冲区是在进程末尾写入的。


12

@Kusalananda解释了为什么重复输出的原因。如果您好奇为什么输出要重复8次而不是4次(基本程序+ 3个fork):

int main()
{
    printf("hello world...");
    fork(); // here it creates a copy of itself --> 2 instances
    fork(); // each of the 2 instances creates another copy of itself --> 4 instances
    fork(); // each of the 4 instances creates another copy of itself --> 8 instances
}

2
这是叉子的基本知识
Prvt_Yadav

3
@Debian_yadav仅当您熟悉其含义时才可能很明显。例如,像冲洗stdio缓冲区一样。
roaima

2
@Debian_yadav:en.wikipedia.org/wiki/False_consensus_effect-我们为什么要问每个人都知道什么的问题?
Honza Zidek '18

8
@Debian_yadav我无法阅读OP的想法,所以我不知道。无论如何,stackexchange是其他人也在这里寻找知识的地方,我认为我的答案可以对Kulasandra的好答案有所帮助。与edc65相比,我的回答增加了一些内容(基本但有用),而edc65只是重复了Kulasandra在他面前两个小时说的话。
Honza Zidek

2
这只是对答案的简短评论,而不是实际答案。现在的问题询问有关“多次”不为什么这正是8

3

此处的重要背景是标准stdout要求将行缓冲设置为默认设置。

这将导致\n刷新输出。

由于第二个示例不包含换行符,因此不会刷新输出,并且在fork()复制整个过程时,它还会复制stdout缓冲区的状态。

现在,fork()您的示例中的这些调用总共创建了8个进程-所有这些都带有stdout缓冲区状态的副本。

根据定义,所有这些进程exit()在从所有活动的stdio流中返回时都会调用,然后main()exit()调用。这包括,因此,您看到八次相同的内容。fflush()fclose()stdout

优良作法是在调用之前fflush()对所有具有挂起输出的流进行调用,fork()或者显式地让派生的子调用_exit()仅退出进程而不刷新stdio流。

请注意,调用exec()不会刷新stdio缓冲区,因此如果您(在调用之后fork())调用exec()和(如果失败)调用,则可以不在意stdio缓冲区_exit()

顺便说一句:要了解错误的缓冲可能会导致,这是Linux中的一个先前错误,最近已修复:

该标准stderr默认情况下需要取消缓冲,但是stderr如果通过管道重定向stderr ,Linux会忽略此设置,并进行行缓冲和(甚至更糟)完全缓冲。因此,为UNIX编写的程序在Linux上输出没有换行符的内容太晚了。

请参阅下面的评论,现在似乎已修复。

这是为了解决此Linux问题而要做的事情:

    /* 
     * Linux comes with a broken libc that makes "stderr" buffered even 
     * though POSIX requires "stderr" to be never "fully buffered". 
     * As a result, we would get garbled output once our fork()d child 
     * calls exit(). We work around the Linux bug by calling fflush() 
     * before fork()ing. 
     */ 
    fflush(stderr); 

这段代码不会对其他平台造成伤害,因为调用fflush()刚刚刷新的流是noop。


2
不,stdout需要完全缓冲,除非它是一个交互式设备(在这种情况下未指定),但实际上它是行缓冲的。stderr必须不被完全缓冲。见pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/...
斯特凡Chazelas

setbuf()在Debian上的的手册页(man7.org上的手册页看起来与此类似)指出:“默认情况下,标准错误流stderr始终未缓冲。” 不管输出是发送到文件,管道还是终端,一个简单的测试似乎都可以这样做。您对C库的其他版本有任何参考吗?
ilkkachu

4
Linux是内核,stdio缓冲是一个userland功能,那里不涉及内核。有许多可用于Linux内核的libc实现,在服务器/工作站类型的系统中最常见的是GNU实现,其中stdout是全缓冲的(如果是tty,则是行缓冲),stderr是无缓冲的。
斯特凡Chazelas

1
@schily,就是我运行的测试:paste.dy.fi/xk4。我也用过时的系统获得了相同的结果。
ilkkachu

1
@schily这不是真的。例如,我正在使用Alpine Linux(使用musl)编写此注释。
NieDzejkob
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.