为什么 fread 循环需要额外的 Ctrl+D 来用 glibc 发出 EOF 信号?

Why does an fread loop require an extra Ctrl+D to signal EOF with glibc?

通常,要向 Linux 终端上附加到标准输入的程序指示 EOF,如果我只是按 Enter,则需要按 Ctrl+D 一次,否则需要按两次。不过,我注意到 patch 命令是不同的。有了它,如果我只是按 Enter,我需要按 Ctrl+D 两次,否则需要按三次。 (而不是做 cat | patch 没有这种奇怪。此外,如果我在键入任何实际输入之前按 Ctrl+D,它就没有这种奇怪。)深入研究 patch 的来源代码,我追溯到 the way it loops on 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. 我按回车键。
  5. read系统调用returns 5.
  6. fread再次调用read系统调用。
  7. 我按Ctrl+D。
  8. read系统调用returns0.
  9. fread returns 5.
  10. 我的程序打印 Read 5 bytes. EOF: 1. Error: 0.
  11. 我的程序再次调用 fread
  12. fread 调用 read 系统调用。
  13. 我又按了Ctrl+D
  14. read系统调用returns0.
  15. fread returns 0.
  16. 我的程序打印 Read zero bytes. EOF: 1. Error: 0. Exiting.

为什么这种读取 stdin 的方式有这种行为,不像其他所有程序似乎读取它的方式?这是 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 的行为是错误的,还是假设其 libc 没有这种行为的补丁错误?

我已经设法确认这是由于 2.28 之前的 glibc 版本中存在一个明确的错误(提交 2cc7bad). Relevant quotes from the C standard:

The byte input/output functions — those functions described in this subclause that perform input/output: [...], fread

The byte input functions read characters from the stream as if by successive calls to the fgetc function.

If the end-of-file indicator for the stream is set, or if the stream is at end-of-file, the end-of-file indicator for the stream is set and the fgetc function returns EOF. Otherwise, the fgetc function returns the next character from the input stream pointed to by stream.

(强调 "or" 我的)

以下程序演示了 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;
}

演示错误:

  1. 编译程序并执行
  2. 按 Ctrl+D
  3. 按回车键

确切的错误是,如果设置了文件结束流指示器,但流不在文件结束处,glibc 的 fgetc 将 return 流中的下一个字符,而不是比标准要求的 EOF。

由于fread是根据fgetc定义的,所以这是我最初看到的原因。它曾于 2018 年 2 月被报道为 glibc bug #1190 and has been fixed since commit 2cc7bad,并于 2018 年 8 月登陆 glibc 2.28。