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

Jos*_*ica 12 c glibc libc fread eof

通常,为了向连接到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;
}
Run Code Online (Sandbox Code Playgroud)

在完全按原样编译和运行上述程序时,这是事件的时间表:

  1. 我的程序调用fread.
  2. fread调用read系统调用.
  3. 我输入"asdf".
  4. 我按Enter键.
  5. read系统调用返回5.
  6. freadread再次呼叫系统呼叫.
  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.

为什么这种读取stdin的方法有这种行为,不像其他程序似乎读取它的方式?这是一个错误patch吗?如何编写这种循环以避免这种行为?

更新:这似乎与libc有关.我最初在Ubuntu 16.04上的glibc 2.23-0ubuntu3上体验过它.@Barmar在评论中指出,它不会发生在macOS上.听到这个之后,我尝试编译同样的程序对抗musl 1.1.9-1,也来自Ubuntu 16.04,它没有这个问题.在MUSL,事件的顺序有步骤12至14取出,这就是为什么它没有问题,但在其他方面一样(除了的无关紧要的细节readv代替read).

现在,问题变成:glibc的行为是错误的,还是假设它的libc不会有这种行为的错误?

Jos*_*ica 6

我已经设法确认这是由于2.28(提交2cc7bad)之前的glibc版本中的明确错误.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;
}
Run Code Online (Sandbox Code Playgroud)

为了证明这个bug:

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

确切的错误是,如果设置了文件结束流指示符,但流不在文件末尾,则glibc的fgetc将返回流中的下一个字符,而不是标准要求的EOF.

由于fread是根据定义fgetc,这是我最初看到的原因.它之前被报道为glibc bug#1190并且自2cc7bad2018年2月提交以来已经修复,2018年8月降至glibc 2.28.