Глубины ввода-вывода Windows (часть 4) — диспетчер кэша: когда WriteFile оказывается на диске

· Обновлено: · · Windows, Win32, I/O, Кэш, Ядро, Файловая система, .NET, C#

История изменений (1 обновлений, последнее 31 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан по текущему навыку технического перевода как полный перевод японского оригинала.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.22175325)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Глубины ввода-вывода Windows (часть 4) — диспетчер кэша: когда WriteFile оказывается на диске. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-cache-manager-writefile-disk/

DOI (зарегистрированный архив)
10.5281/zenodo.22175325
DOI (последняя зарегистрированная версия)
10.5281/zenodo.22175326

WriteFile вернул успех. Где сейчас лежат данные?

Почти наверняка ещё не на диске. Их всего лишь скопировали в кэш в памяти. Именно поэтому бывает «сохранил, а после отключения питания файл пропал»; именно поэтому бенчмарки копирования показывают физически невозможную скорость; именно поэтому СУБД аккуратно вызывают fsync.

Четвёртая часть серии «Глубины ввода-вывода Windows» — про диспетчер кэша, который стоит между приложением и диском. В части 2 мы писали: если данные уже в кэше, даже асинхронный I/O завершается синхронно. В части 1 оставили на потом короткий путь без IRP (Fast I/O). Здесь закроем оба сюжета.

1. Сначала выводы

  • Файловый кэш Windows работает по схеме write-back (отложенная запись). Чтение идёт сначала из системного файлового кэша, запись — тоже сначала в кэш. На диск операционная система дописывает позже.1
  • Сам кэш — это проекция файла. Диспетчер кэша проецирует файл кусками по 256 КБ в системное адресное пространство; чтение и запись с включённым кэшем — это копирование памяти между этим представлением и буфером приложения (глава 2).1
  • На диск данные позже сбрасывает lazy writer, который запускается раз в секунду. Если упал процесс приложения, данные не теряются. Если пропало питание или рухнула ОС, грязный кэш пропадает (глава 4).1
  • Гарантированно записать можно тремя способами: FlushFileBuffers (в .NET — Flush(true)), FILE_FLAG_WRITE_THROUGH, FILE_FLAG_NO_BUFFERING. Сбрасывать кэш на каждой частой записи неэффективно; официальная документация для такого сценария указывает связку NO_BUFFERING+WRITE_THROUGH (глава 5).21
  • У NO_BUFFERING есть требования к выравниванию. Размер и смещение должны быть кратны размеру сектора, адрес буфера — выровнен по границе физического сектора. И даже при NO_BUFFERING метаданные по-прежнему кэшируются (п. 5.3).31
  • Представление файла в памяти и кэш разделяют одни и те же данные. Обычный кэшированный I/O и файл, отображённый в память, согласованы. Чтобы проекция оказалась на диске, нужны два шага: FlushViewOfFile и затем FlushFileBuffers (глава 6).45
  • Синхронное чтение и запись из кэша иногда обходятся даже без IRP. Короткий путь Fast I/O идёт напрямую в диспетчер кэша — это ответ на вопрос из части 1 (глава 7).6

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 32, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Как устроен кэш: файл проецируется в память

2.1. Слоты по 256 КБ и копирование в памяти

Если представлять файловый кэш Windows как «ящик дисковых блоков», многое поведение не объяснить. Правильная картина такая: диспетчер кэша проецирует участки файла по 256 КБ в «слоты» системного адресного пространства, а чтение и запись с включённым кэшем выполняются как копирование памяти между этим слотом и буфером приложения.1

Системное адресное пространствоПриложение (пользовательский режим)ReadFile/WriteFile =копирование памяти со слотомПри первом обращении — чтение,запись обратно — отложенно, страницамиСистемный файловый кэшслот с проекцией участка файла в 256 КББуфер приложения(область, переданная в ReadFile/WriteFile)Файл на диске

Рис. 1: Как на самом деле выглядит I/O с включённым кэшем. С точки зрения приложения «чтение и запись файла» чаще всего — просто копирование в памяти.

