Почему 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
, если вы хотите, чтобы сообщение печаталось только в файле.