Windows I/Oの深層(第4回) ── キャッシュマネージャー:あなたのWriteFileはいつディスクに届くのか

· 更新日: · · Windows, Win32, I/O, キャッシュ, カーネル, ファイルシステム, .NET, CSharp

更新履歴(3件・最終更新 2026年08月01日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」を追加しました。本文で扱う概念どうしの関係を一枚の図で見渡せます。関係の全一覧(根拠のURL・確度・確認日つき)と主要概念の定義は知識マップ詳細ページにまとめ、機械可読データをJSON-LDとTurtleで公開しています。
`FILE_FLAG_NO_BUFFERING`の例で、`CreateFileW`が失敗したときに`VirtualFree`を先に呼んでから`GetLastError()`を読んでいたのを直しました。`GetLastError`が返すのは直前のWin32呼び出しの結果なので、アクセス拒否やパスが無いといった本当の失敗理由が後片付けの結果に上書きされ得ます。失敗した直後にコードを控えてから解放するようにしました。
道具の選び方を2段の分岐で示すフロー図を追加しました(これに伴い後続の図番号を繰り下げています)。`FILE_FLAG_NO_BUFFERING`が`ERROR_INVALID_PARAMETER`で止まる整列要件3点の表と最小のC++例、.NETの`FileOptions`に対応する値が無いことを追記し、ファストI/Oの章に2行の要約を置きました。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21739474)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Windows I/Oの深層(第4回) ── キャッシュマネージャー:あなたのWriteFileはいつディスクに届くのか」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21739474 https://staging.comcomponent.com/blog/windows-cache-manager-writefile-disk/

DOI(最新版)
10.5281/zenodo.21739474
DOI(この版)
10.5281/zenodo.21739475

WriteFile が成功を返しました。さて、データはいまどこにあるでしょうか。

答えは、ほぼ確実にまだディスクにありません。メモリ上のキャッシュにコピーされただけです。だから「保存したのに、電源断で戻ったら消えていた」が起こり、だからファイルコピーのベンチマークが物理的にありえない速度を叩き出し、だからデータベースは律儀にfsyncを呼びます。

連載「Windows I/Oの深層」の第4回は、この間に立つキャッシュマネージャーです。第2回で「キャッシュに載っていれば非同期I/Oも同期完了する」と書き、第1回では「IRPを作らない近道(ファストI/O)」を宿題にしました。今回その伏線をすべて回収します。

1. まず結論

  • Windowsのファイルキャッシュはライトバック方式です。読みはまずシステムファイルキャッシュから、書きもまずキャッシュへ。ディスクへの反映はOSが後から行います。1
  • キャッシュの実体はファイルマッピングです。キャッシュマネージャーはファイルの256KB単位の区間をシステムのアドレス空間にマップし、読み書きは「そのビューとの間のメモリコピー」になります(2章)。1
  • 書き込みは毎秒の遅延書き込み(lazy writer)が後追いで反映します。アプリのクラッシュではデータは失われませんが、電源断やOSクラッシュではダーティなキャッシュが失われます(4章)。1
  • 「確実に書く」道具は3つ。FlushFileBuffers(=.NETの Flush(true))、FILE_FLAG_WRITE_THROUGHFILE_FLAG_NO_BUFFERING。頻繁な書き込みでの毎回フラッシュは非効率で、公式ドキュメントはNO_BUFFERING+WRITE_THROUGHの併用を挙げています(5章)。21
  • NO_BUFFERINGには整列要件があります。サイズ・オフセットはセクターサイズの整数倍、バッファアドレスも物理セクター境界に整列。そしてNO_BUFFERINGでもメタデータはキャッシュされ続けます(5.3節)。31
  • マップビューとキャッシュは同じデータを共有します。メモリマップトファイルと通常のキャッシュI/Oは一貫しており、マップの永続化は FlushViewOfFile+FlushFileBuffers の2段です(6章)。45
  • キャッシュに乗った同期の読み書きは、IRPすら作らないことがあります。ファストI/Oという近道がキャッシュマネージャーへ直行します──第1回の宿題の答えです(7章)。6

