为啥 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;
当完全按原样编译和运行上述程序时,以下是事件时间线:
-
我的程序调用
fread
。
fread
调用read
系统调用。
我输入“asdf”。
我按 Enter。
read
系统调用返回 5。
fread
再次调用read
系统调用。
我按 Ctrl+D。
read
系统调用返回 0。
fread
返回 5。
我的程序打印Read 5 bytes. EOF: 1. Error: 0.
我的程序再次调用fread
。
fread
调用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 信号?的主要内容,如果未能解决你的问题,请参考以下文章