【问题标题】:How is printf getting flushed before scanf is executed?在执行 scanf 之前如何刷新 printf?
【发布时间】:2021-06-23 04:19:29
【问题描述】:

我最近阅读了很多关于标准输出缓冲的内容。我知道printf 已缓冲,但到目前为止,我认为它的缓冲区仅在将新行读入缓冲区或调用fflush(stdout) 或调用printf的进程正常退出时才被刷新。

我编写了这个程序,它在scanf 之前没有换行就调用了 printf。当我用谷歌搜索时,我发现很多人说他们不明白为什么 scanf 在 printf 之前执行。由于我现在了解标准输出缓冲的概念,这对我来说很有意义。

但是,就我而言,缓冲区在我运行 scanf 之前被刷新。这样做确实有意义,因为用户可能希望 printf 在任何 scanf 之前执行,但它是如何发生的?究竟什么是刷新标准输出?是scanf吗?

int main(void) {
    char things;
    printf("Hello ");
    scanf("%c", &things);
}

(我正在运行 Arch Linux)

编辑:由于一些 cmets 说我的系统的标准输出是无缓冲的,我只想补充一点,没有在我的程序上运行 scanf,我的程序具有我上面提到的行为,它肯定是缓冲的。

【问题讨论】:

  • Doing printf("Hello "); sleep(1); scanf("%c", &things); 向我展示了确实,使用交互式程序 scanf 刷新标准输出缓冲区。例如,您可以执行 ./a.out | cat 来查看它是否已缓冲。 ||我认为它在gnu.org/software/libc/manual/html_node/Flushing-Buffers.html 被记录为Whenever an input operation on any stream actually reads data from its file 并假设 stdout 和 stdin 都引用同一个“文件”。
  • 我刚刚在 docker 的 alpine 上测试了 musl,Hello 被打印出来之后 scanf。请注意,行为可能会发生变化,并且刷新缓冲区的是 glibc。

标签: c linux printf scanf buffering


【解决方案1】:

这是一个实施质量问题。

C 标准仅要求 stdinstdout 默认情况下仅在附加到常规文件时才完全缓冲。但它明确鼓励交互式设备的特定行为:

5.1.2.3 程序执行
[...]
对一致性实现的最低要求是:
[...]
交互设备的输入和输出动态应按照 7.21.3 中的规定进行。这些要求的目的是尽快出现无缓冲或行缓冲的输出,以确保在程序等待输入之前实际出现提示消息。

在许多 Posix 系统上,stdinstdout 在附加到字符设备时是行缓冲的,然后当来自 stdin 的读取尝试需要从底层系统句柄读取时,stdout 会自动刷新。即使没有尾随换行符,这也允许提示出现在终端上。

在 linux 上,此行为在 stdio(3) linux manual page: 中指定:

引用终端设备的输出流总是行 默认缓冲;写入此类流的未决输出 每当引用终端的输入流时自动 设备被读取。在计算量很大的情况下 在输出终端上打印一行的一部分后完成,它是 在关闭之前需要 fflush(3) 标准输出 计算,以便出现输出。

然而 GNU libc 有一个微妙的不同行为:只有 stdout 以这种方式刷新,如 glibc/libio/fileops.c 中的编码(由 Ulrich Drepper 在 2001-08-04 23:59 修改: 30):

  /* Flush all line buffered files before reading. */
  /* FIXME This can/should be moved to genops ?? */
  if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
#if 0
      INTUSE(_IO_flush_all_linebuffered) ();
#else
      /* We used to flush all line-buffered stream.  This really isn't
         required by any standard.  My recollection is that
         traditional Unix systems did this for stdout.  stderr better
         not be line buffered.  So we do just that here
         explicitly.  --drepper */
      _IO_acquire_lock (_IO_stdout);

      if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
          == (_IO_LINKED | _IO_LINE_BUF))
        _IO_OVERFLOW (_IO_stdout, EOF);

      _IO_release_lock (_IO_stdout);
#endif
    }

【讨论】:

    【解决方案2】:

    我认为只有在将新行读入缓冲区或调用fflush(stdout) 或调用printf 的进程正常退出时才会刷新其缓冲区。

    这不是 C 标准的意图。当流是行缓冲时,只要程序在任何非缓冲流或从“主机环境”(例如用户键入输入的终端窗口)获取输入的行缓冲流上请求输入,也应刷新输出,如 C 2018 7.21.3 3 所述:

    …当流被行缓冲时,当遇到换行符时,字符将作为块传输到主机环境或从主机环境传输。此外,当缓冲区被填满时、在非缓冲流上请求输入时,或者在需要从宿主环境t…

    这仅表达了一个意图,并且该标准进一步表示对这些特性的支持是实现定义的,因此这在技术上是一个实现质量问题。但是,就诊断信息的质量而言,这不是质量问题。关于支持是实现定义的,以及关于标准输出是否是行缓冲的保留,主要是对各种旧计算机系统中的可行性或可能性的让步。在大多数现代 C 实现中,C 实现不应使用 C 标准的此许可作为不实现这些功能的借口。

    这是一个示例,说明如何从不相关的流中读取输入可以刷新标准输出。当我使用 Xcode 11.3.1 在 macOS 10.14.6 上执行该程序时,标准输出中的“Hello”在读取到 /dev/null 的不相关流时被刷新,但当输出仅用 printf 写入而没有读取时则不会刷新:

    #include <stdio.h>
    #include <unistd.h>
    
    
    int main(void)
    {
        printf("Hello");
        FILE *dummy = fopen("/dev/null", "r");
        setvbuf(dummy, NULL, _IONBF, 0); // Make dummy unbuffered.
        fgetc(dummy);       // "Hello" appears on terminal.
        printf(" world.");  // " world." does not appear on terminal.
        sleep(5);
        printf("\n");       // " world." appears on terminal.
    }
    

    如果我们删除setvbuffgetc,“Hello”不会立即出现在终端上,这表明它是对未缓冲流的读取,导致标准输出被刷新。

    我知道printf 被缓冲了……

    这取决于具体情况。它实际上是缓冲或不缓冲的流,C 2018 7.21.3 7 说:

    …标准输入和标准输出流被完全缓冲当且仅当流可以被确定不引用交互式设备。

    因此,如果您将程序的标准输出重定向到文件,它并不指向交互式设备,因此如果程序能够检测到这一点,则必须完全缓冲。当程序的输出进入交互式终端窗口时,它不能被完全缓冲。 (替代方案是行缓冲和非缓冲,典型的 C 实现使用行缓冲。)'

    因此,如果标准输入和标准输出都连接到交互式设备(并且 C 实现无法检测到其他情况),那么 printf 输出应该出现在 scanf 执行之前,因为标准输出没有缓冲(所以printf 输出立即出现)或者因为标准输出是行缓冲的,并且在调用 scanf 时被刷新。

    【讨论】:

    • @AndreasWenzel:谢谢,已更正为“(否则 C 实现无法检测到)”。
    猜你喜欢
    • 1970-01-01
    • 2020-08-13
    • 1970-01-01
    • 1970-01-01
    • 2019-04-15
    相关资源
    最近更新 更多