この記事の知識マップ

WriteFileの成功はディスクへの永続化を意味せず、Windowsのファイルキャッシュはまずシステムキャッシュビューへコピーし、ディスクへの反映はlazy writerが毎秒後追いします。電源断やOSクラッシュではまだ書かれていないダーティページが失われるため、確実に書きたいデータにはFlushFileBuffers・FILE_FLAG_WRITE_THROUGH・FILE_FLAG_NO_BUFFERINGを使い分ける必要があります。

キャッシュマネージャーとライトビハインドの知識マップキャッシュマネージャー、ライトバックキャッシュ、256KBのシステムキャッシュビュー、先読み、lazy writerによる遅延書き込み、電源断でのダーティページ喪失、FlushFileBuffers・FILE_FLAG_WRITE_THROUGH・FILE_FLAG_NO_BUFFERINGによる確実な書き込み、整列要件、メモリマップトファイルとの一貫性、ファストI/Oの関係を示す図実装を担う利用する利用する利用する利用する利用する自動化する軽減する両立しない原因になり得るに保存される原因になり得る防止する利用するで構成できるで構成できる防止する防止する軽減する前提とする防止する原因になり得る用いるのは非推奨推奨される対応推奨される対応両立しない前提とするより先に行うべきで確認できる前提とする両立しない前提とするキャッシュマネージャーライトバックキャッシュ(遅延書き込み方式)ファイルマッピング(メモリマップトファイル)システムキャッシュビュー(256KBスロット)ファイルオブジェクトlazy writer(遅延書き込みスレッド)未反映データの喪失FILE_ATTRIBUTE_TEMPORARYダーティページ電源断・OSクラッシュ先読み(read-ahead)FILE_FLAG_SEQUENTIAL_SCANFILE_FLAG_RANDOM_ACCESSFlushFileBuffersFILE_FLAG_WRITE_THROUGHFILE_FLAG_NO_BUFFERINGセクター整列要件ERROR_INVALID_PARAMETER(87)頻繁な書き込みでの確実な永続化FlushViewOfFileファストI/OProcess Monitor(procmon.exe)IRP(I/O要求パケット)同期I/O

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全32件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. キャッシュの正体 ── ファイルはメモリにマップされる

2.1. 256KBのスロットとメモリコピー

Windowsのファイルキャッシュを「ディスクブロックの入れ物」と考えると、いろいろな挙動が説明できなくなります。正しい絵はこうです──キャッシュマネージャーは、ファイルの256KB単位の区間をシステムアドレス空間の「スロット」にマップし、キャッシュ有効の読み書きはそのスロットとアプリのバッファの間のメモリコピーとして実行されます1

システムアドレス空間アプリ(ユーザーモード)ReadFile/WriteFile =スロットとの間のメモリコピー初回アクセス時の読み込みと後追いの書き戻しはページ単位システムファイルキャッシュファイルの256KB区間をマップしたスロットアプリのバッファ(ReadFile/WriteFileに渡した領域)ディスク上のファイル

図1: キャッシュ有効I/Oの実像。アプリから見た「ファイルの読み書き」は、多くの場合ただのメモリコピー

誤解しやすい点をひとつ。256KBはビュー(マップ)の粒度であって、ディスクI/Oが常に256KB単位で行われるという意味ではありません。スロット内のページは必要に応じて読み込まれ、実際にディスクへ飛ぶI/Oの量は要求サイズやアクセスパターンで変わります。初めて読む区間なら、充填のためにディスクI/Oが発生します(ここで第1回のIRPが下位のストレージスタックへ飛びます)。すでにキャッシュにあれば、読み取りはコピーだけで完了します。第2回5章で見た「キャッシュヒットだと非同期発行しても同期完了する」のは、この「即答できる要求はその場で完了させる」動きの現れでした。逆に、キャッシュ有効のままページがメモリに無い場合、ページフォールト処理に非同期の仕組みがないため非同期読み取りが同期的に処理されることがある──という罠も第2回で見たとおりです。7

