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

lma*_*ofl 57 c fork

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

方案一

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

int main()
{
    printf("hello world...\n");
    fork();
    fork();
    fork();
}
Run Code Online (Sandbox Code Playgroud)

输出 1:

hello world... 
Run Code Online (Sandbox Code Playgroud)

方案二

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

int main()
{
    printf("hello world...");
    fork();
    fork();
    fork();
}
Run Code Online (Sandbox Code Playgroud)

输出 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...
Run Code Online (Sandbox Code Playgroud)

Kus*_*nda 102

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

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

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

它是8 个,因为在每个过程中,fork()您获得的进程数量是之前的两倍fork()(因为它们是无条件的),并且您拥有其中的三个 (2 3 = 8)。

  • 相关:你可以用 `_exit(0)` 结束 `main`,只进行退出系统调用而不刷新缓冲区,然后它会在没有换行符的情况下打印零次。([退出()的系统调用实现](/sf/ask/3283222631/)和[为什么\_exit(0)(由系统调用退出)阻止我接收任何标准输出内容?](https:// stackoverflow.com/q/9755027))。或者你可以通过管道将 Program1 导入 `cat` 或将其重定向到一个文件,然后查看它被打印 8 次。(当标准输出不是 TTY 时,默认情况下它是全缓冲的)。或者在第二个 `fork()` 之前的无换行符情况下添加一个 `fflush(stdout)`... (14认同)

edc*_*c65 19

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

在第一种情况下,您最终有 8 个进程没有可写,因为输出缓冲区已经被清空(由于\n)。

在第二种情况下,您仍然有 8 个进程,每个进程都有一个包含“Hello world...”的缓冲区,缓冲区在进程结束时写入。


Hon*_*dek 12

@Kusalananda 解释了为什么重复输出。如果您好奇为什么输出重复8 次而不是 4 次(基础程序 + 3 个分叉):

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
}
Run Code Online (Sandbox Code Playgroud)

  • @Debian_yadav 我看不懂 OP 的想法,所以我不知道。无论如何,stackexchange 是其他人搜索知识的地方,我认为我的答案可以成为 Kulasandra 好的答案的有用补充。与 edc65 的回答相比,我的回答 * 添加了 * 一些东西(基本但有用),后者只是重复了 Kulasandra 在他之前 2 小时所说的话。 (8认同)
  • @Debian_yadav 只有在您熟悉其含义时才可能显而易见。例如,像刷新 _stdio_ 缓冲区一样。 (3认同)
  • 这是 fork 的基础 (2认同)
  • @Debian_yadav:https://en.wikipedia.org/wiki/False_consensus_effect - 如果每个人都知道一切,我们为什么要提问? (2认同)
  • 这只是对答案的简短评论,而不是实际答案。问题问的是“多次”,而不是为什么正好是 8。 (2认同)
  • 我同意@pipe 的观点,这似乎没有回答所提出的问题,尽管它确实回答了一个不同的问题。 (2认同)

sch*_*ily 5

这里的重要背景是作为默认设置stdout需要由标准行缓冲。

这会导致\n刷新输出。

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

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

根据定义,所有这些进程调用exit()的返回时main()和exit()通话fflush(),然后fclose()在所有活动标准输入输出流。这包括stdout,因此,您会看到八次相同的内容。

fflush()在调用之前调用所有带有挂起输出的流,fork()或者让分叉子调用显式地_exit()只退出进程而不刷新 stdio 流,这是一种很好的做法。

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

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

stderr默认情况下,该标准要求无缓冲,但 Linux 忽略了这一点,stderr并在 stderr 通过管道重定向的情况下将行缓冲并(甚至更糟)完全缓冲。因此,为 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); 
Run Code Online (Sandbox Code Playgroud)

此代码不会在其他平台fflush()上造成损害,因为调用刚刚刷新的流是一个 noop。

  • Linux 是一个内核,stdio 缓冲是一个用户态特性,内核不涉及。有许多可用于 Linux 内核的 libc 实现,在服务器/工作站类型系统中最常见的是 GNU 实现,其中 stdout 是全缓冲的(如果是 tty,则为行缓冲),而 stderr 是无缓冲的。 (5认同)
  • 不,标准输出需要被完全缓冲,除非它是一个交互式设备,在这种情况下它是未指定的,但实际上它是行缓冲的。stderr 需要不被完全缓冲。见 http://pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/V2_chap02.html#tag_15_05 (3认同)
  • @schily 这不是真的。例如,我使用 Alpine Linux 编写此评论,而它使用 musl。 (2认同)