为什么 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;
}
按原样编译和 运行 上面的程序,这是事件的时间表:
- 我的程序调用
fread
.
fread
调用 read
系统调用。
- 我输入"asdf".
- 我按回车键。
read
系统调用returns 5.
fread
再次调用read
系统调用。
- 我按Ctrl+D。
read
系统调用returns0.
fread
returns 5.
- 我的程序打印
Read 5 bytes. EOF: 1. Error: 0.
- 我的程序再次调用
fread
。
fread
调用 read
系统调用。
- 我又按了Ctrl+D
read
系统调用returns0.
fread
returns 0.
- 我的程序打印
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;
}
演示错误:
- 编译程序并执行
- 按 Ctrl+D
- 按回车键
确切的错误是,如果设置了文件结束流指示器,但流不在文件结束处,glibc 的 fgetc 将 return 流中的下一个字符,而不是比标准要求的 EOF。
由于fread
是根据fgetc
定义的,所以这是我最初看到的原因。它曾于 2018 年 2 月被报道为 glibc bug #1190 and has been fixed since commit 2cc7bad
,并于 2018 年 8 月登陆 glibc 2.28。
通常,要向 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;
}
按原样编译和 运行 上面的程序,这是事件的时间表:
- 我的程序调用
fread
. fread
调用read
系统调用。- 我输入"asdf".
- 我按回车键。
read
系统调用returns 5.fread
再次调用read
系统调用。- 我按Ctrl+D。
read
系统调用returns0.fread
returns 5.- 我的程序打印
Read 5 bytes. EOF: 1. Error: 0.
- 我的程序再次调用
fread
。 fread
调用read
系统调用。- 我又按了Ctrl+D
read
系统调用returns0.fread
returns 0.- 我的程序打印
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 returnsEOF
. Otherwise, thefgetc
function returns the next character from the input stream pointed to bystream
.
(强调 "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;
}
演示错误:
- 编译程序并执行
- 按 Ctrl+D
- 按回车键
确切的错误是,如果设置了文件结束流指示器,但流不在文件结束处,glibc 的 fgetc 将 return 流中的下一个字符,而不是比标准要求的 EOF。
由于fread
是根据fgetc
定义的,所以这是我最初看到的原因。它曾于 2018 年 2 月被报道为 glibc bug #1190 and has been fixed since commit 2cc7bad
,并于 2018 年 8 月登陆 glibc 2.28。