2.2. 「空きメモリが減った」の正体

キャッシュを使うかどうかや先読みの状態は開き方ごと(ファイルオブジェクト単位)に管理されますが1キャッシュされたデータ本体はファイル(ストリーム)単位で共有されます。同じファイルを何度開いても、別々のキャッシュができるわけではなく、どのハンドルからも同じキャッシュ内容が見えます(6章の一貫性の土台です)。キャッシュはWindowsが動いている間ずっとキャッシュマネージャーの指揮下で動きます。1 大きなファイルをコピーしたり大量に読み書きすれば、空いている物理メモリはどんどんキャッシュに転用されます。タスクマネージャーの空きメモリが減って見えても、その多くは「アプリが要求すればすみやかに明け渡される、価値ある使われ方をしている待機中のメモリ」です。メモリ不足の診断でこの区別を誤らないために、観測の実務は「.NETでGC待ちとメモリリークを見分ける」でも扱いました。

この動きは画面で確かめられます。タスクマネージャー > パフォーマンス > メモリ を開くと、下側の「メモリの構成」バーが 使用中 / 変更済み / スタンバイ / 空き に区切られています。ファイルキャッシュの大半はこのスタンバイに入り、右側の一覧では「キャッシュ済み」として合計されています。もっと細かく見るなら リソースモニター > メモリ タブ で、同じ区分が容量つきで並びます。数GBのファイルを1本コピーしてから見直すと、スタンバイが増えて空きが減るのに、「使用中」はほとんど変わらない──つまり「メモリが食い潰された」のではなく「空いていたメモリがキャッシュに使われた」ことが目で確認できます。

3. 先読み ── 読みの投機

キャッシュマネージャーは、過去のアクセスパターンから次に読まれそうな区間を先回りして読み込みます(read-ahead)。順番に読んでいるファイルなら、アプリが要求する前に続きのデータがもうキャッシュに載っている──これがシーケンシャル読みの速さの種明かしです。先読みの量は固定ではなく、検出したパターンや要求サイズに応じて変わります。

アプリの読み取り要求の履歴先頭から順番に読んでいるキャッシュマネージャーがパターンを検出先読み: 続きの区間を要求される前に読み込んでおく(量はパターンと要求サイズに応じて可変)ヒント FILE_FLAG_SEQUENTIAL_SCAN= 先読みを積極的にヒント FILE_FLAG_RANDOM_ACCESS= 先読みは無駄になるので抑える

図2: 先読み。アクセスパターンの検出に加えて、CreateFileのフラグでヒントを与えられる

第1回の対応表に載せた FileOptions.SequentialScan / RandomAccess は、この先読みエンジンへのヒントです。「全部なめる」バッチ処理には前者、インデックスを辿るようなアクセスには後者──アプリしか知らない未来を、OSに教えるためのフラグだと考えると使いどころが明確になります。

4. 遅延書き込み ── WriteFileの「成功」の意味

4.1. lazy writerは毎秒やってくる

書き込み側はライトバックキャッシュです。WriteFile はデータをスロットへコピーした時点で成功を返し、ディスクへの反映は後回しにされます。この「遅らせて書く」方針が遅延書き込み(lazy writing)です。1

反映を担うのが、キャッシュマネージャーが毎秒起動するlazy writerです。最近フラッシュされていないページの8分の1をキューに積んで書き出し、書くべきデータが多ければさらに積み増します。なお、FILE_ATTRIBUTE_TEMPORARY 属性付きで作られた一時ファイルはlazy writerのフラッシュ対象から外されます──すぐ消される前提のものを書くだけ無駄だからです。1 ただしこれは属性によるヒントであって、メモリが逼迫すれば書き戻されることはありますし、「名前が一時っぽいだけ」のファイルには適用されません。

