Page cache и fsync: почему «записали» и «не потеряется» — разные утверждения
`write` возвращается до того, как данные окажутся на диске: они попадают в кеш ядра. Отсюда всё остальное — почему записанное переживает падение процесса, но при потере питания может быть потеряно, почему сброс дороже записи в десятки раз и почему база данных фиксирует транзакции пачками.
Полное техническое изложение
TL;DR
write доводит данные до кеша ядра и возвращается — на этом его работа
кончается: A successful return from write() does not
make any guarantee that data has been committed to disk
(Успешный возврат из write() никак не гарантирует, что данные зафиксированы на диске). Кеш при этом —
память ядра, а не процесса: данные в нём уже общие, но ещё не устойчивые.
Устойчивость просят отдельно, вызовом сброса.
Отсюда главное следствие: две аварии различаются. Измерено: процесс
записал без сброса и был убит SIGKILL — данные на месте и видны другому
процессу, то есть падение процесса не теряет ничего. А при потере питания
данные, ещё не ставшие устойчивыми, могут быть потеряны — вместе с памятью, в
которой кеш и жил. Устойчивость и стоит соответственно: измерено на этой
машине, запись 4 КиБ — 1,8 мкс, запись со сбросом — 164,7 мкс, отношение
90,1. Числа у вас будут другими, разрыв в два порядка — тем же.
Дальше — то, что отличает знающего от читавшего. fsync и fdatasync
здесь неразличимы, и это тоже результат: разрыв 0,9 мкс при разбросе 51,1 мкс
между повторами одного и того же режима, а последняя строка блока,
difference resolvable on this machine, говорит no — заявлять кратность,
которой не видно, нельзя. Один сброс на сто записей дешевле ста сбросов в
разы: измерено 17,2 мс против 1,0 мс, и ровно поэтому базы данных фиксируют
транзакции группами. fsync файла не делает долговечным его имя: создание
файла — изменение каталога, а каталог синхронизируется отдельно, For that an explicit fsync() on a file
descriptor for the directory is also needed
(для этого нужен отдельный вызов fsync() на дескрипторе каталога). И закрытие файла не сбрасывает
ничего: Typically, filesystems do not flush
buffers when a file is closed
(Как правило, файловые системы не сбрасывают буферы при закрытии файла) — привычка «закрыл, значит сохранил» не имеет
основания.
- программа записывает данные в файл и получает от этой операции успех или ошибку;
- у машины есть оперативная память, которая исчезает при выключении питания, и есть устройство хранения, которое не исчезает;
- процесс может умереть, а машина при этом продолжит работать, — и наоборот.
- что такое кеш страниц, фоновая запись ядра и чем кеш ядра отличается от буфера в самой программе;
fsync,fdatasync,O_DIRECT, групповая фиксация, сброс каталога.
Что здесь на самом деле спрашивают
Лестница обычно такая:
- «Что делает
write?» — разминка, на которой выясняется, думает ли собеседник, что данные ушли на диск. - «Данные потеряются, если процесс упадёт сразу после
write?» — здесь начинается содержание. - «А если упадёт машина?» — тот же вопрос, другой ответ, и разница объясняет весь механизм.
- «Чем
fsyncотличается отfdatasync?» — знаете ли вы, что различаются они метаданными. - «Почему база данных не делает
fsyncна каждую запись?» — вопрос про цену. - «Достаточно ли
fsyncфайла, чтобы файл точно существовал?» — вопрос, где долговечность содержимого и долговечность имени расходятся.
Числа получены прогоном bench/durability/fsync.py и
bench/durability/practice.py. Времена зависят от диска сильно: у вас будут
другие. От машины не зависят наблюдения: что переживает падение процесса и что
обещает каждый вызов.
База: где данные после того, как запись вернула успех
Прежде чем говорить о кеше и о сбросе, стоит назвать обычными словами путь, по которому идут данные. У него четыре участка:
- приложение вызвало запись — передало ядру кусок памяти и сказало, в какой файл его положить;
- память ядра — данные приняты и лежат в области, которой распоряжается ядро; вызов на этом возвращается;
- файловая система — она решает, куда именно на устройстве эти байты лягут и какие служебные записи при этом надо поправить;
- устройство хранения — то место, где байты остаются, когда питания нет.
Ключевое здесь — на каком участке заканчивается вызов записи. Он заканчивается на втором, а не на четвёртом: приложение получает успех, когда данные приняло ядро, а не когда их принял диск. Дальше их несёт ядро — само, в своём темпе, и приложение при этом ничего не ждёт и ни о чём не узнаёт.
И вот главный вопрос урока: запись вернула успех — где на самом деле данные? От ответа зависит всё остальное: что с ними случится, если сейчас умрёт процесс, что случится, если пропадёт питание, и во что обходится требование, чтобы они уже точно были на устройстве.
Короткий ответ такой: данные в памяти ядра. Эта память общая — она не принадлежит записавшему процессу и переживает его смерть. Но она же и исчезает вместе с питанием, поэтому «данные приняты» и «данные устойчивы» — два разных утверждения, и второе нужно просить отдельным вызовом.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Всё дальнейшее — про то, сколько стоит этот отдельный вызов, чем различаются два его вида и почему базы данных требуют подтверждения не на каждую запись.
Механизм 1: write доводит данные до кеша, а не до диска
Начнём с того, что write(2) говорит о себе прямо:
A successful return from write() does not make any guarantee that data has been
committed to disk. On some filesystems, including NFS, it does not even
guarantee that space has successfully been reserved for the data. In this case,
some errors might be delayed until a future write(2), fsync(2), or even
close(2).
Успешный возврат из write() никак не гарантирует, что данные зафиксированы на диске. На некоторых файловых системах, включая NFS, он не гарантирует даже того, что под данные успешно зарезервировано место. В этом случае часть ошибок может отложиться до следующего write(2), fsync(2) или даже close(2).
Обратите внимание на вторую половину: ошибка может прийти позже, и прийти
она может в close. Это первое практическое следствие: код, который
игнорирует результат close, теряет ошибки записи молча.
Что делает write на самом деле — переносит данные в page cache, кеш
страниц файлов, живущий в памяти ядра. Оттуда их когда-нибудь вынесет на диск
фоновая запись ядра; когда именно, приложение не знает и не решает.
И раз кеш — это память ядра, привычка «закрыл файл, значит сохранил» тоже не
имеет оснований, о чём close(2) говорит отдельно:
A successful close does not guarantee that the data has been successfully saved
to disk, as the kernel uses the buffer cache to defer writes. Typically,
filesystems do not flush buffers when a file is closed.
Успешное закрытие не гарантирует, что данные удалось сохранить на диск, поскольку ядро откладывает запись через буферный кеш. Как правило, файловые системы не сбрасывают буферы при закрытии файла.
Механизм 2: падение процесса и потеря питания — разные события
Самый полезный вопрос темы: если данные ещё не на диске, что их теряет?
Проверяем самым грубым способом. Процесс пишет в файл, fsync не зовёт — и
получает SIGKILL, то есть умирает без единой строки уборки:
2. WHOSE MEMORY THE PAGE CACHE IS
---------------------------------
child wrote without fsync, then SIGKILL exit -9
file content after the kill survived the kill
exit -9 — так о смерти по сигналу сообщает subprocess: отрицательное число
здесь и есть номер сигнала. Оболочка на том же месте напечатала бы 137, то
есть 128 + 9 (урок про сигналы).
Данные на месте. Более того, их видит другой процесс: bench/durability/practice.py
печатает содержимое файла дважды — глазами того, кто прочитал его после смерти
автора, и глазами постороннего процесса. Обе строки одинаковы, потому что
чтение идёт через тот же кеш.
Отсюда различение, которым стоит отвечать:
- Падение процесса не теряет ничего, что дошло до
write. Кеш переживёт и падение, и убийство, и OOM-killer. - Потеря питания — другое дело: данные, ещё не ставшие устойчивыми, при ней могут быть потеряны, потому что память, в которой кеш и жил, выключается вместе с машиной.
Обратите внимание на «могут быть». Гарантии, что несброшенное уцелеет, нет никакой; гарантии, что оно непременно пропадёт, тоже нет — часть страниц ядро могло вынести на устройство фоновой записью раньше, и какие именно, приложение не знает. Планировать на этом нельзя: с точки зрения приложения всё несброшенное — это данные, о судьбе которых после отключения питания ничего не известно.
Именно поэтому «у нас всё в порядке, приложение просто перезапустилось» — честная фраза, а «у нас всё в порядке, машина просто перезагрузилась» — нет.
Механизм 3: что обещает fsync и сколько это стоит
Обещание записано в fsync(2):
fsync() transfers ("flushes") all modified in-core data of (i.e., modified
buffer cache pages for) the file referred to by the file descriptor fd to the
disk device.
fsync() переносит («сбрасывает») на дисковое устройство все изменённые данные файла, находящиеся в памяти ядра, — то есть изменённые страницы буферного кеша для файла, на который ссылается дескриптор fd.
Цена этого обещания и есть главное измерение урока:
1. THE PRICE OF DURABILITY, PER WRITE
-------------------------------------
write only (4 KiB), median 1.4 us
write + fdatasync, median 142.9 us
write + fsync, median 143.9 us
fsync over plain write 104x
gap between fsync and fdatasync 0.9 us
run-to-run spread of the same mode 51.1 us
difference resolvable on this machine no
Этот блок снят прогоном bench/durability/fsync.py. В задачах ниже цитируется
второй прогон, bench/durability/practice.py: там та же пара получилась 1,8 и
164,7 мкс, а кратность — 90,1.
Два прогона на одной машине расходятся именно так, как обещает строка
run-to-run spread: содержателен порядок кратности, а не её цифра.
Две вещи в этом блоке важны по-разному.
Кратность — содержательна. Сотня раз: столько стоит разница между «данные в памяти ядра» и «данные на устройстве». Это не свойство Linux и не недоработка, а физика: во втором случае в дело вступает устройство и его подтверждение.
Разница между fsync и fdatasync — не различима здесь, и скрипт это
печатает сам. Разрыв 0,9 мкс при разбросе 51,1 мкс между повторами одного и
того же режима: скрипт меряет каждый режим пятью кругами, и это строка
run-to-run spread of the same mode. Заявлять кратность в такой ситуации
нельзя, и последняя строка блока, difference resolvable on this machine,
говорит no.
Различие между вызовами при этом реальное и записано в документации:
fdatasync() is similar to fsync(), but does not flush modified metadata unless
that metadata is needed in order to allow a subsequent data retrieval to be
correctly handled.
fdatasync() подобен fsync(), но не сбрасывает изменённые метаданные, если только они не нужны для корректного последующего чтения данных.
То есть fdatasync экономит на записи метаданных — и выигрыш появляется там,
где эти метаданные дороги: при росте файла, на файловых системах с журналом
метаданных, на медленных устройствах. На виртуальном диске этого контейнера
экономия утонула в разбросе, и честный ответ здесь — «не измерено», а не
«одинаково».
Механизм 4: почему база данных не делает fsync на каждую запись
Кратность из прошлого блока — это цена одного обещания. Дальше вопрос арифметики: сколько раз его требовать.
3. ONE FSYNC FOR MANY RECORDS, OR ONE EACH
------------------------------------------
100 records, fsync after each 17.2 ms
100 records, one fsync at the end 1.0 ms
ratio 17x
Байты те же, диск тот же, и после последней строки данные одинаково долговечны в обоих случаях. Разница — в числе точек, в которых мы потребовали подтверждения.
Отсюда групповая фиксация, которую в базах называют group commit: транзакции, пришедшие почти одновременно, ждут общего сброса. Каждая ждёт дольше, чем ждала бы в одиночку, но одна задержка обслуживает всю группу, а не каждую транзакцию по отдельности.
И отсюда же ответ на вопрос «почему нельзя просто делать fsync на каждый
запрос»: можно, и это будет корректно. Цена — та самая кратность, помноженная
на число запросов, и она превращается в предел пропускной способности.
Глубже: имя файла живёт не в файле
Последняя ступень — про место, где долговечность содержимого и долговечность
имени расходятся. Вы создали файл, записали, вызвали fsync, машина упала. Гарантированно ли файл существует?
fsync(2) отвечает недвусмысленно:
Calling fsync() does not necessarily ensure that the entry in the directory
containing the file has also reached disk. For that an explicit fsync() on a
file descriptor for the directory is also needed.
Вызов fsync() не гарантирует, что запись о файле в содержащем его каталоге тоже дошла до диска. Для этого нужен отдельный вызов fsync() на дескрипторе каталога.
4. WHAT FSYNC IS FOR: THE FILE, NOT THE DIRECTORY
-------------------------------------------------
file synced yes
directory sync cost 0.11 ms
Замер здесь отвечает не на вопрос «нужен ли сброс каталога» — на него отвечает документация, — а на вопрос «сколько он стоит».
Содержимое файла и факт его существования под этим именем — два разных объекта, и синхронизируются они отдельно. Отсюда обязательная последовательность для атомарной замены файла, которую и спрашивают: записать во временный файл, сбросить его, переименовать поверх целевого, сбросить каталог. Пропуск последнего шага — это код, который работает до первого отключения питания и теряет ровно то, что считал записанным.
Как отвечать на собеседовании
Короткий ответ: write доводит данные только до кеша ядра и возвращается;
долговечность обещает fsync, и стоит она на два порядка дороже. Поэтому
падение процесса не теряет записанного — кеш принадлежит ядру, — а при потере
питания всё несброшенное может быть потеряно.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Хороший ответ отличают три вещи. Первая — вы различаете две аварии: смерть
процесса и отключение питания, и говорите, почему ответ разный. Вторая — вы
называете цену числом и знаете, что из неё следует: групповая фиксация
существует не ради красоты, а потому что сброс на каждую запись упирается в
устройство. Третья — вы помните про каталог: fsync файла делает долговечным
содержимое, а не имя, и при атомарной замене файла синхронизировать нужно оба.
И одна аккуратность в формулировке, которую замечают. «Падение машины теряет данные» — слишком сильно сказано: правильно говорить, что данные, ещё не ставшие устойчивыми, при потере питания могут быть потеряны. Часть их ядро могло успеть вынести фоновой записью, и какая именно часть — приложению неизвестно. Практический вывод от этого не меняется, а вот утверждение становится верным: на несброшенное нельзя рассчитывать ни в одну, ни в другую сторону.
Дальше спросят
Если fsync такой дорогой, можно ли просто полагаться на кеш?
Можно — ровно до первого отключения питания. Всё, что не сброшено, живёт в памяти ядра, и в этом смысле «данные записаны» означает «данные записаны, пока машина включена».
Практический выбор поэтому не «сбрасывать или нет», а где поставить
границу: какие данные обязаны пережить отключение питания, а какие можно
потерять. Журнал транзакций — обязаны; кеш отрисованных картинок — нет. Эта
граница и определяет, где в коде стоит fsync.
Ошибка записи придёт в write?
Не обязательно. write(2) прямо предупреждает, что часть ошибок откладывается
до следующего write, до fsync или даже до close. Причина та же: во время
write данные до устройства не доходили, и об устройстве ещё ничего не
известно.
Отсюда правило, которое в коде нарушают чаще всего: результат close
проверять обязательно, как и результат fsync. Код, который закрывает файл
без проверки, теряет ровно те ошибки, ради которых всё и затевалось.
Помогает ли fsync, если диск врёт о завершении записи?
Нет, и это граница обещания. fsync доводит данные до устройства и ждёт его
подтверждения; если устройство подтверждает раньше, чем данные оказались в
энергонезависимой памяти, гарантия рушится на уровне, куда ядро не достаёт.
Практически это означает, что долговечность — свойство всей цепочки: приложение, ядро, файловая система, контроллер, диск. Проверяют её не рассуждением, а испытанием с внезапным отключением питания — и по этой причине такие испытания входят в приёмку систем хранения.
Что даёт O_DIRECT?
O_DIRECT — не про долговечность, а про обход кеша: данные идут мимо page cache, и
приложение само отвечает за буферизацию и выравнивание. Долговечность из этого
не следует — устройство всё ещё может держать запись в своём кеше, поэтому
базы данных сочетают прямой ввод-вывод со сбросом.
Смысл O_DIRECT в другом: когда приложение уже держит собственный кеш
(а база данных держит), кеш ядра дублирует его и тратит память дважды.
Частые заблуждения
если write вернулся успешно, данные на диске
Нет: A successful return from write() does not make any guarantee that data has been committed to disk
(Успешный возврат из write() никак не гарантирует, что данные зафиксированы на диске). Данные дошли до кеша ядра, и когда они попадут на устройство, решает не приложение. Обещание даёт только fsync — и стоит оно на два порядка дороже: измерено 1,8 мкс против 164,7.
закрыл файл — значит сохранил
close(2) отвечает прямо: Typically, filesystems do not flush buffers when a file is closed
(Как правило, файловые системы не сбрасывают буферы при закрытии файла). Более того, именно в close может прийти ошибка предыдущей записи — поэтому его результат обязателен к проверке.
если процесс упадёт до fsync, данные потеряются
Не потеряются. Измерено: процесс записал без сброса и был убит SIGKILL — содержимое на месте и видно другому процессу. Кеш принадлежит ядру, а не процессу. А вот при потере питания несброшенные данные могут быть потеряны — вместе с памятью, в которой кеш и жил, — и в этом вся разница между двумя авариями.
fdatasync заметно быстрее fsync
На этой машине разница не различима: разрыв 0,9 мкс при разбросе между кругами 51,1 мкс, и скрипт сам печатает в строке difference resolvable on this machine значение no. Различие реально и записано в документации — fdatasync не сбрасывает метаданные, если они не нужны для чтения, — но выигрыш появляется там, где метаданные дороги, а не всегда.
fsync на каждую запись — это правильно, просто медленно
Это корректно и стоит измеримо: сто записей со сбросом после каждой — 17,2 мс, те же сто с одним сбросом в конце — 1,0 мс, разница в семнадцать раз. Долговечность после последней строки одинаковая. Разница — только в числе требований подтверждения, и на этом стоит групповая фиксация в базах данных.
fsync файла гарантирует, что файл существует
Гарантирует, что долговечно его содержимое. Имя живёт в каталоге, и это отдельный объект: For that an explicit fsync() on a file descriptor for the directory is also needed
(Для этого нужен отдельный вызов fsync() на дескрипторе каталога). Отсюда обязательный последний шаг атомарной замены — сброс каталога.
O_DIRECT делает запись долговечной
O_DIRECT обходит кеш ядра, но не отменяет кеш устройства: подтверждение может прийти раньше, чем данные окажутся в энергонезависимой памяти. Поэтому базы данных сочетают прямой ввод-вывод со сбросом, а не заменяют одно другим.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
print(content_after_kill(path) or "(empty)") print(seen_by_another_process(path) or "(empty)")
Практика · оцените
Проверка знаний
Процесс записал данные в файл и был убит SIGKILL до того, как вызвал fsync. Что с данными?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
writeдоводит данные до кеша ядра и возвращается — на этом его работа кончается: A successful return from write() does not make any guarantee that data has been committed to disk. Кеш при этом — память ядра, а не процесса: данные в нём уже общие, но ещё не устойчивые. Устойчивость просят отдельно, вызовом сброса.- Отсюда главное следствие: две аварии различаются. Измерено: процесс записал без сброса и был убит
SIGKILL— данные на месте и видны другому процессу, то есть падение процесса не теряет ничего. А при потере питания данные, ещё не ставшие устойчивыми, могут быть потеряны — вместе с памятью, в которой кеш и жил. Устойчивость и стоит соответственно: измерено на этой машине, запись 4 КиБ — 1,8 мкс, запись со сбросом — 164,7 мкс, отношение 90,1. Числа у вас будут другими, разрыв в два порядка — тем же. - Дальше — то, что отличает знающего от читавшего.
fsyncиfdatasyncздесь неразличимы, и это тоже результат: разрыв 0,9 мкс при разбросе 51,1 мкс между повторами одного и того же режима, а последняя строка блока,difference resolvable on this machine, говоритno— заявлять кратность, которой не видно, нельзя. Один сброс на сто записей дешевле ста сбросов в разы: измерено 17,2 мс против 1,0 мс, и ровно поэтому базы данных фиксируют транзакции группами.fsyncфайла не делает долговечным его имя: создание файла — изменение каталога, а каталог синхронизируется отдельно, For that an explicit fsync() on a file descriptor for the directory is also needed. И закрытие файла не сбрасывает ничего: Typically, filesystems do not flush buffers when a file is closed — привычка «закрыл, значит сохранил» не имеет основания.
На самом деле
- Нет: A successful return from write() does not make any guarantee that data has been committed to disk. Данные дошли до кеша ядра, и когда они попадут на устройство, решает не приложение. Обещание даёт только
fsync— и стоит оно на два порядка дороже: измерено 1,8 мкс против 164,7. close(2)отвечает прямо: Typically, filesystems do not flush buffers when a file is closed. Более того, именно вcloseможет прийти ошибка предыдущей записи — поэтому его результат обязателен к проверке.- Не потеряются. Измерено: процесс записал без сброса и был убит
SIGKILL— содержимое на месте и видно другому процессу. Кеш принадлежит ядру, а не процессу. А вот при потере питания несброшенные данные могут быть потеряны — вместе с памятью, в которой кеш и жил, — и в этом вся разница между двумя авариями. - На этой машине разница не различима: разрыв 0,9 мкс при разбросе между кругами 51,1 мкс, и скрипт сам печатает в строке
difference resolvable on this machineзначениеno. Различие реально и записано в документации —fdatasyncне сбрасывает метаданные, если они не нужны для чтения, — но выигрыш появляется там, где метаданные дороги, а не всегда. - Это корректно и стоит измеримо: сто записей со сбросом после каждой — 17,2 мс, те же сто с одним сбросом в конце — 1,0 мс, разница в семнадцать раз. Долговечность после последней строки одинаковая. Разница — только в числе требований подтверждения, и на этом стоит групповая фиксация в базах данных.
- Гарантирует, что долговечно его содержимое. Имя живёт в каталоге, и это отдельный объект: For that an explicit fsync() on a file descriptor for the directory is also needed. Отсюда обязательный последний шаг атомарной замены — сброс каталога.
- O_DIRECT обходит кеш ядра, но не отменяет кеш устройства: подтверждение может прийти раньше, чем данные окажутся в энергонезависимой памяти. Поэтому базы данных сочетают прямой ввод-вывод со сбросом, а не заменяют одно другим.
Что разобрано
- Что здесь на самом деле спрашивают
- База: где данные после того, как запись вернула успех
- Механизм 1: `write` доводит данные до кеша, а не до диска
- Механизм 2: падение процесса и потеря питания — разные события
- Механизм 3: что обещает `fsync` и сколько это стоит
- Механизм 4: почему база данных не делает `fsync` на каждую запись
- Глубже: имя файла живёт не в файле
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
3 ИСТОЧНИКА
- fsync(2), Linux man-pages 6.7Официальная документация. Что именно обещает сброс: «fsync() transfers ("flushes") all modified in-core data of (i.e., modified buffer cache pages for) the file referred to by the file descriptor fd to the disk device» (fsync() переносит („сбрасывает“) на дисковое устройство все изменённые данные файла, находящиеся в памяти ядра, — то есть изменённые страницы буферного кеша для файла, на который ссылается дескриптор fd). И чего он НЕ обещает — это половина урока: «Calling fsync() does not necessarily ensure that the entry in the directory containing the file has also reached disk. For that an explicit fsync() on a file descriptor for the directory is also needed» (Вызов fsync() не гарантирует, что запись о файле в содержащем его каталоге тоже дошла до диска. Для этого нужен отдельный вызов fsync() на дескрипторе каталога). Разница с fdatasync: «fdatasync() is similar to fsync(), but does not flush modified metadata unless that metadata is needed in order to allow a subsequent data retrieval to be correctly handled» (fdatasync() подобен fsync(), но не сбрасывает изменённые метаданные, если только они не нужны для корректного последующего чтения данных).https://man7.org/linux/man-pages/man2/fsync.2.html
- write(2), Linux man-pages 6.7Официальная документация. Почему возврат из write ничего не говорит о диске: «A successful return from write() does not make any guarantee that data has been committed to disk. On some filesystems, including NFS, it does not even guarantee that space has successfully been reserved for the data. In this case, some errors might be delayed until a future write(2), fsync(2), or even close(2)» (Успешный возврат из write() никак не гарантирует, что данные зафиксированы на диске. На некоторых файловых системах, включая NFS, он не гарантирует даже того, что под данные успешно зарезервировано место. В этом случае часть ошибок может отложиться до следующего write(2), fsync(2) или даже close(2)).https://man7.org/linux/man-pages/man2/write.2.html
- close(2), Linux man-pages 6.7Официальная документация. Против привычки «закрыл файл — значит сохранил»: «A successful close does not guarantee that the data has been successfully saved to disk, as the kernel uses the buffer cache to defer writes. Typically, filesystems do not flush buffers when a file is closed» (Успешное закрытие не гарантирует, что данные удалось сохранить на диск, поскольку ядро откладывает запись через буферный кеш. Как правило, файловые системы не сбрасывают буферы при закрытии файла).https://man7.org/linux/man-pages/man2/close.2.html