Легко ошибиться в одном месте. 256 КБ — это гранулярность представления (проекции), а не размер каждого дискового запроса. Страницы внутри слота подгружаются по необходимости; объём I/O, который реально уходит на диск, зависит от размера запроса и шаблона доступа. Если участок читают впервые, для заполнения возникает дисковый I/O (сюда уходит IRP из части 1 вниз по стеку хранилища). Если данные уже в кэше, чтение заканчивается одним копированием. Фраза «попадание в кэш — даже асинхронный выпуск завершается синхронно» из главы 5 части 2 как раз про это: запрос, на который можно ответить сразу, завершают на месте. Обратная сторона: если кэш включён, а страницы в памяти нет, у обработки ошибки страницы нет асинхронного механизма, и асинхронное чтение иногда обрабатывается синхронно — та же ловушка из части 2.7

2.2. Что стоит за фразой «свободной памяти стало меньше»

Включать ли кэш и состояние упреждающего чтения ведутся отдельно для каждого открытия (на объект файла),1 но сами кэшированные данные общие на файл (поток). Сколько раз ни открывай один и тот же файл, отдельных кэшей не появляется: через любой дескриптор видно одно и то же содержимое (это основание согласованности в главе 6). Кэш работает под управлением диспетчера кэша всё время, пока жива Windows.1 Скопировали большой файл или много читали и писали — свободная физическая память всё больше уходит под кэш. Даже если в диспетчере задач свободной памяти меньше, большая часть — это «ожидающая память, занятая с пользой и сравнительно быстро отдаваемая, как только приложение её попросит». Чтобы при диагностике нехватки памяти не перепутать одно с другим, практику наблюдения разбирали в «.NET: как отличить ожидание GC от утечки памяти».

Это можно увидеть на экране. Диспетчер задач → Производительность → Память: нижняя полоса «состав памяти» разделена на используется / изменено / ожидание / свободно. Большая часть файлового кэша попадает в это ожидание, в списке справа суммируется как «кэшировано». Ещё подробнее — монитор ресурсов → вкладка «Память»: те же категории с объёмами. Скопируйте один файл в несколько гигабайт и посмотрите снова: ожидание выросло, свободная уменьшилась, а «используется» почти не изменилась — то есть не «память сожрали», а «свободную память взяли под кэш». Это видно глазами.

3. Упреждающее чтение — спекулятивная подгрузка

Диспетчер кэша по прошлым шаблонам доступа заранее читает участок, который, похоже, запросят следующим (read-ahead). Если файл читают подряд, данные продолжения уже лежат в кэше, пока приложение их ещё не запросило — вот почему последовательное чтение такое быстрое. Объём упреждения не фиксирован: он зависит от обнаруженного шаблона и размера запроса.

История запросов чтения приложенияфайл читается с начала подрядДиспетчер кэшараспознаёт шаблонУпреждающее чтение: следующий участокподгружается до запроса(объём зависит от шаблона и размера запроса)Подсказка FILE_FLAG_SEQUENTIAL_SCAN= упреждающее чтение активнееПодсказка FILE_FLAG_RANDOM_ACCESS= упреждение бесполезно, его ограничивают

Рис. 2: Упреждающее чтение. Кроме распознавания шаблона доступа подсказку дают флаги CreateFile.

FileOptions.SequentialScan / RandomAccess из таблицы соответствий части 1 — подсказки этому движку упреждения. Для пакетной обработки «прочитать всё подряд» — первое, для доступа вроде обхода индекса — второе. Это флаги, которыми приложение сообщает ОС то, что знает только оно: как будет читать файл дальше. Тогда место применения становится очевидным.

4. Отложенная запись: что на самом деле значит «успех» WriteFile

4.1. lazy writer приходит каждую секунду

На стороне записи работает кэш с отложенной записью (write-back). WriteFile возвращает успех в момент копирования данных в слот, запись на диск откладывается. Эта политика «писать позже» и есть отложенная запись (lazy writing).1

