Почему stdout требует явной очистки при перенаправлении в файл?

Поведение printf() кажется, зависит от местоположения stdout,

  1. Если stdout отправляется на консоль, затем printf() буферизуется строкой и сбрасывается после печати новой строки.
  2. Если stdout перенаправлен в файл, буфер не очищается, если fflush() называется.
  3. Более того, если printf() используется раньше stdout перенаправляется в файл, последующие записи (в файл) буферизуются строкой и сбрасываются после новой строки.

Когда stdout буферизованная строка и когда fflush() нужно позвонить?

Минимальный пример каждого:

void RedirectStdout2File(const char* log_path) {
    int fd = open(log_path, O_RDWR|O_APPEND|O_CREAT,S_IRWXU|S_IRWXG|S_IRWXO);
    dup2(fd,STDOUT_FILENO);
    if (fd != STDOUT_FILENO) close(fd);
}

int main_1(int argc, char* argv[]) {
    /* Case 1: stdout is line-buffered when run from console */
    printf("No redirect; printed immediately\n");
    sleep(10);
}

int main_2a(int argc, char* argv[]) {
    /* Case 2a: stdout is not line-buffered when redirected to file */
    RedirectStdout2File(argv[0]);
    printf("Will not go to file!\n");
    RedirectStdout2File("/dev/null");
}
int main_2b(int argc, char* argv[]) {
    /* Case 2b: flushing stdout does send output to file */
    RedirectStdout2File(argv[0]);
    printf("Will go to file if flushed\n");
    fflush(stdout);
    RedirectStdout2File("/dev/null");
}

int main_3(int argc, char* argv[]) {
    /* Case 3: printf before redirect; printf is line-buffered after */
    printf("Before redirect\n");
    RedirectStdout2File(argv[0]);
    printf("Does go to file!\n");
    RedirectStdout2File("/dev/null");
}

3 ответа

Решение

Промывка для stdout определяется его буферным поведением. Буферизация может быть настроена на три режима: _IOFBF (полная буферизация: ждет до fflush() если возможно), _IOLBF (буферизация строки: новая строка запускает автоматическую очистку), и _IONBF (прямая запись всегда используется). "Поддержка этих характеристик определяется реализацией и может зависеть от setbuf() а также setvbuf() функции. "[C99: 7.19.3.3]

"При запуске программы три текстовых потока предопределены и не требуют явного открытия - стандартный ввод (для чтения обычного ввода), стандартный вывод (для записи обычного вывода) и стандартная ошибка (для записи диагностического вывода). стандартный поток ошибок не полностью буферизован; стандартные входные и стандартные выходные потоки полностью буферизуются, если и только если можно определить, что поток не относится к интерактивному устройству ". [С99:7.19.3.7]

Объяснение наблюдаемого поведения

Итак, что происходит, так это то, что реализация делает что-то для платформы, чтобы решить, стоит ли stdout будет буферизироваться строкой. В большинстве реализаций libc этот тест выполняется при первом использовании потока.

  1. Поведение № 1 легко объяснимо: когда поток предназначен для интерактивного устройства, он буферизуется в строке, и printf() сбрасывается автоматически.
  2. Также ожидается случай № 2: когда мы перенаправляем в файл, поток полностью буферизуется и не будет очищен, кроме как с помощью fflush(), если вы не напишите gobloads данных к нему.
  3. Наконец, мы понимаем случай №3 также для реализаций, которые выполняют проверку базового fd только один раз. Потому что мы принудительно инициализировали буфер stdout в первом printf(), stdout получил линейно-буферизованный режим. Когда мы меняем fd для перехода в файл, он все еще буферизуется, поэтому данные автоматически сбрасываются.

Некоторые актуальные реализации

Каждая библиотека libc обладает широтой интерпретации этих требований, поскольку C99 не указывает, что такое "интерактивное устройство", и запись stdio в POSIX не расширяет ее (за исключением того, что stderr должен быть открыт для чтения).

  1. Glibc. Смотрите filedoalloc.c: L111. Здесь мы используем stat() проверить, является ли fd tty, и соответственно установить режим буферизации. (Это вызывается из fileops.c.) stdout изначально имеет нулевой буфер и распределяется при первом использовании потока на основе характеристик fd 1.

  2. BSD libc. Очень похожий, но гораздо более понятный код! Смотрите эту строку в makebuf.c

Вы неправильно комбинируете буферизованные и небуферизованные функции ввода-вывода. Такое сочетание должно выполняться очень осторожно, особенно когда код должен быть переносимым. (и это плохо писать непереносимый код...)
Конечно, лучше избегать комбинирования буферизованного и небуферизованного ввода-вывода в одном файловом дескрипторе.

Буферизованный ввод-вывод: fprintf(), fopen(), fclose(), freopen()...

Небуферизованный ввод-вывод: write(), open(), close(), dup()...

Когда вы используете dup2() перенаправить стандартный вывод. Функция не знает о буфере, который был заполнен fprintf(), Так когда dup2() закрывает старый дескриптор 1, он не очищает буфер, и содержимое может быть сброшено на другой вывод. В вашем случае 2a он был отправлен /dev/null,

Решение

В вашем случае лучше всего использовать freopen() вместо dup2(), Это решает все ваши проблемы:

  1. Сбрасывает буферы оригинала FILE поток. (случай 2а)
  2. Он устанавливает режим буферизации в соответствии с вновь открытым файлом. (случай 3)

Вот правильная реализация вашей функции:

void RedirectStdout2File(const char* log_path) {
    if(freopen(log_path, "a+", stdout) == NULL) err(EXIT_FAILURE, NULL);
}

К сожалению, с буферизованным вводом-выводом вы не можете напрямую устанавливать разрешения для вновь созданного файла. Вы должны использовать другие вызовы, чтобы изменить разрешения, или вы можете использовать непереносимые расширения glibc. Увидеть fopen() man page,

Вы не должны закрывать файловый дескриптор, поэтому удалите close(fd) и закрыть stdout_bak_fd если вы хотите, чтобы сообщение было напечатано только в файле.

Другие вопросы по тегам