ディスクlazy writer(毎秒起動)システムキャッシュアプリディスクlazy writer(毎秒起動)システムキャッシュアプリスロットへコピーしページをダーティ(未書き込み)にここから書き戻しまでが「危険な窓」電源断・OSクラッシュならこのデータは消えるここで初めて永続化されるWriteFile(データ)すぐTRUEが返るダーティページの1/8を選ぶまとめて書き戻す

図3: 遅延書き込み。WriteFileの成功は「OSに引き渡した」であって「永続化された」ではない

4.2. 何が起きたら、どこまで消えるのか

「危険な窓」の意味を正確にしておきます。障害の種類で運命が分かれます。

WriteFile成功直後のデータ(キャッシュ上のダーティページ)何が起きたかアプリのプロセスがクラッシュ/強制終了OSごと停止(電源断・ブルースクリーン)データは残るキャッシュはOSのものなのでlazy writerが予定どおり書き戻すダーティページは失われるディスクに届いていた分だけが残る

図4: 障害の種類と生存の分かれ目。キャッシュは「プロセスの持ち物」ではなく「OSの持ち物」

  • アプリが死んでもデータは消えません。キャッシュへのコピーが済んだ時点で、データの所有者はOSです。「保存直後にアプリが落ちたのにファイルは無事だった」のはこのおかげです。
  • OSごと死ぬとダーティ分は消えます。フラッシュの頻度は性能と信頼性のトレードオフとして調整されており、「突然の電源喪失が起これば、キャッシュされたデータは失われる」とドキュメントも明記しています。1

つまり業務アプリの設計での問いは、「このデータは、電源断の瞬間に失われてよいか」です。ログの数秒分なら許せるかもしれません。受注データの確定レコードなら許せないでしょう。許せないものにだけ、次章の道具を使います。

5. 「確実に書いた」を作る道具箱

5.1. FlushFileBuffers ── 今すぐ書き切れ

FlushFileBuffers は、指定したファイルのバッファ済みデータをデバイスへ書き切ります。ファイルシステムのメタデータは常にキャッシュされるため、メタデータまで確実に届けるにはフラッシュ(またはWRITE_THROUGH)が必要、という点も押さえどころです。12 .NETでは FileStream.Flush(true) がこれに相当します(Flush() だけでは.NET内部のバッファをOSへ渡すだけで、OSのキャッシュはそのままです)。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…)。
  • バッファのアドレスも物理セクターサイズに整列していること(4096バイト物理セクターの「Advanced Format」ディスクへの配慮も必要)。
  • それでもメタデータはキャッシュされ続けるので、完全な永続化にはWRITE_THROUGHの併用か FlushFileBuffers が要ります。12

この「約束事」は、フラグを足すだけで済ませようとした人が最初に転ぶところです。整列を守らないまま読み書きすると、ERROR_INVALID_PARAMETER(87) で失敗します。守るべき3点を整理します。3

揃えるもの 条件 どう満たすか
読み書きのサイズ ボリュームのセクターサイズの整数倍 GetDiskFreeSpacelpBytesPerSector を取得して、その倍数に丸める
ファイルオフセット 同上(OVERLAPPEDOffset で指定する場合も同じ) セクターサイズの倍数ずつ進める
バッファのアドレス 物理セクターサイズに整列 VirtualAlloc で確保する(ページ境界=通常4096バイトに整列した領域が返る)

3つ目が特に見落とされます。mallocnew、C#の配列が返すアドレスには、セクター境界への整列の保証がありません。ページ境界に確保できる VirtualAlloc を使えば、物理セクター4096バイトの「Advanced Format」ディスクの要件も同時に満たせます。最小の形はこうなります。

// C++ / Win32。エラー処理は最小限にしてあります
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", &sectorsPerCluster, &bytesPerSector,
                       &freeClusters, &totalClusters))
{
    return GetLastError();
}

// 読み書きの単位をセクターサイズの整数倍にする(ここでは1MiB相当)
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)
{
    // buffer の先頭 read バイトを処理する
    // (ファイル末尾では read < chunk になる。これは正常)
}

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