Сброс на диск ведёт lazy writer, который диспетчер кэша запускает каждую секунду. Он ставит в очередь на запись одну восьмую страниц, которые недавно не сбрасывали, и если писать ещё много — добавляет ещё. Файлы, созданные с атрибутом FILE_ATTRIBUTE_TEMPORARY, из сброса lazy writer исключены: писать то, что вот-вот удалят, бессмысленно.1 Но это подсказка атрибутом: при нехватке памяти страницы всё равно могут уйти на диск, а на файл, у которого «только имя похоже на временный», атрибут не действует.

Дискlazy writer (каждую секунду)Системный кэшПриложениеДискlazy writer (каждую секунду)Системный кэшПриложениеКопирование в слот,страница становится грязной (ещё не на диске)Пока данные не сброшены — «опасное окно»:при отключении питания или сбое ОС они пропадутЗдесь данные впервые становятся устойчивымиWriteFile (данные)Сразу возвращается TRUEБерёт 1/8 грязных страницСбрасывает их пакетом на диск

Рис. 3: Отложенная запись. Успех WriteFile означает «передано ОС», а не «уже на диске».

4.2. Что случилось — сколько пропадает

Смысл «опасного окна» нужно зафиксировать точно. Судьба данных зависит от вида сбоя.

Данные сразу после успеха WriteFile(грязная страница в кэше)Что произошлоПроцесс приложенияупал или его принудительно завершилиОстановилась вся ОС(отключение питания, синий экран)Данные сохраняются:кэш принадлежит ОС,lazy writer сбросит их как запланированоГрязные страницы теряются.Остаётся только то, что уже дошло до диска

Рис. 4: Вид сбоя и развилка выживания. Кэш — не «имущество процесса», а «имущество ОС».

  • Падение приложения данные не уничтожает. С момента копирования в кэш владелец данных — ОС. «Сохранили, приложение сразу упало, а файл цел» — как раз благодаря этому.
  • Падение всей ОС уничтожает грязную часть. Частоту сброса настраивают как обмен между производительностью и надёжностью; документация прямо говорит: при внезапной потере питания незаписанные данные кэша пропадают.1

То есть вопрос при проектировании бизнес-приложения такой: можно ли потерять эти данные в момент отключения питания? Несколько секунд журнала, возможно, можно. Подтверждённую запись заказа — нельзя. Инструменты следующей главы нужны только для того, что терять нельзя.

5. Набор инструментов «точно записано»

5.1. FlushFileBuffers — сбросить прямо сейчас

FlushFileBuffers дописывает буферизованные данные указанного файла на устройство. Метаданные файловой системы кэшируются всегда, поэтому чтобы и они гарантированно дошли, нужен сброс (или WRITE_THROUGH) — это тоже стоит держать в уме.12 В .NET этому соответствует FileStream.Flush(true) (одного Flush() мало: он отдаёт внутренний буфер .NET операционной системе, кэш ОС остаётся как был).8

Но официальная документация прямо предупреждает: вызывать после каждой записи неэффективно. Если нужна фиксация на каждой из многих записей — пользоваться связкой NO_BUFFERING+WRITE_THROUGH ниже.2

5.2. FILE_FLAG_WRITE_THROUGH — убрать только задержку

При открытии с FILE_FLAG_WRITE_THROUGH запись идёт и в кэш, и сразу на диск, не дожидаясь lazy writer.1 Чтение по-прежнему идёт через кэш — прямой ответ на «чтение пусть останется быстрым, задержку записи только убрать».

5.3. FILE_FLAG_NO_BUFFERING — в обход кэша

FILE_FLAG_NO_BUFFERING убирает сам системный кэш из чтения и записи. Каждый обмен становится I/O к дисковому устройству, минуя кэш.1 Но обойти можно только системный кэш Windows; как на рис. 5, кэш записи внутри устройства — отдельный ярус. Если нужна устойчивость и к отключению питания, по-прежнему нужны сочетание с WRITE_THROUGH или FlushFileBuffers. Это инструмент для массовой передачи большого объёма и для движка СУБД, который сам управляет буферами, но ограничения жёсткие.3

  • Размер и смещение в файле чтения и записи — кратны размеру сектора тома (для сектора 512 байт: 512, 1024, 1536…).
  • Адрес буфера тоже выровнен по размеру физического сектора (нужна и забота о дисках Advanced Format с физическим сектором 4096 байт).
  • И всё равно метаданные продолжают кэшироваться, поэтому для полной фиксации нужны сочетание с WRITE_THROUGH или FlushFileBuffers.12

