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

Posted

技术标签:

【中文标题】为啥 fread 循环需要额外的 Ctrl+D 来用 glibc 发出 EOF 信号?【英文标题】:Why does an fread loop require an extra Ctrl+D to signal EOF with glibc?为什么 fread 循环需要额外的 Ctrl+D 来用 glibc 发出 EOF 信号? 【发布时间】:2019-03-11 11:01:39 【问题描述】:

通常,要向附加到 Linux 终端上的标准输入的程序指示 EOF,如果我只是按 Enter,则需要按一次 Ctrl+D,否则需要按两次。不过,我注意到patch 命令是不同的。有了它,如果我只是按 Enter,我需要按 Ctrl+D 两次,否则需要按 3 次。 (而不是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;

当完全按原样编译和运行上述程序时,以下是事件时间线:

    我的程序调用freadfread 调用read 系统调用。 我输入“asdf”。 我按 Enter。 read 系统调用返回 5。 fread 再次调用read 系统调用。 我按 Ctrl+D。 read 系统调用返回 0。 fread 返回 5。 我的程序打印Read 5 bytes. EOF: 1. Error: 0. 我的程序再次调用freadfread 调用read 系统调用。 我再次按 Ctrl+D。 read 系统调用返回 0。 fread 返回 0。 我的程序打印Read zero bytes. EOF: 1. Error: 0. Exiting.

为什么这种读取标准输入的方式会有这种行为,不像其他程序似乎读取它的方式?这是patch 中的错误吗?这种循环应该怎么写才能避免这种行为呢?

更新:这似乎与 libc 有关。我最初在 Ubuntu 16.04 的 glibc 2.23-0ubuntu3 上体验过它。 @Barmar 在 cmets 中指出它不会在 macOS 上发生。听到这个后,我尝试针对同样来自 Ubuntu 16.04 的 musl 1.1.9-1 编译相同的程序,但没有出现这个问题。在 musl 上,事件序列删除了第 12 步到第 14 步,这就是为什么它没有问题,但在其他方面是相同的(除了 readv 代替 read 的不相关细节)。

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

【问题讨论】:

至少请参阅Canonical vs non-canonical terminal input。这提到点击“EOF”指示键可使所有缓冲输入对read() 可用。如果没有缓冲输入,它会提供零字节,读取的零字节表示 EOF。 @JonathanLeffler 这解释了为什么你必须在一行的开头输入 Ctl-D 来表示 EOF。但这并不能解释为什么他必须这样做两次。 @Barmar 另一个重要的细节:您需要输入一些输入而不是立即按 Ctrl+D,否则它可以正常工作。我也会补充的。 糟糕,当我以为我在 Linux 上进行测试时,我不在 Linux 上。它在 MacOS 上可以正常工作,但我在 Linux 上看到的和你一样。 它是 linux 实现的工件,以及 tty 的工作原理。第一个 CTRL+D 将 asdf\n 发送到您的程序,但 CTRL+D 实际上并没有关闭标准输入。 fread() 继续并且 read() 系统调用阻塞,因为标准输入并没有真正关闭。 fread() 决定放弃下一个 CTRL+D,因为 read() 返回 0 并且其内部缓冲区中没有任何内容。 【参考方案1】:

我已经设法确认这是由于 2.28 之前的 glibc 版本中存在明确的错误(提交 2cc7bad)。相关引用来自the C standard:

字节输入/输出函数——本小节中描述的那些执行 输入/输出:[...],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;

为了演示错误:

    编译程序并执行 按 Ctrl+D 按 Enter 键

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

由于fread是根据fgetc定义的,这就是我最初看到的原因。它之前被报告为 glibc bug #1190,并且自 2018 年 2 月提交 2cc7bad 以来一直被修复,该提交于 2018 年 8 月登陆 glibc 2.28。

【讨论】:

不幸的是,此错误修复会导致其他软件出现回归问题,例如 cups-filters。但我们决定keep the fix, at least for now。 是的,这是从 sysv unix 中的一个 bug 继承而来的一个非常古老、众所周知的 glibc 错误。现在大多数其他实现都没有这个错误,所以任何被 glibc 中的修复程序破坏的软件也将在大多数非 glibc(例如 BSD)系统上被破坏。 相反,hexdump 等软件被旧的 GNU C 库行为所破坏,并且可以与其他 C 库一起使用。 unix.stackexchange.com/q/517064/5132

以上是关于为啥 fread 循环需要额外的 Ctrl+D 来用 glibc 发出 EOF 信号?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 putw 在 C 中扩展文件后使用 fread?

R fread and strip 白色

为啥没有额外的错误或状态通道?

为啥 fread 会弄乱我的字节顺序?

为啥C语言我用fread读入数据会乱码,函数如下

电脑的(BACKSPACE)删除键为啥失效了,怎么设置啊?