なお、.NETの FileOptions には 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

選ぶ順番は、「電源断で何件まで失ってよいか」を先に決め、次に「そのためにどれだけ遅くなってよいか」を確認する、の2段です。表を上から眺めるのではなく、この分岐をたどってください。

許される(直近数秒のログなど)許されない節目(取引の確定など)1件ごといいえ(通常のアプリ)はい(DBエンジン等)このデータを書こうとしている電源断・ブルースクリーンの瞬間に失われても許されるか既定のまま(キャッシュ有効)いちばん速い。ほとんどのI/Oはここ失えないのは「節目」か「1件ごと」か節目で FlushFileBuffers.NETなら Flush(true)コスト: 節目の待ちだけ自前でバッファを管理し5.3の整列要件を満たせるかFILE_FLAG_WRITE_THROUGH書き込みごとに即ディスクへ読みはキャッシュのまま速いFILE_FLAG_NO_BUFFERING+ FILE_FLAG_WRITE_THROUGH公式が挙げる「頻繁な永続化」の形

図6: 道具の選び方。1段目の分岐が信頼性の要求、2段目が払えるコスト。「1件ごとに FlushFileBuffers」という道が無いのは、5.1で見たとおり公式ドキュメントがそれを非効率としているためです

実務のパターンも3つだけ挙げておきます。

  1. 「一時ファイルに書く→フラッシュ→リネーム」が、壊れかけファイルを残さない定石です。中身を書き切ってから名前で確定する──この原子的な受け渡しは「ファイル連携の排他制御の基礎知識」で詳しく扱いました。
  2. データベースに任せるのも立派な設計です。SQLiteがWALとフラッシュで耐久性を作り込んでいる話は「C#でSQLiteを業務アプリに使う」を参照してください。「自分でフラッシュ戦略を書かない」という選択肢は常にあります。
  3. ベンチマークはキャッシュを疑う。「読みが速すぎる」測定はたいてい2回目以降のキャッシュヒットを測っています。測定の作法は「Windowsでプログラムのバージョン別速度を正しく比較する方法」にまとめています。

なお図5の最後の一段──ディスク装置内のキャッシュも忘れないでください。FlushFileBuffers はそこまで含めた書き切りを求めますが、USBメモリや外付けディスクでは装置側の書き込みキャッシュポリシー(「クイック取り外し」と「高パフォーマンス」)が絡みます。取り外し可能デバイスの扱いは「WindowsアプリでUSB機器を扱う方法」も参照してください。

6. メモリマップトファイルとの一貫性

第1回で「キャッシュの実体はファイルマッピング」と聞いて、こう思った方がいるはずです──では自分で MapViewOfFile したビューと、ReadFile/WriteFile のキャッシュは喧嘩しないのか?

しません。同じ仕組みの上に乗っているからです。ファイルマッピングオブジェクトはファイルに裏打ちされ、ページの追い出しはファイルへの書き戻しとして行われます。同じローカルファイルに対して複数プロセスがビューを作っても、見える内容はコヒーレント(一貫)です。4

システムアドレス空間プロセスAのアドレス空間キャッシュマネージャーのビュー(ReadFile/WriteFileが使うスロット)MapViewOfFileのビュー同じ物理ページ群(ファイルに裏打ちされたメモリ)ディスク上のファイルFILE_FLAG_NO_BUFFERINGのI/Oはこの共有の枠外(直接ディスクへ)

図7: マップビューもキャッシュも、同じ「ファイルに裏打ちされたページ」を見ている。枠外にいるのはNO_BUFFERINGだけ

注意点は2つです。

  • FILE_FLAG_NO_BUFFERING のI/Oはこの一貫性の枠外です。キャッシュを経由しない読み書きと、マップビュー/キャッシュ経由の内容は突き合わされません。混ぜるなら自分で整合を取る必要があります。
  • マップビューの永続化は2段構えです。FlushViewOfFile は範囲内のダーティページの書き出しを開始しますが、メタデータは書かず、ディスク装置のキャッシュからの物理書き込みも待ちません。確実に届けるには FlushViewOfFile の後に FlushFileBuffers を呼びます。5