Об эти ограничения спотыкаются в первую очередь те, кто просто добавил флаг. Чтение и запись без выравнивания падают с ERROR_INVALID_PARAMETER (87). Три пункта, которые нужно соблюсти.3

Что выровнять Условие Как выполнить
Размер чтения и записи Кратно размеру сектора тома Взять lpBytesPerSector из GetDiskFreeSpace и округлить до кратного
Смещение в файле То же (и когда задаёте Offset в OVERLAPPED) Идти шагами, кратными размеру сектора
Адрес буфера Выравнивание по размеру физического сектора Выделять через VirtualAlloc (возвращает область, выровненную по границе страницы, обычно 4096 байт)

Третий пункт пропускают чаще всего. Адрес, который возвращают malloc, new или массив C#, выравнивания по границе сектора не гарантирует. VirtualAlloc, который выделяет по границе страницы, заодно закрывает требование дисков Advanced Format с физическим сектором 4096 байт. Минимальная форма такая.

// C++ / Win32. Обработка ошибок намеренно минимальная.
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", &sectorsPerCluster, &bytesPerSector,
                       &freeClusters, &totalClusters))
{
    return GetLastError();
}

// Делаем единицу чтения и записи кратной размеру сектора (здесь около 1 МиБ)
const DWORD chunk = (1024 * 1024 / bytesPerSector) * bytesPerSector;

