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_THROUGH、FILE_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
2. キャッシュの正体 ── ファイルはメモリにマップされる
2.1. 256KBのスロットとメモリコピー
Windowsのファイルキャッシュを「ディスクブロックの入れ物」と考えると、いろいろな挙動が説明できなくなります。正しい絵はこうです──キャッシュマネージャーは、ファイルの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はビュー(マップ)の粒度であって、ディスクI/Oが常に256KB単位で行われるという意味ではありません。スロット内のページは必要に応じて読み込まれ、実際にディスクへ飛ぶI/Oの量は要求サイズやアクセスパターンで変わります。初めて読む区間なら、充填のためにディスクI/Oが発生します(ここで第1回のIRPが下位のストレージスタックへ飛びます)。すでにキャッシュにあれば、読み取りはコピーだけで完了します。第2回5章で見た「キャッシュヒットだと非同期発行しても同期完了する」のは、この「即答できる要求はその場で完了させる」動きの現れでした。逆に、キャッシュ有効のままページがメモリに無い場合、ページフォールト処理に非同期の仕組みがないため非同期読み取りが同期的に処理されることがある──という罠も第2回で見たとおりです。7
2.2. 「空きメモリが減った」の正体
キャッシュを使うかどうかや先読みの状態は開き方ごと(ファイルオブジェクト単位)に管理されますが1、キャッシュされたデータ本体はファイル(ストリーム)単位で共有されます。同じファイルを何度開いても、別々のキャッシュができるわけではなく、どのハンドルからも同じキャッシュ内容が見えます(6章の一貫性の土台です)。キャッシュはWindowsが動いている間ずっとキャッシュマネージャーの指揮下で動きます。1 大きなファイルをコピーしたり大量に読み書きすれば、空いている物理メモリはどんどんキャッシュに転用されます。タスクマネージャーの空きメモリが減って見えても、その多くは「アプリが要求すればすみやかに明け渡される、価値ある使われ方をしている待機中のメモリ」です。メモリ不足の診断でこの区別を誤らないために、観測の実務は「.NETでGC待ちとメモリリークを見分ける」でも扱いました。
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に教えるためのフラグだと考えると使いどころが明確になります。
4. 遅延書き込み ── WriteFileの「成功」の意味
4.1. lazy writerは毎秒やってくる
書き込み側はライトバックキャッシュです。WriteFile はデータをスロットへコピーした時点で成功を返し、ディスクへの反映は後回しにされます。この「遅らせて書く」方針が遅延書き込み(lazy writing)です。1
反映を担うのが、キャッシュマネージャーが毎秒起動するlazy writerです。最近フラッシュされていないページの8分の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. 何が起きたら、どこまで消えるのか
「危険な窓」の意味を正確にしておきます。障害の種類で運命が分かれます。
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ごと死ぬとダーティ分は消えます。フラッシュの頻度は性能と信頼性のトレードオフとして調整されており、「突然の電源喪失が起これば、キャッシュされたデータは失われる」とドキュメントも明記しています。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
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 |
実務のパターンも3つだけ挙げておきます。
- 「一時ファイルに書く→フラッシュ→リネーム」が、壊れかけファイルを残さない定石です。中身を書き切ってから名前で確定する──この原子的な受け渡しは「ファイル連携の排他制御の基礎知識」で詳しく扱いました。
- データベースに任せるのも立派な設計です。SQLiteがWALとフラッシュで耐久性を作り込んでいる話は「C#でSQLiteを業務アプリに使う」を参照してください。「自分でフラッシュ戦略を書かない」という選択肢は常にあります。
- ベンチマークはキャッシュを疑う。「読みが速すぎる」測定はたいてい2回目以降のキャッシュヒットを測っています。測定の作法は「Windowsでプログラムのバージョン別速度を正しく比較する方法」にまとめています。
なお図5の最後の一段──ディスク装置内のキャッシュも忘れないでください。FlushFileBuffers はそこまで含めた書き切りを求めますが、USBメモリや外付けディスクでは装置側の書き込みキャッシュポリシー(「クイック取り外し」と「高パフォーマンス」)が絡みます。取り外し可能デバイスの扱いは「WindowsアプリでUSB機器を扱う方法」も参照してください。
6. メモリマップトファイルとの一貫性
第1回で「キャッシュの実体はファイルマッピング」と聞いて、こう思った方がいるはずです──では自分で MapViewOfFile したビューと、ReadFile/WriteFile のキャッシュは喧嘩しないのか?
しません。同じ仕組みの上に乗っているからです。ファイルマッピングオブジェクトはファイルに裏打ちされ、ページの追い出しはファイルへの書き戻しとして行われます。同じローカルファイルに対して複数プロセスがビューを作っても、見える内容はコヒーレント(一貫)です。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
図6: マップビューもキャッシュも、同じ「ファイルに裏打ちされたページ」を見ている。枠外にいるのはNO_BUFFERINGだけ
注意点は2つです。
FILE_FLAG_NO_BUFFERINGのI/Oはこの一貫性の枠外です。キャッシュを経由しない読み書きと、マップビュー/キャッシュ経由の内容は突き合わされません。混ぜるなら自分で整合を取る必要があります。- マップビューの永続化は2段構えです。
FlushViewOfFileは範囲内のダーティページの書き出しを開始しますが、メタデータは書かず、ディスク装置のキャッシュからの物理書き込みも待ちません。確実に届けるにはFlushViewOfFileの後にFlushFileBuffersを呼びます。5
共有メモリとしてのファイルマッピングの実務(名前付き共有、同期、事故パターン)は「共有メモリの落とし穴と実務ベストプラクティス」で扱っています。
7. ファストI/O ── 第1回の宿題回収
第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経路で処理されることがあります。
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
図7: ファストI/Oの分岐。Procmonで FASTIO_READ と IRP_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回) ── すべての読み書きは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 ↩16 ↩17 ↩18 ↩19
-
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
-
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
-
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に割り込める理由
Windowsのフィルタードライバーとミニフィルターを図解で解説する連載の最終回です。フィルターマネージャーとアルティチュード、pre/postコールバック、Procmonやウイルス対策が全I/Oを検査できる仕組み、「あの環境だけ遅い」の調査手順まで整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
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を呼ぶ必要があります。
著者プロフィール
記事の著者プロフィールページです。
小村 豪
合同会社小村ソフト 代表
Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。
公開リンク