Фабиан Гизен объясняет принципиальную асимметрию операций чтения и записи на примере POSIX read и write и буферизованных fread и fwrite. Внешне они выглядят зеркально: одна операция переносит байты из файла в память, другая — обратно. Различие проявляется в момент, когда программа узнаёт об успехе или ошибке.
При чтении несколько слоёв буферизации не мешают своевременной диагностике. Библиотека запрашивает данные у ядра, ядро проверяет buffer cache и при необходимости обращается к устройству. Если файл слишком короткий, сетевая ФС недоступна или диск вернул ошибку, вызов завершится соответствующим результатом до того, как приложение получит данные. Каждому промежуточному слою достаточно сохранить и передать сведения об ошибке.
При записи успешный возврат из fwrite обычно означает лишь копирование в буфер стандартной библиотеки. Системный write произойдёт позже — при заполнении буфера, fflush, fseek или fclose. Поэтому ошибка также откладывается: проверка только fwrite не подтверждает даже выполнение системного вызова. Типичные размеры таких буферов — 4 или 8 КБ; они существуют, чтобы стоимость перехода в ядро не доминировала при мелких операциях.
Но и успешный write чаще всего фиксирует попадание данных только в кэш ядра, а не на постоянный носитель. Ядро запишет их позже, а сбой устройства или системы способен уничтожить ещё не сброшенные изменения. На сетевых файловых системах отдельные ошибки могут проявиться лишь при close, поэтому его результат тоже важно проверять.
Для важных данных автор рекомендует fsync перед close; аналог в Windows — FlushFileBuffers. Однако остаются кэши RAID-контроллеров и самих дисков, рассчитанные на питание при аварийном отключении. Кроме того, fsync файла не обязательно сохраняет запись каталога, через которую этот файл доступен: в Linux для гарантии видимости имени может потребоваться отдельный fsync каталога.
Практический вывод не в том, что буферизация вредна, а в том, что у записи другой контракт надёжности. Чтобы важный файл действительно сохранился, недостаточно отсутствия ошибки у первого вызова — нужно продумать весь путь данных и метаданных до носителя.