// Буфер выделяем с выравниванием по границе страницы (malloc/new этого не дают)
BYTE* buffer = static_cast<BYTE*>(
    VirtualAlloc(nullptr, chunk, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
if (buffer == nullptr) { return GetLastError(); }

HANDLE h = CreateFileW(L"C:\\temp\\big.bin", GENERIC_READ, FILE_SHARE_READ, nullptr,
                       OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, nullptr);
if (h == INVALID_HANDLE_VALUE)
{
    // GetLastError возвращает результат последнего вызова Win32. Если сначала вызвать
    // VirtualFree, причина сбоя CreateFileW (отказано в доступе, путь не найден и т. п.)
    // затрётся кодом очистки, и функция вернёт бессмысленный код ошибки.
    const DWORD err = GetLastError();
    VirtualFree(buffer, 0, MEM_RELEASE);
    return err;
}

// Идём шагами по chunk байт — так размер и смещение остаются выровненными
DWORD read = 0;
while (ReadFile(h, buffer, chunk, &read, nullptr) && read > 0)
{
    // Обрабатываем первые read байт буфера
    // (в конце файла read < chunk — это нормально)
}

CloseHandle(h);
VirtualFree(buffer, 0, MEM_RELEASE);

Ещё: в FileOptions .NET нет значения, соответствующего FILE_FLAG_NO_BUFFERING. Если флаг действительно нужен — придётся вызывать CreateFile напрямую, и тогда выравнивание выше соблюдать самим. Порядок рассмотрения: «NO_BUFFERING, потому что буферами управляем сами», а не «NO_BUFFERING, потому что хотим быстрее».

5.4. Как выбирать инструмент

WriteFile по умолчанию: успех возвращается уже здесьlazy writer (каждую секунду) / WRITE_THROUGH (сразу)По расписанию устройства /FlushFileBuffers требует дописать до концаNO_BUFFERING обходит кэш и идёт напрямуюБуфер приложенияСистемный файловый кэш(грязные страницы)Кэш внутри дискового устройстваЭнергонезависимый носитель

Рис. 5: Ярусы данных и как далеко каждый инструмент их проталкивает. Не забыть последний ярус — кэш внутри дискового устройства.

Способ Что происходит Куда годится
По умолчанию (кэш включён) Завершается копированием в кэш. На диск пишет lazy writer Почти любой файловый I/O
FlushFileBuffers / Flush(true) Дописывает данные этого момента и метаданные Фиксация в контрольной точке (commit транзакции и т. п.)
FILE_FLAG_WRITE_THROUGH При каждой записи сразу на диск (чтение из кэша) Непрерывный лог или журнал, который нельзя терять
FILE_FLAG_NO_BUFFERING (+WRITE_THROUGH) Мимо кэша. Есть требования к выравниванию Собственное управление буферами, массовый I/O

Порядок выбора — два шага: сначала решить, сколько записей можно потерять в момент отключения питания, потом проверить, насколько ради этого можно стать медленнее. Не сверху вниз по таблице — идите по этой развилке.

можно(несколько секунд журнала и т. п.)нельзяконтрольная точка(фиксация сделки и т. п.)каждую записьнет (обычное приложение)да (движок СУБД и т. п.)Нужно записать эти данныеМожно ли потерять их в моментотключения питания или синего экрана?Оставить по умолчанию (кэш включён)Самый быстрый путь. Так работает почти весь I/OНельзя терятьв контрольной точке или каждую запись?В контрольной точке — FlushFileBuffersв .NET — Flush(true)цена: ожидание только в этой точкеСами управляете буферами иможете выполнить выравнивание из п. 5.3?FILE_FLAG_WRITE_THROUGHпри каждой записи сразу на дискчтение по-прежнему быстрое за счёт кэшаFILE_FLAG_NO_BUFFERING+ FILE_FLAG_WRITE_THROUGHвариант «частой гарантированной записи» из документации

Рис. 6: Как выбрать инструмент. Первая развилка — требование к надёжности, вторая — цена, которую можно заплатить. Пути «FlushFileBuffers на каждую запись» нет, потому что, как в п. 5.1, официальная документация считает это неэффективным.

Три практических шаблона.

  1. «Писать во временный файл → сбросить → переименовать» — обычный приём, чтобы не оставлять наполовину записанный файл. Сначала дописать содержимое, потом зафиксировать именем — эта атомарная передача подробно разобрана в «Основы взаимного исключения при файловой интеграции».
  2. Отдать это базе данных — тоже нормальное решение. Как SQLite строит устойчивость из WAL и сброса — в «SQLite в бизнес-приложениях на C#». Вариант «свою стратегию сброса не писать» всегда на столе.
  3. Бенчмарк — подозревать кэш. Измерение «чтение слишком быстрое» обычно ловит попадание в кэш со второго прохода. Как измерять — в «Как правильно сравнивать скорость разных версий программы в Windows».

И не забыть последний ярус рис. 5 — кэш внутри дискового устройства. FlushFileBuffers требует дописать и туда, но у USB-накопителей и внешних дисков вмешивается политика кэша записи устройства («быстрое извлечение» и «повышенная производительность»). Обращение со съёмными устройствами — ещё в «Как работать с USB-устройствами из Windows-приложения».

6. Согласованность с файлом, отображённым в память

В части 1, услышав «кэш — это проекция файла», кто-то наверняка подумал: тогда представление, которое я сам сделал через MapViewOfFile, с кэшем ReadFile/WriteFile не конфликтует?

Не конфликтует. Потому что оба сидят на одном механизме. Объект проекции файла отображает файл на диске; вытеснение страницы выполняется как запись изменений обратно в файл. Даже если несколько процессов создают представления одного локального файла, видимое содержимое согласовано (когерентно).4

Системное адресное пространствоАдресное пространство процесса AПредставление диспетчера кэша(слот, которым пользуются ReadFile/WriteFile)Представление MapViewOfFileОдни и те же физические страницы(память, за которой стоит файл)Файл на дискеI/O с FILE_FLAG_NO_BUFFERINGвне этой схемы (напрямую на диск)

Рис. 7: И проекция, и кэш смотрят на одни и те же «страницы, за которыми стоит файл». Вне этой схемы только NO_BUFFERING.

Два замечания.

  • I/O с FILE_FLAG_NO_BUFFERING в эту согласованность не входит. Чтение и запись, минующие кэш, с содержимым через проекцию или кэш не сверяются. Если смешивать — согласовывать самим.
  • Сброс проекции на диск — два шага. FlushViewOfFile начинает запись грязных страниц диапазона, но метаданные не пишет и физического завершения из кэша дискового устройства не ждёт. Чтобы данные точно дошли, после FlushViewOfFile вызывают FlushFileBuffers.5

Практика проекции файла как разделяемой памяти (именованное разделение, синхронизация, аварийные шаблоны) — в «Разделяемая память: подводные камни и практические рекомендации».

7. Fast I/O — закрываем вопрос из части 1

Сначала двумя строками. Fast I/O — короткий путь для синхронного чтения и записи файла, который уже в кэше: данные с кэшем обмениваются напрямую, без сборки IRP (пакет запроса I/O — контейнер, которым ядро передаёт запрос драйверу). Смесь IRP_MJ_READ и FASTIO_READ в столбце Operation Procmon — это разница, пошёл ли тот же «читать» обычным путём или коротким.

В п. 5.2 части 1 было «не каждый I/O становится IRP». Вот ответ.

Для файла в кэше чтение и запись, как известно, заканчиваются копированием памяти с кэшем, без сборки IRP и прохода по стеку устройств. Поэтому Windows для синхронного I/O кэшированного файла даёт короткий путь Fast I/O: IRP не собирает, напрямую вызывает точки входа Fast I/O файловой системы и копирует данные напрямую из диспетчера кэша.6 Если Fast I/O не справится (нет в кэше, задействована блокировка, вмешивается фильтр и т. п.) — возвращаются на обычный путь IRP. Это быстрый путь именно для синхронного запроса, а не «попадание в кэш = всегда Fast I/O». Операции асинхронного (FILE_FLAG_OVERLAPPED) дескриптора даже при завершении на месте из кэша (глава 5 части 2) иногда идут путём IRP.

можнонельзяСинхронное чтение и запись дескриптора с включённым кэшемМожно обработать через Fast I/O(данные уже в кэше и т. п.)?Fast I/Oкопирование напрямую с кэшем, без IRPв Procmon видно как FASTIO_Обычный путьсобрать IRP и отдать в стек устройств(мир рис. 6 части 1)

Рис. 8: Развилка Fast I/O. Поэтому в Procmon смесь FASTIO_READ и IRP_MJ_READ.

Почему в наблюдении Procmon главы 7 части 1 мешались строки FASTIO_ — объясняется этим. Для попавшего в кэш синхронного чтения IRP уже роскошь. Наличие этого пути влияет и на фильтры части 6 (минифильтр умеет вставать и в Fast I/O).

8. Итог

  • Файловый кэш Windows работает по схеме write-back, а устроен как проекция участков файла по 256 КБ. Чтение и запись с включённым кэшем становятся копированием памяти со слотом.1
  • Чтение упреждает спекулятивно, SequentialScan/RandomAccess — подсказки.1
  • Запись на диск позже делает lazy writer раз в секунду. Падение приложения данные оставляет, падение всей ОС уничтожает только грязную часть. Вопрос проектирования: можно ли потерять эти данные в момент отключения питания?1
  • Инструменты гарантированной записи — FlushFileBuffers (фиксация в контрольной точке) / WRITE_THROUGH (каждую запись) / NO_BUFFERING (мимо кэша + требования к выравниванию). Сброс каждый раз неэффективен; для частой фиксации официально рекомендуют связку NO_BUFFERING+WRITE_THROUGH. Метаданные кэшируются всегда — тоже держать в уме.231
  • Проекция и кэш разделяют одни страницы и согласованы. Вне схемы только NO_BUFFERING. Сброс проекции на диск — два шага: FlushViewOfFile+FlushFileBuffers.45
  • Синхронное чтение и запись с попаданием в кэш Fast I/O обходят даже IRP. Вот что означали строки FASTIO_, которые были в Procmon части 1.6

Продолжение — часть 5 «Внутреннее устройство NTFS — файловая система через MFT». До сих пор файл был «смещение и последовательность байтов»; теперь — как NTFS раскладывает данные: MFT, несколько потоков данных, журнал, жёсткие ссылки — спуск к статической структуре на диске.

Похожие статьи

Смежные области консультирования

KomuraSoft LLC занимается проектированием файлового I/O в бизнес-приложениях Windows и разбором сбоев вроде «данные, которые вроде сохранили, пропали» и «запись в файл слишком медленная или подозрительно быстрая».

Справочные ссылки

  1. Microsoft Learn, File Caching. О том, что Windows по умолчанию кэширует данные файлов: чтение идёт из системного файлового кэша, запись тоже в кэш — это кэш с отложенной записью (write-back); о ведении кэша на объект файла под управлением диспетчера кэша; о политике задерживать запись на диск и держать данные в кэше, называемой отложенной записью (lazy writing); о том, что при чтении файла участок 256 КБ проецируется в слот 256 КБ системного адресного пространства и пользовательский процесс копирует данные с этим слотом; о том, что диспетчер кэша каждую секунду запускает lazy writer и ставит в очередь записи на диск одну восьмую недавно не сброшенных страниц, при необходимости добавляя ещё; о том, что временные файлы не сбрасываются; о том, что при внезапном сбое системы вроде потери питания незаписанные данные кэша пропадают; о том, что даже при отключении кэша через FILE_FLAG_NO_BUFFERING метаданные файла могут кэшироваться; о том, что при FILE_FLAG_WRITE_THROUGH данные пишутся и в кэш, и сразу на диск без задержки lazy writer; о том, что метаданные файловой системы кэшируются всегда, поэтому для их фиксации нужны сброс или FILE_FLAG_WRITE_THROUGH. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19

  2. Microsoft Learn, FlushFileBuffers function. О том, что WriteFile обычно пишет во внутренний буфер, а ОС периодически выводит данные на диск; о том, что FlushFileBuffers выводит на устройство все буферизованные сведения указанного файла; о том, что вызывать функцию после каждой из многих записей неэффективно и приложениям, которым нужна фиксация важных данных при частых записях, следует пользоваться небуферизованным I/O через FILE_FLAG_NO_BUFFERING и FILE_FLAG_WRITE_THROUGH; о том, что вызов на дескрипторе тома (с правами администратора) сбрасывает все открытые файлы на томе. ↩ ↩2 ↩3 ↩4 ↩5

  3. Microsoft Learn, File Buffering. О требованиях доступа к файлу, открытому с FILE_FLAG_NO_BUFFERING: размер чтения и записи и смещение в файле (включая задание через OVERLAPPED) должны быть кратны размеру сектора тома; адрес буфера чтения и записи должен быть выровнен по размеру физического сектора; нужна забота об устройствах Advanced Format с физическим сектором 4096 байт. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn, File Mapping. О том, что объект проекции файла отображает файл на диске и вытеснение страницы выполняется как запись изменений в файл; о том, что когда несколько процессов создают представления локального файла из одного объекта проекции, данные когерентны (то же содержимое, что у файла на диске). ↩ ↩2 ↩3

  5. Microsoft Learn, FlushViewOfFile function. О том, что FlushViewOfFile начинает запись грязных страниц диапазона проекции на диск; о том, что эта функция не сбрасывает метаданные файла и не ждёт завершения физической записи из аппаратного кэша диска; о том, что чтобы физически дописать все грязные страницы и метаданные, после FlushViewOfFile следует вызвать FlushFileBuffers. ↩ ↩2 ↩3

  6. Microsoft Learn, IRPs Are Different From Fast I/O. О том, что Fast I/O — быстрый путь синхронного I/O для кэшированных файлов, который напрямую вызывает точки входа файловой системы и диспетчера кэша, не порождая IRP; о том, что данные передаются напрямую из кэша в пользовательский буфер (и обратно); о том, что если Fast I/O не справится, используется обычный путь на IRP. ↩ ↩2 ↩3

  7. Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. О том, что если данные в кэше, запрос завершается на месте и возвращается TRUE; о том, что кэш Windows реализован проекцией файла и нет механизма асинхронной ошибки страницы, поэтому асинхронное чтение с включённым кэшем иногда обрабатывается синхронно. ↩

  8. Microsoft Learn, FileStream.Flush method (.NET). О том, что Flush() отдаёт внутренний буфер потока операционной системе; о том, что Flush(true) сверх того сбрасывает и все промежуточные файловые буферы (буферы ОС). ↩

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

WriteFile вернул успех. Данные уже на диске?
По умолчанию нет. Файловый кэш Windows работает по схеме write-back: WriteFile возвращает успех в тот момент, когда данные скопированы в системный файловый кэш. На диск их позже записывает lazy writer диспетчера кэша, который запускается раз в секунду. Важно, какой именно сбой произошёл. Если упал процесс приложения, данные, уже попавшие в кэш, не пропадают: пока жива ОС, она допишет их позже. Если падает вся ОС — отключение питания, синий экран — грязный кэш, который ещё не сбросили, теряется. Точнее понимать так: успех WriteFile — это не «данные уже устойчиво на диске», а «данные переданы операционной системе».
Как гарантировать, что данные реально попали на диск?
Есть три инструмента. Первый — FlushFileBuffers: он дописывает на устройство все буферизованные данные и метаданные этого файла (в .NET то же самое делает FileStream.Flush(true)). Второй — FILE_FLAG_WRITE_THROUGH: при каждой записи данные попадают и в кэш, и сразу на диск. Третий — FILE_FLAG_NO_BUFFERING: системный кэш вообще не используется. В документации Microsoft сказано, что вызывать FlushFileBuffers после каждой записи неэффективно; если при частых записях нужна гарантированная фиксация, следует сочетать FILE_FLAG_NO_BUFFERING с FILE_FLAG_WRITE_THROUGH. Все три способа отказываются от части выгод кэша и поэтому медленнее, поэтому на практике их не включают повсеместно, а оставляют для тех записей, которые действительно нельзя потерять.
Чем FILE_FLAG_WRITE_THROUGH отличается от FILE_FLAG_NO_BUFFERING?
WRITE_THROUGH означает: в кэш пишем, но до завершения операции пишем и на диск. Чтение по-прежнему идёт через кэш; снимается только задержка lazy writer на записи. NO_BUFFERING означает: чтение и запись системный кэш не проходят, каждый вызов становится I/O к устройству. Обходится только кэш Windows; кэш записи внутри самого накопителя этот флаг не отключает. Зато появляются жёсткие ограничения: размер и смещение в файле должны быть кратны размеру сектора тома, адрес буфера — выровнен по границе физического сектора. Даже при NO_BUFFERING метаданные файловой системы продолжают кэшироваться, поэтому чтобы гарантированно записать и их, нужны FlushFileBuffers или WRITE_THROUGH. Так обычно работают движки СУБД, которые сами управляют буферами; обычному приложению разумнее сначала смотреть WRITE_THROUGH или FlushFileBuffers.
В диспетчере задач мало свободной памяти. Виноват файловый кэш?
Часто да, и это нормальное поведение. Windows активно занимает свободную физическую память под файловый кэш, поэтому копирование большого файла или интенсивные чтение и запись раздувают кэш, и занятость памяти выглядит выше. Но большая часть страниц кэша — того типа, который сравнительно быстро отдаётся приложению, как только оно запросит память. Это нужно отличать от настоящей нехватки. Если подозреваете, что памяти действительно не хватает, смотрите не только видимый объём свободной, но и выделенную память (Committed) и частоту жёстких ошибок страниц (hard faults).
Если к одному файлу обращаться и через проекцию в память, и через ReadFile/WriteFile, содержимое может разойтись?
С обычным I/O при включённом кэше — нет. Сам кэш Windows реализован как проекция файла, поэтому представление и кэш одного локального файла разделяют одни данные: изменение через одно видно через другое. Несколько процессов, которые создают представления из одного объекта проекции файла, тоже видят согласованные (когерентные) данные. Но чтение и запись через дескриптор, открытый с FILE_FLAG_NO_BUFFERING, кэш обходят и в эту согласованность не входят. Чтобы надёжно сбросить на диск изменения через проекцию, одного FlushViewOfFile мало — метаданные он не пишет и аппаратный кэш не ждёт; после FlushViewOfFile нужно вызвать FlushFileBuffers.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог