为什么使用glibc时,fread循环需要额外的Ctrl+D来表示EOF?

12

通常,在Linux终端上连接到标准输入的程序中,要指示EOF,如果我只按了Enter,则需要按一次Ctrl+D,否则需要按两次。但是我注意到patch命令是不同的。对于它,如果我只按了Enter,则需要按两次Ctrl+D,否则需要按三次。(使用cat | patch代替这种奇怪行为。此外,如果我在键入任何实际输入之前按Ctrl+D,则没有这种奇怪行为。)深入挖掘patch的源代码,我追踪到它在fread上循环的方式。这是一个能够做同样事情的最小化程序:

#include <stdio.h>

int main(void) {
    char buf[4096];
    size_t charsread;
    while((charsread = fread(buf, 1, sizeof(buf), stdin)) != 0) {
        printf("Read %zu bytes. EOF: %d. Error: %d.\n", charsread, feof(stdin), ferror(stdin));
    }
    printf("Read zero bytes. EOF: %d. Error: %d. Exiting.\n", feof(stdin), ferror(stdin));
    return 0;
}

当按照上面的程序编译并运行时,以下是事件发生的时间线:

  1. 我的程序调用 fread
  2. fread 调用 read 系统调用。
  3. 我输入 "asdf"。
  4. 我按 Enter 键。
  5. read 系统调用返回 5。
  6. fread 再次调用 read 系统调用。
  7. 我按下 Ctrl+D。
  8. read 系统调用返回 0。
  9. fread 返回 5。
  10. 我的程序打印出 Read 5 bytes. EOF: 1. Error: 0.
  11. 我的程序再次调用 fread
  12. fread 调用 read 系统调用。
  13. 我再次按下 Ctrl+D。
  14. read 系统调用返回 0。
  15. fread 返回 0。
  16. 我的程序打印出 Read zero bytes. EOF: 1. Error: 0. Exiting.

为什么使用这种方式读取标准输入的行为与其他程序的方式不同?这是 patch 中的一个错误吗?应该如何编写这种循环以避免这种行为?

更新:这似乎与 libc 有关。我最初在 Ubuntu 16.04 的 glibc 2.23-0ubuntu3 上遇到了这个问题。@Barmar 在评论中指出,在 macOS 上不会发生这种情况。听到这个消息后,我尝试使用同样来自 Ubuntu 16.04 的 musl 1.1.9-1 对相同的程序进行编译,它没有这个问题。在 musl 上,事件序列移除了步骤 12 到 14,这就是为什么它没有这个问题,但除了 readv 替换了 read 这个无关紧要的细节之外,它与 glibc 的行为相同。

现在的问题是:glibc 的行为是否有误,或者 patch 假定其 libc 不会有这种行为是否有误?


1
最少请参考规范化与非规范化终端输入。其中提到,按下“EOF”指示键会使所有缓冲的输入可供read()使用。如果没有缓冲输入,则不会有任何字节可用,读取零字节表示EOF。 - Jonathan Leffler
2
@JonathanLeffler 这就解释了为什么你必须在一行的开头键入 Ctl-D 来表示 EOF。但这并不能解释为什么他必须这样做两次。 - Barmar
1
@Barmar 另一个重要细节:你需要输入一些内容而不是立即按下 Ctrl+D,否则它会正常工作。我也会添加这个。 - Joseph Sible-Reinstate Monica
2
糟糕,我在测试时以为自己在Linux上,但实际上不是。它在MacOS上可以正常工作,但在Linux上我看到了和你一样的情况。 - Barmar
2
这是Linux实现和tty工作方式的产物。第一个CTRL+D将asdf\n发送到您的程序,但CTRL+D实际上并没有关闭stdin。fread()继续执行,而read()系统调用会阻塞,因为stdin实际上并没有关闭。由于read()返回0且其内部缓冲区中没有任何内容,fread()决定放弃下一个CTRL+D。 - nos
显示剩余17条评论
1个回答

6

我已经确认,这是由于 glibc 版本在 2.28 之前(提交 2cc7bad)存在明显的 bug。相关引用来自C 标准

字节输入/输出函数 - 描述在此子句中执行输入/输出的函数:[...], fread

字节输入函数从流中读取字符,就像通过连续调用 fgetc 函数一样。

如果流的文件结束指示器设置了,或者流处于文件结束位置,则流的文件结束指示器被设置,并且 fgetc 函数返回 EOF。否则,fgetc 函数返回指向 stream 的输入流的下一个字符。

以下程序演示了使用 fgetc 时的错误:

#include <stdio.h>

int main(void) {
    while(fgetc(stdin) != EOF) {
        puts("Read and discarded a character from stdin");
    }
    puts("fgetc(stdin) returned EOF");
    if(!feof(stdin)) {
        /* Included only for completeness. Doesn't occur in my testing. */
        puts("Standard violation! After fgetc returned EOF, the end-of-file indicator wasn't set");
        return 1;
    }
    if(fgetc(stdin) != EOF) {
        /* This happens with glibc in my testing. */
        puts("Standard violation! When fgetc was called with the end-of-file indicator set, it didn't return EOF");
        return 1;
    }
    /* This happens with musl in my testing. */
    puts("No standard violation detected");
    return 0;
}

为了演示这个bug:
1. 编译并运行该程序 2. 按下Ctrl+D 3. 按下Enter 确切的bug是,如果文件末尾流指示器被设置,但流不在文件末尾,glibc的fgetc将返回流中的下一个字符,而不是标准要求的EOF。
由于fread是基于fgetc定义的,这就是我最初看到的原因。它以前曾被报告为glibc bug #1190,自2018年2月的提交2cc7bad以来已被修复,在2018年8月的glibc 2.28中发布。

1
不幸的是,这个错误修复导致其他软件出现了退化,例如cups-filters。但我们已经决定暂时保留此修复 - Florian Weimer
1
是的,这是一个非常非常古老而且众所周知的 bug,glibc 继承自 sysv unix 中的一个 bug。现在大多数其他实现都没有这个 bug,因此任何被 glibc 修复破坏的软件也将在大多数非 glibc(例如 BSD)系统上破坏。 - R.. GitHub STOP HELPING ICE
1
相反,像 hexdump 这样的软件由于旧的 GNU C 库行为而出现故障,并且可以使用其他 C 库工作。https://unix.stackexchange.com/q/517064/5132 - JdeBP

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