共有メモリとしてのファイルマッピングの実務(名前付き共有、同期、事故パターン)は「共有メモリの落とし穴と実務ベストプラクティス」で扱っています。

7. ファストI/O ── 第1回の宿題回収

先に2行で。ファストI/Oとは、キャッシュに載っているファイルへの同期の読み書きのために用意された近道で、IRP(I/O要求パケット。カーネルがドライバーに渡す要求の入れ物)を組み立てずにキャッシュと直接データをやり取りします。ProcmonのOperation列に IRP_MJ_READFASTIO_READ が混ざって出るのは、同じ「読み取り」が通常経路と近道のどちらを通ったかの違いです。

第1回5.2節で「すべてのI/OがIRPになるわけではない」と書きました。答え合わせです。

キャッシュに乗っているファイルの読み書きは、IRPを組み立ててデバイススタックを流すまでもなく、キャッシュとのメモリコピーで済むことが分かっています。そこでWindowsは、キャッシュされたファイルへの同期I/OのためにファストI/Oという近道を用意しています──IRPを作らず、ファイルシステムの「ファストI/Oエントリポイント」を直接呼び、キャッシュマネージャーから直接コピーする経路です。6 ファストI/Oで処理できない場合(キャッシュに無い、ロックが絡む、フィルターが割り込む等)は、通常のIRP経路へ折り返します。なお、これは同期の要求のための高速経路であって「キャッシュヒット=常にファストI/O」ではありません。非同期(FILE_FLAG_OVERLAPPED)ハンドルの操作は、キャッシュからその場で完了する場合(第2回5章)でもIRP経路で処理されることがあります。

できるできないキャッシュ有効ハンドルへの同期的な読み書きファストI/Oで処理できるか(キャッシュに載っている等)ファストI/OIRPを作らずキャッシュと直接コピーProcmonではFASTIO_と表示通常経路IRPを組み立ててデバイススタックへ(第1回の図6の世界)

図8: ファストI/Oの分岐。Procmonで FASTIO_READIRP_MJ_READ が混ざって見えるのはこのため

第1回7章のProcmon観察で FASTIO_ の行が混ざっていた理由が、これで説明できます。キャッシュヒットの読み取りは、IRPすら贅沢品なのです。この経路の存在は、第6回で扱うフィルタードライバーにも影響します(ミニフィルターはファストI/Oにも割り込めるようになっています)。

8. まとめ

  • Windowsのファイルキャッシュはライトバックで、実体はファイルの256KB区間のマッピングです。キャッシュ有効の読み書きはスロットとのメモリコピーになります。1
  • 読みは先読みが投機し、SequentialScan/RandomAccess はそのヒントです。1
  • 書きは毎秒のlazy writerが後追いします。アプリが死んでもデータは残り、OSごと死ぬとダーティ分だけが消えます。設計の問いは「このデータは電源断の瞬間に失われてよいか」です。1
  • 確実に書く道具は FlushFileBuffers(節目の確定)/WRITE_THROUGH(書き込みごと)/NO_BUFFERING(キャッシュ非経由+整列要件)。毎回フラッシュは非効率で、頻繁な永続化にはNO_BUFFERING+WRITE_THROUGH併用が公式の推奨です。メタデータが常にキャッシュされる点にも注意。231
  • マップビューとキャッシュは同じページを共有し一貫します。枠外はNO_BUFFERINGのみ。マップの永続化は FlushViewOfFile+FlushFileBuffers の2段です。45
  • キャッシュヒットの同期の読み書きはファストI/OでIRPすら省略されます。第1回のProcmonで見た FASTIO_ の正体です。6

