Почему в фуд-петле требуется дополнительный 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;
}

При компиляции и запуске вышеуказанной программы точно как-есть, здесь график событий:

  1. Моя программа вызывает fread.
  2. fread вызывает системный вызов read.
  3. Я печатаю "asdf".
  4. Я нажимаю Enter.
  5. read системный вызов возвращает 5.
  6. fread снова вызывает системный вызов read.
  7. Я нажимаю Ctrl + D.
  8. read системный вызов возвращает 0.
  9. fread возвращает 5.
  10. Моя программа печатает Read 5 bytes. EOF: 1. Error: 0. 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. 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;
}

Чтобы продемонстрировать ошибку:

  1. Скомпилируйте программу и выполните ее
  2. Нажмите Ctrl + D
  3. нажмите Ввод

Точная ошибка заключается в том, что если индикатор потока конца файла установлен, но поток не находится в конце файла, glibc fgetc вернет следующий символ из потока, а не EOF по мере необходимости.

Поскольку fread определяется в терминах fgetc, это является причиной того, что я изначально видел. Ранее он сообщался как ошибка glibС# 1190 и был исправлен с момента совершения 2cc7bad в феврале 2018 года, который высадился в glibc 2.28 в августе 2018 года.