Почему 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");
}
3 ответа
Промывка для stdout
определяется его буферным поведением. Буферизация может быть настроена на три режима: _IOFBF
(полная буферизация: ждет до fflush()
если возможно), _IOLBF
(буферизация строки: новая строка запускает автоматическую очистку), и _IONBF
(прямая запись всегда используется). "Поддержка этих характеристик определяется реализацией и может зависеть от setbuf()
а также setvbuf()
функции. "[C99: 7.19.3.3]
"При запуске программы три текстовых потока предопределены и не требуют явного открытия - стандартный ввод (для чтения обычного ввода), стандартный вывод (для записи обычного вывода) и стандартная ошибка (для записи диагностического вывода). стандартный поток ошибок не полностью буферизован; стандартные входные и стандартные выходные потоки полностью буферизуются, если и только если можно определить, что поток не относится к интерактивному устройству ". [С99:7.19.3.7]
Объяснение наблюдаемого поведения
Итак, что происходит, так это то, что реализация делает что-то для платформы, чтобы решить, стоит ли stdout
будет буферизироваться строкой. В большинстве реализаций libc этот тест выполняется при первом использовании потока.
- Поведение № 1 легко объяснимо: когда поток предназначен для интерактивного устройства, он буферизуется в строке, и
printf()
сбрасывается автоматически. - Также ожидается случай № 2: когда мы перенаправляем в файл, поток полностью буферизуется и не будет очищен, кроме как с помощью
fflush()
, если вы не напишите gobloads данных к нему. - Наконец, мы понимаем случай №3 также для реализаций, которые выполняют проверку базового fd только один раз. Потому что мы принудительно инициализировали буфер stdout в первом
printf()
, stdout получил линейно-буферизованный режим. Когда мы меняем fd для перехода в файл, он все еще буферизуется, поэтому данные автоматически сбрасываются.
Некоторые актуальные реализации
Каждая библиотека libc обладает широтой интерпретации этих требований, поскольку C99 не указывает, что такое "интерактивное устройство", и запись stdio в POSIX не расширяет ее (за исключением того, что stderr должен быть открыт для чтения).
Glibc. Смотрите filedoalloc.c: L111. Здесь мы используем
stat()
проверить, является ли fd tty, и соответственно установить режим буферизации. (Это вызывается из fileops.c.)stdout
изначально имеет нулевой буфер и распределяется при первом использовании потока на основе характеристик fd 1.BSD libc. Очень похожий, но гораздо более понятный код! Смотрите эту строку в makebuf.c
Вы неправильно комбинируете буферизованные и небуферизованные функции ввода-вывода. Такое сочетание должно выполняться очень осторожно, особенно когда код должен быть переносимым. (и это плохо писать непереносимый код...)
Конечно, лучше избегать комбинирования буферизованного и небуферизованного ввода-вывода в одном файловом дескрипторе.
Буферизованный ввод-вывод: fprintf()
, fopen()
, fclose()
, freopen()
...
Небуферизованный ввод-вывод: write()
, open()
, close()
, dup()
...
Когда вы используете dup2()
перенаправить стандартный вывод. Функция не знает о буфере, который был заполнен fprintf()
, Так когда dup2()
закрывает старый дескриптор 1, он не очищает буфер, и содержимое может быть сброшено на другой вывод. В вашем случае 2a он был отправлен /dev/null
,
Решение
В вашем случае лучше всего использовать freopen()
вместо dup2()
, Это решает все ваши проблемы:
- Сбрасывает буферы оригинала
FILE
поток. (случай 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
если вы хотите, чтобы сообщение было напечатано только в файле.