続きは第5回「NTFSの内部構造 ── MFTから理解するファイルシステム」です。今回まではファイルを「オフセットとバイト列」として扱ってきましたが、その裏でNTFSがどうデータを配置しているのか──MFT、複数データストリーム、ジャーナル、ハードリンク──ディスクの上の静的な構造へ降りていきます。

関連記事

関連する相談領域

合同会社小村ソフトでは、「保存したはずのデータが消えた」「ファイル書き込みが遅い/速すぎて怪しい」といったWindows業務アプリのファイルI/Oの設計・不具合調査を扱っています。

参考リンク

  1. Microsoft Learn, File Caching. Windowsが既定でファイルデータをキャッシュし、読み取りがシステムファイルキャッシュから行われ、書き込みもキャッシュへ行われるライトバックキャッシュであること、キャッシュがファイルオブジェクト単位で管理されキャッシュマネージャーの指揮下で動くこと、ディスクへの書き込みを遅らせてキャッシュに保持する方針が遅延書き込み(lazy writing)と呼ばれること、ファイル読み取り時に256KBの区間がシステムアドレス空間の256KBスロットに読み込まれ、ユーザープロセスがそのスロットとの間でデータをコピーすること、キャッシュマネージャーが毎秒lazy writerを起動し、最近フラッシュされていないページの8分の1をディスク書き込みのキューに積み、必要ならさらに積み増すこと、一時ファイルはフラッシュされないこと、電源喪失のような突然のシステム障害が起これば書かれていないキャッシュデータは失われること、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が通常は内部バッファへ書き、OSが定期的にディスクへ書き出すこと、FlushFileBuffersが指定ファイルのバッファ済み情報をすべてデバイスへ書き出すこと、多数の書き込みのたびに毎回呼ぶのは非効率であり、頻繁な書き込みで重要データの永続化が必要なアプリはFILE_FLAG_NO_BUFFERINGとFILE_FLAG_WRITE_THROUGHによる非バッファI/Oを使うべきこと、ボリュームハンドルに対して呼べば(管理者権限で)ボリューム上の全オープンファイルをフラッシュできることについて。  2 3 4 5

  3. Microsoft Learn, File Buffering. FILE_FLAG_NO_BUFFERINGで開いたファイルへのアクセス要件として、読み書きのサイズとファイルオフセット(OVERLAPPEDで指定する場合を含む)がボリュームのセクターサイズの整数倍でなければならないこと、読み書きバッファのアドレスが物理セクターサイズに整列しているべきこと、物理セクター4,096バイトのAdvanced Formatデバイスへの考慮が必要なことについて。  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. ファストI/OがIRPを生成せずにファイルシステムやキャッシュマネージャーのエントリポイントを直接呼ぶ、キャッシュされたファイル向けの同期I/Oの高速経路であること、キャッシュから直接ユーザーバッファへ(またはその逆へ)データが転送されること、ファスト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()がストリームの内部バッファをOSへ書き出すこと、Flush(true)を指定するとそれに加えてすべての中間ファイルバッファ(OSのバッファ)もフラッシュされることについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

Windows I/Oの深層(第6回・最終回) ── フィルタードライバーとミニフィルター:ProcmonとウイルススキャンがI/Oに割り込める理由

Windowsのフィルタードライバーとミニフィルターを図解で解説する連載の最終回です。フィルターマネージャーとアルティチュード、pre/postコールバック、Procmonやウイルス対策が全I/Oを検査できる仕組み、「あの環境だけ遅い」の調査手順まで整理します。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

