更新履歴(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)
以下のDOIは過去に登録されたアーカイブを指しており、現在の本文とは一致しない場合があります。現在の本文を参照するときは、このページのURLを使用してください。
小村 豪(2026)「Windows I/Oの深層(第4回) ── キャッシュマネージャー:あなたのWriteFileはいつディスクに届くのか」合同会社小村ソフト. https://comcomponent.com/blog/windows-cache-manager-writefile-disk/
- DOI(登録済みアーカイブ)
- 10.5281/zenodo.21739474
- DOI(前回登録した版)
- 10.5281/zenodo.21739475
WriteFile が成功を返しました。さて、データはいまどこにあるでしょうか。
通常のキャッシュ有効の書き込みでは、まずOSのメモリ上のキャッシュへデータが渡されます。WriteFile の成功は「OSに引き渡した」であって、「ディスクへ永続化した」ではありません。 ディスクへの反映は、OSが後から行います。1
この違いを知ると、「保存したはずなのに電源断で消えた」「ファイルコピーの測定値がディスクの性能より速い」といった現象を説明できます。データベースが fsync などで明示的に書き出すのも、書き込みの受付と永続化を区別するためです。
本記事は、連載「Windows I/Oの深層」の第4回です。アプリとディスクの間にあるキャッシュマネージャーを取り上げ、キャッシュの仕組み、書き出しのタイミング、失えないデータの保存方法を順に整理します。
あわせて、第2回の「キャッシュに載っていれば非同期I/Oも同期完了する理由」と、第1回の「IRPを作らない近道であるファストI/O」を説明します。
1. まず結論
- 既定の書き込みは、キャッシュへのコピーとディスクへの反映が別です。 Windowsはライトバック方式を使い、lazy writerが後から書き出します。OSのキャッシュへ渡ったデータはアプリだけのクラッシュでは残りますが、電源断やOSクラッシュでは未反映のダーティページが失われます。1
- 失えないデータには、保存の区切りに合う方法を選びます。 節目で確定するなら
FlushFileBuffers(.NETではFileStream.Flush(true))、書き込みごとの遅延をなくすならFILE_FLAG_WRITE_THROUGHが基本です。FILE_FLAG_NO_BUFFERINGはシステムキャッシュを迂回する別の道具で、整列要件があり、装置内キャッシュやメタデータへの配慮も残ります。頻繁な永続化にはNO_BUFFERINGとWRITE_THROUGHの併用が公式に挙げられています。231 - 読み取りの速さやI/O経路も、キャッシュの仕組みから理解できます。 キャッシュの実体はファイルマッピングで、読み取りには先読みが働きます。マップビューとの一貫性と永続化は別に考え、ファストI/OはIRPを省略できる場合の近道として捉えます。456
目的に応じて、次の節から読めます。
| 知りたいこと | 読む節 |
|---|---|
| キャッシュの実体、空きメモリが減る理由、先読み | 2章・3章 |
| WriteFile成功後、障害で何が失われるか | 4章 |
| Flush・WRITE_THROUGH・NO_BUFFERINGの使い分けと整列要件 | 5章 |
| マップビューの一貫性と、2段階のフラッシュ | 6章 |
| キャッシュヒットとファストI/Oの違い | 7章 |
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全32件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. キャッシュの正体 ── ファイルはメモリにマップされる
2.1. 256KBのスロットとメモリコピー
キャッシュの実体は、ファイルの一部をメモリ上にマップしたビューです。キャッシュマネージャーは、ファイルの256KB単位の区間をシステムアドレス空間の「スロット」にマップします。キャッシュ有効の読み書きは、そのスロットとアプリのバッファの間のメモリコピーとして実行されます。1
flowchart TB
subgraph U["アプリ(ユーザーモード)"]
BUF["アプリのバッファ<br/>(ReadFile/WriteFileに渡した領域)"]
end
subgraph S["システムアドレス空間"]
SLOT["システムファイルキャッシュ<br/>ファイルの256KB区間をマップしたスロット"]
end
DISK[("ディスク上のファイル")]
BUF <-->|"ReadFile/WriteFile =<br/>スロットとの間のメモリコピー"| SLOT
SLOT <-->|"初回アクセス時の読み込みと<br/>後追いの書き戻しはページ単位"| DISK
図1: キャッシュ有効I/Oの実像。アプリから見た「ファイルの読み書き」は、多くの場合ただのメモリコピー
256KBは、ディスクへ読み書きする単位ではない
256KBはビュー(マップ)の粒度であって、ディスクI/Oが常に256KB単位になるという意味ではありません。 スロット内のページは必要に応じて読み込まれ、実際のI/O量は要求サイズやアクセスパターンで変わります。
初めて読む区間なら、キャッシュを満たすためにディスクI/Oが発生し、第1回で見たIRPが下位のストレージスタックへ進みます。すでにキャッシュにあれば、読み取りはメモリコピーだけで完了します。
非同期で発行しても、その場で処理されることがある
第2回5章の「キャッシュヒットだと非同期発行しても同期完了する」は、すぐに答えられる要求をその場で完了させる動きです。
逆に、キャッシュ有効でもページがメモリにない場合、ページフォールト処理に非同期の仕組みがないため、非同期読み取りが同期的に処理されることがあります。キャッシュヒットによる即時完了と、ページがないための同期的な処理は、分けて理解します。7
2.2. 「空きメモリが減った」の正体
開き方ごとの設定と、共有されるデータを分ける
キャッシュを使うかどうかや先読みの状態は、開き方ごと(ファイルオブジェクト単位)に管理されます。1 一方、キャッシュされたデータ本体はファイル(ストリーム)単位で共有されます。同じファイルを何度開いても、別々のキャッシュができるわけではありません。各ハンドルが同じキャッシュ内容を見ることが、6章で扱う一貫性の土台です。
キャッシュマネージャーはWindowsが動いている間、継続してキャッシュを管理します。1 大きなファイルのコピーや大量の読み書きでは、空いている物理メモリがキャッシュに使われます。その多くは、アプリがメモリを要求すれば比較的すみやかに転用できる待機中のメモリです。
したがって、空きメモリが減ったことと、メモリが足りなくなったことは同じではありません。 この区別を踏まえた観測の実務は「.NETでGC待ちとメモリリークを見分ける」でも扱っています。
画面では「スタンバイ」と「空き」の変化を見る
タスクマネージャーの「パフォーマンス > メモリ」では、下側の「メモリの構成」バーに 使用中 / 変更済み / スタンバイ / 空き が表示されます。ファイルキャッシュの大半はスタンバイに入り、右側の一覧では「キャッシュ済み」として合計されています。リソースモニターの「メモリ」タブなら、同じ区分を容量付きで確認できます。
数GBのファイルを1本コピーし、前後を見比べてみてください。スタンバイが増えて空きが減る一方、「使用中」はほとんど変わらないという動きから、空いていたメモリがキャッシュに使われたことを確認できます。
3. 先読み ── 読みの投機
キャッシュマネージャーは、過去のアクセスパターンから次に読まれそうな区間を先回りして読み込みます。これが先読み(read-ahead)です。
ファイルを先頭から順番に読む場合、次の要求を出す前に続きのデータがキャッシュへ入っていれば、その分だけ読み取りは速くなります。先読みの量は固定ではなく、検出したパターンや要求サイズに応じて変わります。
flowchart LR
A["アプリの読み取り要求の履歴<br/>先頭から順番に読んでいる"]
D{"キャッシュマネージャーが<br/>パターンを検出"}
R["先読み: 続きの区間を<br/>要求される前に読み込んでおく<br/>(量はパターンと要求サイズに応じて可変)"]
H1["ヒント FILE_FLAG_SEQUENTIAL_SCAN<br/>= 先読みを積極的に"]
H2["ヒント FILE_FLAG_RANDOM_ACCESS<br/>= 先読みは無駄になるので抑える"]
A --> D
D --> R
H1 -.-> D
H2 -.-> D
図2: 先読み。アクセスパターンの検出に加えて、CreateFileのフラグでヒントを与えられる
第1回の対応表に載せた FileOptions.SequentialScan / RandomAccess は、この先読みへのヒントです。アプリが知っているアクセスの予定を、OSへ伝えます。
| アクセスの予定 | Win32のフラグ | .NETの指定 | 先読みへのヒント |
|---|---|---|---|
| ファイル全体を順番に読むバッチ処理 | FILE_FLAG_SEQUENTIAL_SCAN |
FileOptions.SequentialScan |
先読みを積極的に行う |
| インデックスをたどるような不規則なアクセス | FILE_FLAG_RANDOM_ACCESS |
FileOptions.RandomAccess |
無駄になりやすい先読みを抑える |
処理内容に合うヒントを選ぶことが大切で、どちらも先読み量を固定する指定ではありません。
4. 遅延書き込み ── WriteFileの「成功」の意味
4.1. lazy writerは毎秒やってくる
既定の書き込みでは、WriteFile がデータをキャッシュのスロットへコピーした時点で成功を返し、ディスクへの反映は後回しになります。このライトバックキャッシュの方針を、遅延書き込み(lazy writing)と呼びます。1
反映を担うのは、キャッシュマネージャーが毎秒起動する lazy writer です。最近フラッシュされていないページの8分の1をキューに積み、書くべきデータが多ければさらに積み増します。1
ここでいう「毎秒」は処理の起動間隔です。個々の書き込みが1秒以内に永続化されるという保証として読んではいけません。
一時ファイルの属性は、書き戻しを抑えるヒント
FILE_ATTRIBUTE_TEMPORARY 属性付きの一時ファイルは、すぐに削除されることを想定し、lazy writerのフラッシュ対象から外されます。1 ただし、属性はあくまでヒントです。メモリが逼迫すれば書き戻されることはあり、名前が一時ファイルらしいだけでは適用されません。
sequenceDiagram
participant App as アプリ
participant C as システムキャッシュ
participant LW as lazy writer(毎秒起動)
participant D as ディスク
App->>C: WriteFile(データ)
Note over C: スロットへコピーし<br/>ページをダーティ(未書き込み)に
C-->>App: すぐTRUEが返る
Note over App,C: ここから書き戻しまでが「危険な窓」<br/>電源断・OSクラッシュならこのデータは消える
LW->>C: ダーティページの1/8を選ぶ
LW->>D: まとめて書き戻す
Note over D: ここで初めて永続化される
図3: 遅延書き込み。WriteFileの成功は「OSに引き渡した」であって「永続化された」ではない
4.2. 何が起きたら、どこまで消えるのか
WriteFile が成功してから書き戻しが終わるまでには、データがまだキャッシュにある期間が残ります。この間のデータがどうなるかは、アプリだけが止まったのか、OSごと止まったのかで分かれます。
flowchart TB
W["WriteFile成功直後のデータ<br/>(キャッシュ上のダーティページ)"]
Q{"何が起きたか"}
A1["アプリのプロセスが<br/>クラッシュ/強制終了"]
A2["OSごと停止<br/>(電源断・ブルースクリーン)"]
S["データは残る<br/>キャッシュはOSのものなので<br/>lazy writerが予定どおり書き戻す"]
L["ダーティページは失われる<br/>ディスクに届いていた分だけが残る"]
W --> Q
Q --> A1
Q --> A2
A1 --> S
A2 --> L
図4: 障害の種類と生存の分かれ目。キャッシュは「プロセスの持ち物」ではなく「OSの持ち物」
| 障害 | OSのキャッシュに渡ったデータはどうなるか |
|---|---|
| アプリのクラッシュ・強制終了 | キャッシュはOSのものなので、OSが動いていれば後から書き戻される |
| 電源断・OSクラッシュ | まだ書き戻されていないダーティページが失われ、ディスクに届いていた分だけが残る |
「保存直後にアプリが落ちたのにファイルは無事だった」のは、キャッシュへのコピー後はOSがデータを保持しているためです。一方、突然の電源喪失では、未反映のキャッシュデータが失われると公式ドキュメントにも明記されています。フラッシュの頻度は、性能と信頼性のトレードオフとして調整されます。1
設計で先に決めるのは、「このデータは、電源断の瞬間に失われてよいか」です。ログの直近数秒分なら許容できても、受注データの確定レコードは失えないでしょう。失えない書き込みに、次章の方法を使います。
5. 「確実に書いた」を作る道具箱
この章では、今あるバッファを書き出す、書き込みの遅延をなくす、システムキャッシュを迂回するという3つの方法を区別します。最後の5.4節で、信頼性の要求と払えるコストから選ぶ順番をまとめます。
5.1. FlushFileBuffers ── 今すぐ書き切れ
FlushFileBuffers は、指定したファイルのバッファ済みデータをデバイスへ書き出します。ファイルシステムのメタデータは常にキャッシュされるため、メタデータまで届けるにはフラッシュ、またはWRITE_THROUGHが必要です。12
.NETでは Flush() と Flush(true) を区別する
| 呼び出し | 書き出す範囲 |
|---|---|
FileStream.Flush() |
.NET内部のバッファをOSへ渡す。OSのキャッシュのフラッシュは要求しない |
FileStream.Flush(true) |
.NET内部に加え、OSなどの中間ファイルバッファもフラッシュする |
Windowsで FlushFileBuffers に相当する指定は、FileStream.Flush(true)です。単に Flush() を呼ぶ場合との違いを意識してください。8
毎回フラッシュするコストも考える
多数の書き込みのたびに FlushFileBuffers を呼ぶ方法は、非効率になると公式ドキュメントが指摘しています。頻繁な書き込みで毎回の永続化が必要な場合には、後述する 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 は、読み書きからWindowsのシステムキャッシュを外す指定です。読み取りも書き込みも、キャッシュを経由せずディスク装置へのI/Oになります。1
ただし、装置内の書き込みキャッシュは別の段です。NO_BUFFERINGでそこまで迂回できるわけではないため、電源断耐性が必要なら、WRITE_THROUGHの併用や FlushFileBuffers を引き続き検討します。
大量データの一括転送や、自前でバッファ管理を行うデータベースエンジンに向く一方、次の整列要件をアプリ側で満たす必要があります。3
- 読み書きのサイズとファイルオフセットは、ボリュームのセクターサイズの整数倍であること(512バイトセクターなら512・1024・1536…)。
- バッファのアドレスも物理セクターサイズに整列していること(4096バイト物理セクターの「Advanced Format」ディスクへの配慮も必要)。
- それでもメタデータはキャッシュされ続けるので、完全な永続化にはWRITE_THROUGHの併用か
FlushFileBuffersが要ります。12
サイズ・オフセット・アドレスの3点を揃える
フラグを追加するだけでは、NO_BUFFERINGへ切り替えられません。整列要件を守らない読み書きは ERROR_INVALID_PARAMETER(87) で失敗します。 確認するのは次の3点です。3
| 揃えるもの | 条件 | どう満たすか |
|---|---|---|
| 読み書きのサイズ | ボリュームのセクターサイズの整数倍 | GetDiskFreeSpace の lpBytesPerSector を取得して、その倍数に丸める |
| ファイルオフセット | 同上(OVERLAPPED の Offset で指定する場合も同じ) |
セクターサイズの倍数ずつ進める |
| バッファのアドレス | 物理セクターサイズに整列 | VirtualAlloc で確保する(ページ境界=通常4096バイトに整列した領域が返る) |
最小のC++例で、バッファの整列を確認する
特に見落としやすいのが、3つ目のバッファアドレスです。malloc、new、C#の配列が返すアドレスには、セクター境界へ整列する保証がありません。
次の例は、VirtualAlloc でページ境界に整列した領域を確保します。通常の4096バイトのページ境界なら、物理セクター4096バイトのAdvanced Formatディスクの要件も満たせます。サイズとオフセットは、取得したセクターサイズの倍数で進めます。
// C++ / Win32。エラー処理は最小限にしてあります
DWORD sectorsPerCluster = 0, bytesPerSector = 0, freeClusters = 0, totalClusters = 0;
if (!GetDiskFreeSpaceW(L"C:\\", §orsPerCluster, &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でも、整列の管理は省略できない
.NETの FileOptions には、FILE_FLAG_NO_BUFFERING に対応する値がありません。必要なら CreateFile を直接呼びますが、その場合も整列要件は自分で守る必要があります。
検討の順番は、「速くしたいからNO_BUFFERING」ではなく、「自前でバッファ管理をするからNO_BUFFERING」です。
5.4. 使い分けの整理
flowchart TB
A["アプリのバッファ"]
B["システムファイルキャッシュ<br/>(ダーティページ)"]
C["ディスク装置内のキャッシュ"]
D[("不揮発の記録媒体")]
A -->|"既定のWriteFile: ここまでで成功が返る"| B
B -->|"lazy writer(毎秒)/ WRITE_THROUGH(即時)"| C
C -->|"装置のタイミング /<br/>FlushFileBuffersは書き切りを要求"| D
A -.->|"NO_BUFFERINGはキャッシュを飛ばして直行"| C
図5: データの階層と、各道具がどこまで押し込むか。「ディスク装置内のキャッシュ」という最後の一段にも注意
| 方法 | 何が起きるか | 向いている場面 |
|---|---|---|
| 既定(キャッシュ有効) | キャッシュコピーで完了。反映はlazy writer | ほとんどのファイルI/O |
FlushFileBuffers / Flush(true) |
その時点のデータ+メタデータを書き切る | 節目での確定(トランザクションのコミット等) |
FILE_FLAG_WRITE_THROUGH |
書き込みごとに即ディスクへ(読みはキャッシュ) | 失えない書き込みが続くログ・ジャーナル |
FILE_FLAG_NO_BUFFERING(+WRITE_THROUGH) |
キャッシュ非経由。整列要件あり | 自前バッファ管理・大量一括I/O |
先に信頼性を決め、次にコストを確認する
まず 「電源断で何件まで失ってよいか」を決めます。そのうえで、失えないのは取引の確定などの「節目」なのか、それとも「書き込み1件ごと」なのかを分けます。
次に、そのためにどれだけ遅くなってよいか、自前のバッファ管理と整列に対応できるかを確認します。方法の名前から選ぶのではなく、次の分岐に沿って考えてください。
flowchart TB
S["このデータを書こうとしている"]
Q1{"電源断・ブルースクリーンの瞬間に<br/>失われても許されるか"}
A0["既定のまま(キャッシュ有効)<br/>いちばん速い。ほとんどのI/Oはここ"]
Q2{"失えないのは<br/>「節目」か「1件ごと」か"}
A1["節目で FlushFileBuffers<br/>.NETなら Flush(true)<br/>コスト: 節目の待ちだけ"]
Q3{"自前でバッファを管理し<br/>5.3の整列要件を満たせるか"}
A2["FILE_FLAG_WRITE_THROUGH<br/>書き込みごとに即ディスクへ<br/>読みはキャッシュのまま速い"]
A3["FILE_FLAG_NO_BUFFERING<br/>+ FILE_FLAG_WRITE_THROUGH<br/>公式が挙げる「頻繁な永続化」の形"]
S --> Q1
Q1 -->|"許される<br/>(直近数秒のログなど)"| A0
Q1 -->|"許されない"| Q2
Q2 -->|"節目<br/>(取引の確定など)"| A1
Q2 -->|"1件ごと"| Q3
Q3 -->|"いいえ(通常のアプリ)"| A2
Q3 -->|"はい(DBエンジン等)"| A3
図6: 道具の選び方。1段目の分岐が信頼性の要求、2段目が払えるコスト。「1件ごとに FlushFileBuffers」という道が無いのは、5.1で見たとおり公式ドキュメントがそれを非効率としているためです
保存の設計と、性能測定に落とし込む
実務では、次の3つのパターンに結び付けて考えると選びやすくなります。
- 「一時ファイルに書く→フラッシュ→リネーム」が、壊れかけファイルを残さない定石です。中身を書き切ってから名前で確定する──この原子的な受け渡しは「ファイル連携の排他制御の基礎知識」で詳しく扱いました。
- データベースに任せるのも立派な設計です。SQLiteがWALとフラッシュで耐久性を作り込んでいる話は「C#でSQLiteを業務アプリに使う」を参照してください。「自分でフラッシュ戦略を書かない」という選択肢は常にあります。
- ベンチマークはキャッシュを疑う。「読みが速すぎる」測定はたいてい2回目以降のキャッシュヒットを測っています。測定の作法は「Windowsでプログラムのバージョン別速度を正しく比較する方法」にまとめています。
装置内のキャッシュまで含めて考える
図5の最後には、ディスク装置内のキャッシュが残っています。FlushFileBuffers はそこまで含めた書き切りを要求します。
USBメモリや外付けディスクでは、装置側の書き込みキャッシュポリシーである「クイック取り外し」「高パフォーマンス」も関わります。取り外し可能デバイスの扱いは「WindowsアプリでUSB機器を扱う方法」も参照してください。
6. メモリマップトファイルとの一貫性
6.1. マップビューとキャッシュは同じデータを見る
キャッシュの実体がファイルマッピングなら、自分で MapViewOfFile したビューと、ReadFile / WriteFile が使うキャッシュの内容はどうなるのでしょうか。
通常のキャッシュ有効I/Oとマップビューは、同じファイルに裏打ちされたデータを共有します。ファイルマッピングオブジェクトのページが追い出されるときは、変更がファイルへ書き戻されます。同じローカルファイルを、複数プロセスが同じファイルマッピングオブジェクトからビューにした場合も、見える内容はコヒーレント(一貫)です。4
flowchart TB
subgraph P1["プロセスAのアドレス空間"]
V1["MapViewOfFileのビュー"]
end
subgraph SYS["システムアドレス空間"]
SC["キャッシュマネージャーのビュー<br/>(ReadFile/WriteFileが使うスロット)"]
end
PAGES["同じ物理ページ群<br/>(ファイルに裏打ちされたメモリ)"]
DISK[("ディスク上のファイル")]
V1 --> PAGES
SC --> PAGES
PAGES --> DISK
NB["FILE_FLAG_NO_BUFFERINGのI/Oは<br/>この共有の枠外(直接ディスクへ)"]
NB -.-> DISK
図7: マップビューもキャッシュも、同じ「ファイルに裏打ちされたページ」を見ている。枠外にいるのはNO_BUFFERINGだけ
6.2. 一貫性と永続化は別に確認する
NO_BUFFERINGのI/Oは、この一貫性の枠外です。 キャッシュを経由しない読み書きと、マップビューやキャッシュ経由の内容は突き合わされません。混在させるなら、アプリ側で整合を取る必要があります。
また、マップビューに変更が見えていることと、その変更がディスクへ永続化されたことは別です。マップビューの永続化は、次の順序で行います。5
| 順序 | 呼び出し | 役割と注意点 |
|---|---|---|
| 1 | FlushViewOfFile |
範囲内のダーティページの書き出しを開始する。メタデータは書かず、装置内キャッシュからの物理書き込み完了も待たない |
| 2 | FlushFileBuffers |
ファイルのメタデータと装置内キャッシュまで含めた書き切りを要求する |
共有メモリとしてのファイルマッピングの実務(名前付き共有、同期、事故パターン)は「共有メモリの落とし穴と実務ベストプラクティス」で扱っています。
7. ファストI/O ── 第1回の宿題回収
ファストI/Oは、キャッシュされたファイルへの同期の読み書きで、IRPを作らずに処理する近道です。IRPは「I/O要求パケット」のことで、カーネルがドライバーへ渡す要求を入れる構造です。6
7.1. IRPを省略できる場合と、通常経路へ戻る場合
第1回5.2節の「すべてのI/OがIRPになるわけではない」という説明は、この経路を指しています。
キャッシュとのメモリコピーだけで処理できる読み書きなら、IRPを組み立ててデバイススタックへ流す必要はありません。ファストI/Oでは、ファイルシステムのエントリポイントを直接呼び、キャッシュマネージャーとデータをやり取りします。6
ただし、キャッシュにない、ロックが関わる、フィルターが介入するなどの理由でファストI/Oが使えない場合は、通常のIRP経路へ戻ります。
「キャッシュヒットなら常にファストI/O」ではない点にも注意してください。非同期(FILE_FLAG_OVERLAPPED)ハンドルの操作は、キャッシュからその場で完了する場合でも、IRP経路で処理されることがあります。第2回5章の「その場で完了するか」と、この章の「IRPを省略するか」は別の話です。
flowchart TB
REQ["キャッシュ有効ハンドルへの同期的な読み書き"]
Q{"ファストI/Oで処理できるか<br/>(キャッシュに載っている等)"}
FAST["ファストI/O<br/>IRPを作らずキャッシュと直接コピー<br/>ProcmonではFASTIO_と表示"]
IRP["通常経路<br/>IRPを組み立ててデバイススタックへ<br/>(第1回の図6の世界)"]
REQ --> Q
Q -->|できる| FAST
Q -->|できない| IRP
図8: ファストI/Oの分岐。Procmonで FASTIO_READ と IRP_MJ_READ が混ざって見えるのはこのため
7.2. 観察では、同じ読み取りの経路を区別する
第1回7章のProcmon観察で FASTIO_ の行が混ざっていたのは、同じ読み取りでも通る経路が異なるためです。FASTIO_READ と IRP_MJ_READ は、ファストI/Oと通常のIRP経路の違いとして読み分けます。
この近道は、第6回で扱うフィルタードライバーにも関わります。ミニフィルターは、通常のIRP経路だけでなくファストI/Oにも介入できるようになっています。
8. まとめ
記事全体を、仕組み・障害・設計判断の順に振り返ります。
| 観点 | 押さえること |
|---|---|
| キャッシュの実体 | ファイルの256KB区間をマップしたビュー。キャッシュ有効の読み書きはスロットとのメモリコピーで、256KBはディスクI/Oの固定サイズではない |
| 読み取り | 次の区間を先読みする。SequentialScan / RandomAccess はアクセスパターンを伝えるヒント |
| 書き込みと障害 | 既定ではlazy writerが後から書き出す。OSキャッシュへ渡ったデータはアプリだけのクラッシュでは残るが、電源断・OSクラッシュでは未反映のダーティページが失われる |
| 保存方法の選択 | 節目の確定は FlushFileBuffers、書き込みごとの遅延をなくすならWRITE_THROUGH。NO_BUFFERINGには整列要件があり、頻繁な永続化ではWRITE_THROUGHとの併用を検討する |
| 一貫性と永続化 | マップビューと通常のキャッシュI/Oはデータを共有する。NO_BUFFERINGは枠外で、マップの永続化には FlushViewOfFile の後に FlushFileBuffers を使う |
| I/O経路 | ファストI/Oは、キャッシュされたファイルへの同期I/OでIRPを省略する経路。ただし、キャッシュヒットが常にファストI/Oになるわけではない |
ライトバック・先読み・lazy writerを踏まえ、まず 「このデータは電源断で失われてよいか」を決めてください。そのうえで、フラッシュのコスト、常にキャッシュされるメタデータ、装置内キャッシュまで考えて保存方法を選びます。123
マップビューの一貫性と書き出しの完了、キャッシュヒットとIRPの省略も、それぞれ別の判断です。この区別が、保存の不具合や不自然に速いベンチマークを調べる手掛かりになります。456
続きは第5回「NTFSの内部構造 ── MFTから理解するファイルシステム」です。今回まではファイルを「オフセットとバイト列」として扱ってきましたが、その裏でNTFSがどうデータを配置しているのか──MFT、複数データストリーム、ジャーナル、ハードリンク──ディスクの上の静的な構造へ降りていきます。
関連記事
- Windows I/Oの深層(第1回) ── すべての読み書きはIRPになる:I/Oシステムの全体像
- Windows I/Oの深層(第2回) ── 同期I/Oと非同期I/O:OVERLAPPEDの本当の意味
- Windows I/Oの深層(第3回) ── I/O完了ポート(IOCP)と.NETスレッドプール:async/awaitの地下室
- ファイル連携の排他制御の基礎知識 - ファイルロックと原子的 claim のベストプラクティス
- 共有メモリの落とし穴と実務ベストプラクティス
- C#でSQLiteを業務アプリに使う ── WALモード・排他制御・破損対策・EF Coreとの使い分け
- Windowsでプログラムのバージョン別速度を正しく比較する方法
- WindowsアプリでUSB機器を扱う方法 ── 仮想COM・HID・WinUSB・専用SDKの選び方
関連する相談領域
合同会社小村ソフトでは、「保存したはずのデータが消えた」「ファイル書き込みが遅い/速すぎて怪しい」といったWindows業務アプリのファイルI/Oの設計・不具合調査を扱っています。
参考リンク
-
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
-
Microsoft Learn, FlushFileBuffers function. WriteFileが通常は内部バッファへ書き、OSが定期的にディスクへ書き出すこと、FlushFileBuffersが指定ファイルのバッファ済み情報をすべてデバイスへ書き出すこと、多数の書き込みのたびに毎回呼ぶのは非効率であり、頻繁な書き込みで重要データの永続化が必要なアプリはFILE_FLAG_NO_BUFFERINGとFILE_FLAG_WRITE_THROUGHによる非バッファI/Oを使うべきこと、ボリュームハンドルに対して呼べば(管理者権限で)ボリューム上の全オープンファイルをフラッシュできることについて。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, File Buffering. FILE_FLAG_NO_BUFFERINGで開いたファイルへのアクセス要件として、読み書きのサイズとファイルオフセット(OVERLAPPEDで指定する場合を含む)がボリュームのセクターサイズの整数倍でなければならないこと、読み書きバッファのアドレスが物理セクターサイズに整列しているべきこと、物理セクター4,096バイトのAdvanced Formatデバイスへの考慮が必要なことについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, File Mapping. ファイルマッピングオブジェクトがディスク上のファイルに裏打ちされ、ページのスワップアウトが変更内容のファイルへの書き込みとして行われること、複数のプロセスが同じファイルマッピングオブジェクトからローカルファイルのビューを作った場合にデータがコヒーレント(ディスク上のファイルと同一の内容)であることについて。 ↩ ↩2 ↩3
-
Microsoft Learn, FlushViewOfFile function. FlushViewOfFileがマップビューの範囲内のダーティページのディスクへの書き込みを開始すること、この関数がファイルメタデータをフラッシュせず、ハードウェアディスクキャッシュからの物理書き込み完了も待たないこと、ダーティページとメタデータをすべて物理的に書き切るにはFlushViewOfFileの後にFlushFileBuffersを呼ぶべきことについて。 ↩ ↩2 ↩3
-
Microsoft Learn, IRPs Are Different From Fast I/O. ファストI/OがIRPを生成せずにファイルシステムやキャッシュマネージャーのエントリポイントを直接呼ぶ、キャッシュされたファイル向けの同期I/Oの高速経路であること、キャッシュから直接ユーザーバッファへ(またはその逆へ)データが転送されること、ファストI/Oで処理できない場合にIRPベースの通常経路が使われることについて。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Asynchronous disk I/O appears as synchronous on Windows. データがキャッシュにある場合に要求がその場で完了してTRUEが返ること、Windowsのキャッシュがファイルマッピングで実装されており、ページが無い場合の非同期ページフォールト機構がないため、キャッシュ有効の非同期読み取りが同期的に処理されることがあることについて。 ↩
-
Microsoft Learn, FileStream.Flush method (.NET). Flush()がストリームの内部バッファをOSへ書き出すこと、Flush(true)を指定するとそれに加えてすべての中間ファイルバッファ(OSのバッファ)もフラッシュされることについて。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Windows I/Oの深層(第2回) ── 同期I/Oと非同期I/O:OVERLAPPEDの本当の意味
Windowsの同期I/Oと非同期I/O(オーバーラップI/O)を図解で解説する連載の第2回です。FILE_FLAG_OVERLAPPEDの意味、完了通知の4方式、非同期のはずが同期完了する条件、キャンセルの作法、.NETとの対応までを整理します。
Windows I/Oの深層(第1回) ── すべての読み書きはIRPになる:I/Oシステムの全体像
WindowsのI/Oシステムを底から解説する連載の第1回です。オブジェクトマネージャーの名前空間、ドライバー・デバイス・ファイルの3つのオブジェクト、IRPのライフサイクル、CloseHandleの裏側までを図解で整理します。
Windows I/Oの深層(第5回) ── NTFSの内部構造:MFTから理解するファイルシステム
NTFSの内部構造を図解で解説する連載の第5回です。MFTとファイルレコード、複数データストリーム(Zone.Identifier)、ハードリンクと8.3名、リパースポイント、2つのジャーナル、スパースと圧縮までを開発者視点で整理します。
Windows I/Oの深層(第3回) ── I/O完了ポート(IOCP)と.NETスレッドプール:async/awaitの地下室
I/O完了ポート(IOCP)を図解で解説する連載の第3回です。完了キューとスレッド数制御を一体化した設計、コンカレンシー値とLIFO解放、.NETスレッドプールとasync/await継続の実行スレッドまでを整理します。
Windows I/Oの深層(第6回・最終回) ── ミニフィルターの仕組みとProcmonによる遅延調査
ミニフィルターがファイルI/Oを監視・制御する仕組みを解説します。FltMgr、アルティチュード、pre/postコールバック、fltmcの読み方を整理し、Procmonで遅い操作を特定する手順と、除外設定・Dev Driveの注意点をまとめます。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- 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を呼ぶ必要があります。