Почему в фуд-петле требуется дополнительный Ctrl + D для сигнала EOF с glibc?
Обычно, чтобы указать EOF на программу, подключенную к стандартным входам на терминале Linux, мне нужно нажать Ctrl + D один раз, если я просто нажал Enter, или два раза в противном случае. Однако я заметил, что команда patch
отличается. При этом мне нужно дважды нажать Ctrl + D, если я просто нажал Enter, или три раза в противном случае. (Это 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;
}
При компиляции и запуске вышеуказанной программы точно как-есть, здесь график событий:
- Моя программа вызывает
fread
. -
fread
вызывает системный вызов read
. - Я печатаю "asdf".
- Я нажимаю Enter.
-
read
системный вызов возвращает 5. -
fread
снова вызывает системный вызов read
. - Я нажимаю Ctrl + D.
-
read
системный вызов возвращает 0. -
fread
возвращает 5. - Моя программа печатает
Read 5 bytes. EOF: 1. Error: 0.
Read 5 bytes. EOF: 1. Error: 0.
- Моя программа снова вызывает
fread
. -
fread
вызывает системный вызов read
. - Я снова нажимаю Ctrl + D.
-
read
системный вызов возвращает 0. -
fread
возвращает 0. - Моя программа печатает
Read zero bytes. EOF: 1. Error: 0. Exiting.
Read zero bytes. EOF: 1. Error: 0. Exiting.
Почему это означает, что чтение stdin имеет такое поведение, в отличие от того, как каждая другая программа читает его? Это ошибка в patch
? Как следует писать такой тип цикла, чтобы избежать такого поведения?
UPDATE: похоже, это связано с libc. Первоначально я испытал это на glibc 2.23-0ubuntu3 от Ubuntu 16.04. @Barmar отметил в комментариях, что это не происходит на macOS. Услышав это, я попытался составить ту же программу против musl 1.1.9-1, также из Ubuntu 16.04, и у нее не было этой проблемы. На муслисе последовательность событий имеет шаги с 12 по 14, и поэтому она не имеет проблемы, но в остальном readv
та же (за исключением нерелевантных деталей readv
вместо read
).
Теперь возникает вопрос: является ли glibc неправильным в его поведении или неверно исправляется, если предположить, что его libc не будет иметь такое поведение?
Ответы
Ответ 1
Мне удалось подтвердить, что это связано с однозначной ошибкой в версиях glibc до 2.28 (commit 2cc7bad
). Соответствующие цитаты из стандарта 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;
}
Чтобы продемонстрировать ошибку:
- Скомпилируйте программу и выполните ее
- Нажмите Ctrl + D
- нажмите Ввод
Точная ошибка заключается в том, что если индикатор потока конца файла установлен, но поток не находится в конце файла, glibc fgetc вернет следующий символ из потока, а не EOF по мере необходимости.
Поскольку fread
определяется в терминах fgetc
, это является причиной того, что я изначально видел. Ранее он сообщался как ошибка glibС# 1190 и был исправлен с момента совершения 2cc7bad
в феврале 2018 года, который высадился в glibc 2.28 в августе 2018 года.