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

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

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

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

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

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

  • Файловый кэш Windows — с отложенной записью (write-back). Чтение сначала из системного файлового кэша, запись тоже сначала в кэш. Отражение на диск ОС делает потом.1
  • Сущность кэша — проекция файла. Диспетчер кэша проецирует участки файла по 256 КБ в адресное пространство системы, чтение и запись становятся «копией памяти между этим представлением и буфером приложения» (глава 2).1
  • Запись догоняет отложенная запись (lazy writer) раз в секунду. Падение приложения данные не теряет, отключение питания или падение ОС теряет грязный кэш (глава 4).1
  • Три инструмента «точно записать». FlushFileBuffers (= Flush(true) в .NET), 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

Карта знаний этой статьи

Успех WriteFile не означает долговечность на диске: файловый кэш Windows сначала копирует в представление системного кэша, а отражение на диск каждую секунду догоняет lazy writer. При отключении питания или падении ОС ещё не записанные грязные страницы пропадают, поэтому данные, которые нужно записать точно, требуют выбора между FlushFileBuffers, FILE_FLAG_WRITE_THROUGH и FILE_FLAG_NO_BUFFERING.

Карта знаний диспетчера кэша и отложенной записиРисунок, показывающий связи диспетчера кэша, кэша с отложенной записью, представления системного кэша 256 КБ, упреждающего чтения, отложенной записи lazy writer, потери грязных страниц при отключении питания, надёжной записи через FlushFileBuffers, FILE_FLAG_WRITE_THROUGH и FILE_FLAG_NO_BUFFERING, требований выравнивания, согласованности с проекцией файла в память, Fast I/Oреализуетиспользуетиспользуетиспользуетиспользуетиспользуетавтоматизируетснижаетнесовместимо сможет вызватьхранится вможет вызватьпредотвращаетиспользуетнастраиваетсянастраиваетсяпредотвращаетпредотвращаетснижаеттребуетпредотвращаетможет вызватьне рекомендуетсярекомендуется длярекомендуется длянесовместимо стребуетдолжен предшествоватьпроверяетсятребуетнесовместимо стребуетдиспетчер кэшакэш с отложенной записью (write-back)проекция файла (файл, отображённый в память)представление системного кэша (слот 256 КБ)объект файлаlazy writer (поток отложенной записи)потеря неотражённых данныхFILE_ATTRIBUTE_TEMPORARYгрязная страницаотключение питания / падение ОСупреждающее чтение (read-ahead)FILE_FLAG_SEQUENTIAL_SCANFILE_FLAG_RANDOM_ACCESSFlushFileBuffersFILE_FLAG_WRITE_THROUGHFILE_FLAG_NO_BUFFERINGтребование выравнивания по секторуERROR_INVALID_PARAMETER (87)надёжная долговечность при частых записяхFlushViewOfFileFast I/OProcess Monitor (procmon.exe)IRP (пакет запроса I/O)синхронный ввод-вывод

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

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

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

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

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

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

Одно место, где легко ошибиться. 256 КБ — гранулярность представления (проекции), а не «дисковый 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 байт буфера buffer
    // (в конце файла 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) Дописывает данные этого момента + метаданные Фиксация на рубеже (фиксация транзакции и т. п.)
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 — контейнер, которым ядро передаёт запрос драйверу). В столбце Operation Procmon смесь IRP_MJ_READ и FASTIO_READ — разница, пошёл ли тот же «читать» обычным путём или коротким.

В п. 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 — с отложенной записью, сущность — проекция участков файла по 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 по умолчанию кэширует данные файлов, чтение идёт из системного файлового кэша, запись тоже в кэш — кэш с отложенной записью; о ведении кэша на объект файла под управлением диспетчера кэша; о политике задерживать запись на диск и держать в кэше, называемой отложенной записью (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) память и частоту жёстких отказов, а не только на видимый объём свободной.
Если трогать один файл и через проекцию в память, и через ReadFile/WriteFile, содержимое может разъехаться?
С обычным I/O с включённым кэшем — нет. Собственный кэш Windows сам реализован как проекция файла, поэтому проекция и кэш одного локального файла делят одни данные — изменение через одно видно через другое. Несколько процессов, которые делают проекции из одного объекта проекции файла, тоже видят согласованные данные. Но чтение и запись дескриптора, открытого с FILE_FLAG_NO_BUFFERING, кэш обходят и из этой согласованности выпадают. Чтобы надёжно закрепить изменения через проекцию, одного FlushViewOfFile мало — метаданные он не пишет и аппаратный кэш не ждёт, — после FlushViewOfFile нужен FlushFileBuffers.

Об авторе

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

Го Комура

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

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

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

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