Вопрос или проблема
Я прочитал в книге “Расширенное программирование в Unix-среде” следующее:
Буферизация по строкам имеет два предупреждения. Во-первых, размер буфера, который стандартная библиотека ввода-вывода использует для накопления каждой строки, фиксирован, поэтому ввод-вывод может произойти, если мы заполним этот буфер перед записью символа новой строки. Во-вторых, каждый раз, когда ввод запрашивается через стандартную библиотеку ввода-вывода из (а) небужеризованного потока или (б) потока с буферизацией по строкам (который требует запроса данных из ядра), все выходные потоки с буферизацией по строкам сбрасываются. Причина для квалификатора в (б) заключается в том, что запрашиваемые данные могут уже находиться в буфере, что не требует чтения данных из ядра. Очевидно, что любой ввод из небужеризованного потока, пункт (а), требует получения данных из ядра.
https://www.gnu.org/software/libc/manual/html_node/Flushing-Buffers.html утверждает:
Сброс выходных данных из буферизованного потока означает передачу всех накопленных символов в файл. Существует множество случаев, когда буферизованный вывод на потоке автоматически сбрасывается:
Когда вы пытаетесь сделать вывод, и выходной буфер заполнен.
Когда поток закрывается. См. Закрытие потоков.
Когда программа завершается вызовом exit. См. Нормальное завершение.
Когда запись новой строки выполняется, если поток с буферизацией по строкам.
Когда любая операция ввода на любом потоке фактически считывает данные из его файла.
Итак, я написал программу, чтобы протестировать это:
#include <stdio.h>
#include <unistd.h>
int
main()
{
printf("Привет");
FILE *fp = fopen("hugefile", "r");
if (fp == NULL) {
fprintf(stderr, "Не удается открыть файл\n");
return -1;
}
setvbuf(fp, NULL, _IONBF, 0);
sleep(2);
char buf[10];
while (fread(buf, sizeof(char), 10, fp) == 10) {
}
if (ferror(fp)) {
fprintf(stderr, "Ошибка чтения\n");
return -1;
}; // Я ожидаю, что "Привет" появится на экране, но этого не происходит
fclose(fp);
sleep(50);
}
Но fread() не вызывает сброса вывода. Я что-то упускаю?
Я не знаю, что может иметь значение здесь, но я использую
gcc версия 14.2.1 20240910
glibc 2.40+r16+gaa533d58ff-2
Я думаю, что сброс буфера не подразумевает, что он будет распечатан на экране или куда бы ни было перенаправлен стандартный вывод.
Сброс стандартного вывода при чтении из стандартного ввода:
Вы можете добиться такого поведения, как в следующем buf.c:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("Привет");
sleep(2);
fprintf(stderr, "Нет вывода из stdout через 2 секунды\n");
char buf[10];
while (fread(buf, sizeof(char), 10, stdin) == 10) {
}
sleep(5);
}
Запустите это:
$ ./buf
Нет вывода из stdout через 2 секунды
Привет
Строка “Привет” распечатана в stdout и не заканчивается символом новой строки
(\n), поэтому она не сбрасывается через 2 секунды, но сбрасывается, когда
fread()
выполнен. Если бы она была удалена, она была бы сброшена только после
последнего сна.
Когда любая операция ввода на любом потоке фактически считывает данные из его
файла.
Сброс в файл при чтении из файла:
Чтобы воспроизвести Когда любая операция ввода на любом потоке фактически считывает
данные из его файла., что, я думаю, была вашей главной целью:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
FILE *fp = fopen("FILE", "a");
fprintf(fp, "добавить больше символов в FILE\n");
fprintf(stderr, "записано больше символов в FILE\n");
sleep(5);
fprintf(stderr, "чтение теперь начнется\n");
char buf[10];
while (fread(buf, sizeof(char), 10, fp) == 10) {
}
fprintf(stderr, "Сон на 5 секунд перед sleep()\n");
sleep(5);
fprintf(stderr, "Выход\n");
}
Запустите это так, я создам небольшой файл FILE, который будет содержать только текущую дату
и запущу бинарник в фоновом режиме, чтобы я мог видеть
все сообщения, которые он печатает, и одновременно смогу изучить содержимое файла FILE:
$ date > FILE
$ ./buf &
[1] 14522
записано больше символов в FILE
$ cat FILE
Сб Nov 9 13:02:28 CET 2024
$ чтение теперь начнется
Сон на 5 секунд перед sleep()
$ cat FILE
Сб Nov 9 13:02:28 CET 2024
добавить больше символов в FILE
Выход
Как вы видите, строка добавить больше символов в FILE
была добавлена в файл
только после того, как была выполнена fread()
.
Ответ или решение
Автоматическое сброс буфера при чтении ввода в C
В операционных системах Unix основным механизмом для работы с выводом является система буферизации ввода-вывода (I/O buffering). В данном случае нас интересует автоматическое сброс буфера при чтении данных из потока. Рассмотрим, как это работает, и почему ваше ожидание не подтвердилось в приведенном примере кода.
Основы работы с буферизацией
Когда программа выполняет операции с потоками, данные могут храниться в память (буфере) до тех пор, пока не произойдут определённые события, такие как достижение конца строки или заполнение буфера. В вашем случае, с помощью printf("Hello");
вы выводите текст в стандартный поток stdout
, который обычно является строковым, или по умолчанию полностью буферизованным потоком, если происходит вывод не через stderr
.
Согласно стандартной библиотеке C, вывод осуществляется при следующих условиях:
- При заполнении буфера;
- При закрытии потока;
- При завершении программы с помощью
exit
; - При записи символа новой строки (
\n
) в поток, который использует построчную буферизацию; - При операции чтения из любого потока, которая фактически требует считывания данных из файла (или устройства).
Анализ вашего кода
Вы написали следующую часть кода:
printf("Hello");
FILE *fp = fopen("hugefile", "r");
// ...
fread(buf, sizeof(char), 10, fp);
В данном коде строка "Hello"
не будет отображаться на экране немедленно из-за того, что она не завершается символом новой строки (\n
). Следовательно, stdout
не будет сброшен в момент вызова printf
, а будет ждать, когда буфер будет заполнен или когда произойдет другое условие для сброса, например, чтение из другого потока.
Ваша установка буфера как unbuffered с помощью setvbuf(fp, NULL, _IONBF, 0);
применяется к файлу fp
, а не к стандартному выводу. Следовательно, этот вызов не повлияет на поведение stdout
.
Пример автоматического сброса
Попробуйте изменить ваш код, чтобы включить вызов fflush(stdout);
после printf
, чтобы явно сбросить буфер:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Hello");
fflush(stdout); // Явный сброс буфера stdout
FILE *fp = fopen("hugefile", "r");
if (fp == NULL) {
fprintf(stderr, "Cannot open file\n");
return -1;
}
setvbuf(fp, NULL, _IONBF, 0);
sleep(2);
char buf[10];
while (fread(buf, sizeof(char), 10, fp) == 10) {
}
if (ferror(fp)) {
fprintf(stderr, "Read error\n");
return -1;
};
fclose(fp);
sleep(50);
}
Как результат, строка "Hello"
отобразится на экране до того, как вы начнете читать из файла hugefile
.
Заключение
Важно помнить, что поведение буферизации может варьироваться в зависимости от среды выполнения и настроек потоков. Автоматическое сбрасывание происходит в ответ на определенные действия (например, ввод), и если вы хотите контролировать это поведение, стоит использовать явные вызовы fflush(stdout);
в нужных местах вашего кода.
Для практического понимания автоматического сброса буферов рекомендуется экспериментировать с различными комбинациями ввода и вывода, что позволит глубже понять, как взаимодействуют потоковые операции и механизмы буферизации в C.