在C语言中,自动的标准输出缓冲刷新规则是什么?

18

我只是好奇自动刷新stdout缓冲区需要满足哪些条件。

首先,我感到困惑的是这个伪代码没有在每次迭代时打印输出:

while (1) {
    printf("Any text");
    sleep(1);
}

但如果我添加换行符,它就会生效。

经过几次实验,我发现在我的机器上,stdout缓冲区被刷新的情况有:

  1. 当我向stdout输出1025个或更多字符时;
  2. 当我读取stdin时;
  3. 当我向stdout输出换行符时;

第一个条件是完全明确的——当缓冲区满时应该将其刷新。第二个条件也是合理的。但是为什么换行符会导致刷新呢?这其中还有哪些隐含的条件呢?


我最近被 @chux 告知,第三个条件不在 C 标准中,而是实现定义的,前两个条件也是如此。 - Weather Vane
你可以看一下这个链接:https://dev59.com/g2Yr5IYBdhLWcg3wIG1u - owacoder
2
@WeatherVane:我认为你误解了。对于行缓冲输出流,这种行为是C语言所必需的,但stdout不需要是行缓冲,除非它是一个交互式设备。 - R.. GitHub STOP HELPING ICE
@R..,chux 实际上的评论是“'\n' 是否指定刷新输出?fflush(stdout); 更具可移植性和确定性...”,所以我不是很了解情况,而是受到了质疑。 - Weather Vane
@R.. 不同意“该行为是C语言所要求的”的说法。根据 §7.21.3,对于这些特性的支持是由实现定义的。更多信息请参见此答案 - chux - Reinstate Monica
5个回答

19

自动清空标准输出缓冲区的规则是实现定义(ID)。 当流是不带缓冲的完全缓冲的行缓冲的时,就会出现ID。

当流是不带缓冲的时,字符旨在尽快从源或目的地显示出来。否则,字符可能会累积并作为块传输到或从主机环境中。

当流是完全缓冲的时,字符旨在在填充缓冲区时作为块传输到或从主机环境中。

当流是行缓冲时,字符旨在遇到新行字符时作为块传输到或从主机环境中。此外,在填充缓冲区时,请求非缓冲流上的输入或需要从主机环境传输字符的行缓冲流上的输入时,字符旨在作为块传输到主机环境中。

支持这些特性是实现定义的,…C11dr §7.21.3 3


我只是好奇哪些条件必须满足才能自动清空stdout缓冲区。

如果代码想确保输出被清空,使用fflush()。 自动刷新流的其他条件是实现定义的。


1
不错的警告:“对这些特征的支持是实现定义的” - Andreas

6
请查看setbuf(3)的man页面。默认情况下,stdout设置为行缓冲模式。 printf()及其变体与缓冲输出一起使用,并委托给write()。因此,该缓冲由printf的C库实现控制,缓冲区和缓冲区设置位于FILE结构中。
还值得注意unix手册第3部分和第2部分之间的区别。第2节由直接与操作系统对话并执行从纯用户程序中无法执行的操作的函数调用组成。第3节由用户可以自己重现的函数调用组成,这些函数调用通常委托给第2节调用。第2节函数包含允许C程序与外部世界进行交互并执行I / O所需的低级“魔法”。第3节函数可以提供更方便的接口以使用第2节函数。 printf,scanf,getchar,fputs和其他文件*函数都是委托给read()和write()的第3节函数,后者是第2节函数。read()和write()不会缓冲。printf()与FILE结构中的缓冲区交互,并偶尔决定通过write()发送该缓冲区的内容。

1
谢谢。这真的很有用。我不知道有这些不同类型的缓冲区。而且这个手册解释了一切。 - Andrii Zymohliad
我写了一个简短的答案,然后编辑了更详细的内容,以防你错过了它。 - NovaDenizen
哦,是的,在你的评论之后我才看到它 :) 但无论如何,那个链接对我来说是关于这个问题最有用的信息。我知道这些手册章节之间的区别,但也许对其他人有用。谢谢。 - Andrii Zymohliad

5
  • 每当输出一个换行符时,具有行缓冲的输出流都应该被刷新。

  • 在任何具有行缓冲输入流的读取尝试时,实现可以(但不必)刷新所有行缓冲输出流。

  • 除非可以确定它们未与“交互设备”关联,否则实现不允许默认使用完全缓冲流。因此,当stdin/stdout是终端时,它们不能是完全缓冲,只能是行缓冲(或unbuffered)。

如果只需要在输出到终端时进行刷新,则假定写入换行符会导致刷新即可。否则,您应该在需要刷新的地方显式调用fflush


谢谢。这解释了很多问题。 - Andrii Zymohliad
没有找到“默认情况下实现不允许使流完全缓冲”的要求。有引用吗? - chux - Reinstate Monica
2
@chux: 7.21.3 文件 ¶7:“仅当可以确定流不是交互设备时,标准输入和标准输出流才是完全缓冲的。”以及7.21.5.3 fopen函数 ¶8:“打开时,仅当可以确定流不是交互设备时,流才是完全缓冲的。” - R.. GitHub STOP HELPING ICE
关于第二点,APUE 也有类似的说法:“每当通过标准 I/O 库从未缓冲流或需要从内核请求数据的行缓冲流中请求输入时,所有行缓冲输出流都会被刷新”(第145-146页)。这个想法是,如果你要处理下一行,那么应该先清除生成当前行的任何输出吗? - Kvass
@Kvass:这个想法是有些实现这样做是为了适应打印提示字符串而不带最后一个换行符的习惯,然后执行输入操作,而程序没有显式地刷新输出。然而,这种行为是有害的,应该被视为弃用,因为它必然会在多线程环境中创建死锁,并且(至少没有复杂的簿记)具有O(n)时间成本,其中n是打开流的数量。 - R.. GitHub STOP HELPING ICE

3

有许多情况下,流上的缓冲输出会自动刷新:

  1. 当您尝试输出并且输出缓冲区已满时。
  2. 当流关闭时。
  3. 当程序通过调用exit终止时。
  4. 如果流是行缓冲,则写入换行符时。
  5. 每当任何流上的输入操作实际从其文件中读取数据时。

stdout默认为行缓冲。

如果您想在其他时间刷新缓冲输出,可以调用fflush。


2
如果stdout连接到终端,则仅为行缓冲。 如果程序使用重定向到管道或文件的stdout运行,则在正常的Unix系统上将是完全缓冲的。 因此,这取决于您所说的“默认值”。 (显然,一些IDE使用其他东西而不是伪终端来运行正在调试的程序,从而破坏了假定行缓冲stdout的交互式程序:在Eclipse中执行printf之前执行Scanf / printf输出总是在scanf之前,谁能给出解决方案? - Peter Cordes

3

在线C2011标准

7.21.3 文件
...
3 当流是无缓冲的时候,字符应该尽快地从源或目的地出现。否则,字符可能会被累积并作为一个块传输到或从主机环境中。当流是完全缓冲的时候,字符应该在缓冲区填满时作为一个块传输到或从主机环境中。当流是行缓冲的时候,字符应该在遇到换行符时作为一个块传输到或从主机环境中。此外,当需要从主机环境传输字符时,字符应该在缓冲区填满、在无缓冲流上请求输入或在需要从主机环境传输字符的行缓冲流上请求输入时作为一个块传输到主机环境中。这些特性的支持是实现定义的,并且可以通过 setbuf 和 setvbuf 函数进行影响。
...
7 在程序启动时,三个文本流被预定义并不需要显式打开 — 标准输入(用于读取常规输入)、标准输出(用于写入常规输出)和标准错误(用于写入诊断输出)。标准错误流最初没有完全缓冲;当且仅当可以确定流不引用交互设备时,标准输入和标准输出流才是完全缓冲的。

因此,行缓冲流将在换行符上刷新。在我有经验的大多数系统中,在交互会话中,stdout 是行缓冲的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接