WriteFileが成功を返した時点で、データはディスクに書かれていますか?
既定では書かれていません。Windowsのファイルキャッシュはライトバック方式で、WriteFileはデータをシステムファイルキャッシュにコピーした時点で成功を返します。ディスクへの書き込みは、キャッシュマネージャーが毎秒起動する遅延書き込み(lazy writer)が後から行います。重要なのは障害の種類による差です。アプリのプロセスがクラッシュしても、キャッシュに入ったデータはOSが生きている限り後で書かれるので失われません。一方、OSごと落ちる障害(電源断、ブルースクリーン)では、まだ書かれていないダーティなキャッシュは失われます。「WriteFileが成功した=永続化された」ではなく、「成功した=OSに引き渡した」と理解するのが正確です。
確実にディスクへ書き込むにはどうすればよいですか?
3つの道具があります。第一にFlushFileBuffersで、そのファイルのバッファ済みデータとメタデータをデバイスへ書き切ります(.NETならFileStream.Flush(true)がこれに相当します)。第二にFILE_FLAG_WRITE_THROUGHで、書き込みのたびにキャッシュへ書きつつ即座にディスクへも書きます。第三にFILE_FLAG_NO_BUFFERINGで、キャッシュ自体を経由しません。マイクロソフトのドキュメントは、書き込みのたびにFlushFileBuffersを呼ぶのは非効率で、頻繁な書き込みで確実な永続化が必要ならFILE_FLAG_NO_BUFFERINGとFILE_FLAG_WRITE_THROUGHの併用を使うべきとしています。どれもキャッシュの恩恵を手放すぶん遅くなるので、「全部に付ける」のではなく、失えないデータの書き込みに絞って使うのが実務の勘所です。
FILE_FLAG_WRITE_THROUGHとFILE_FLAG_NO_BUFFERINGはどう違いますか?
WRITE_THROUGHは「キャッシュには書くが、完了前にディスクにも書く」です。読み取りは引き続きキャッシュの恩恵を受けられ、遅延書き込みの遅延だけを取り除きます。NO_BUFFERINGは「読み書きがシステムキャッシュを経由しない」で、読みも書きも毎回ディスク装置へのI/Oになります(ただし迂回するのはWindowsのキャッシュまでで、装置内の書き込みキャッシュまで飛ばすわけではありません)。その代わり厳しい制約が付きます。読み書きのサイズとファイルオフセットはボリュームのセクターサイズの整数倍でなければならず、バッファのアドレスも物理セクター境界に整列させる必要があります。また、NO_BUFFERINGでもファイルシステムのメタデータはキャッシュされ続けるため、メタデータまで確実に書くにはFlushFileBuffersかWRITE_THROUGHの併用が必要です。データベースエンジンのように自前でバッファ管理をするソフトウェアが使うのが典型で、通常のアプリではまずWRITE_THROUGHやFlushFileBuffersから検討するのが順当です。
タスクマネージャーで空きメモリが少なく見えるのは、ファイルキャッシュのせいですか?
多くの場合そうで、しかも正常な動作です。Windowsは空いている物理メモリを積極的にファイルキャッシュとして使い、大きなファイルコピーや大量の読み書きをすれば、その分キャッシュが膨らんでメモリ使用量が増えて見えます。ただしキャッシュが使っているページの多くは、アプリがメモリを要求すれば比較的すみやかに転用される種類のもので、「メモリが食い潰されて足りない」状態とは区別が必要です。メモリ不足を疑う際は、見かけの空き容量だけでなく、コミット済みメモリやハードフォールトの頻度といった指標を見るのが実務的です。
メモリマップトファイルとReadFile/WriteFileで同じファイルを触ると、内容はずれませんか?
通常のキャッシュ有効I/Oとの間ではずれません。Windowsのキャッシュ自体がファイルマッピングで実装されており、同じローカルファイルに対するマップビューとキャッシュは同じデータを共有するため、一方の変更はもう一方からも見えます。複数プロセスが同じファイルマッピングオブジェクトからビューを作った場合もデータはコヒーレント(一貫)です。ただし、FILE_FLAG_NO_BUFFERINGで開いたハンドルの読み書きはキャッシュを経由しないため、この一貫性の枠外になります。またマップビューの変更を確実にディスクへ書くには、FlushViewOfFileだけではメタデータが書かれずハードウェアのキャッシュも待たないため、FlushViewOfFileの後にFlushFileBuffersを呼ぶ必要があります。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る