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

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

  • Если stdout отправляется на консоль, то printf() буферизируется по строке и очищается после печати новой строки.
  • Если stdout перенаправляется в файл, буфер не сбрасывается, если не вызывается fflush().
  • Кроме того, если 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");
}

Ответы

Ответ 1

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

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

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

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

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

Некоторые фактические реализации

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

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

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

Ответ 2

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

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

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

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

Решение

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

  • Он очищает буферы исходного потока FILE. (случай 2a)
  • Он устанавливает режим буферизации в соответствии с вновь открывшимся файлом. (случай 3)

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

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

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

Ответ 3

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