🎓 レベル:標準 | 重要度:A(必須)
📎 前提:ファイルシステムの構造・ディスクとストレージ階層 | 関連:仮想記憶とページング・入出力とバス・割り込み
要点(BLUF)
- ディスクは桁違いに遅い(ディスクとストレージ階層)。OSはページキャッシュでよく読むデータを主記憶に保持し、書き込みはバッファに溜めてまとめて書く(遅延書き込み)。これで速度を稼ぐ。
- 代償は永続性の遅れ:バッファに溜めた書き込みは、ディスクに届く前にクラッシュすると失われる。複数ブロックの更新が途中で止まると不整合になる。
- ジャーナリングは「これから何をするか」を先にログへ書いてから本体を更新する。クラッシュ後はログを見てやり直すか捨てるので、中途半端な状態を防げる(クラッシュ整合性)。
概念 ── 速さと永続性は対立する
ファイルI/Oには相反する2つの願いがあります。速くしたい(遅いディスクを待ちたくない)と、確実に残したい(書いたデータは電源が落ちても消えてほしくない)。OSは前者のためにメモリでキャッシュ/バッファし、それが後者(永続性)を危うくする――この緊張が本トピックの核です。
仕組み① ── ページキャッシュと遅延書き込み
OSは空いている主記憶を使って、ディスクのブロックをページキャッシュとして保持します(仮想記憶とページング のページと同じ仕組み)。
- 読み込み:一度読んだブロックはキャッシュに残す。次は主記憶から(ディスクに行かない)。先読み(read-ahead)で連続アクセスを見越して先にロードもする。
- 書き込み:すぐディスクに書かず、まず**バッファ(ダーティページ)**に書いて「書いたことにする」(write-back / 遅延書き込み)。後でまとめてディスクへ反映する。
flowchart LR
app["アプリ(write())"] --> pc["ページキャッシュ(ダーティページ)"]
pc -.->|"後でまとめて (flush/fsync)"| disk["ディスク(永続)"]
disk -->|"read(初回のみ)"| pc
pc -->|"read(2回目以降は即返す)"| app
これが効くのは局所性(メモリ階層とキャッシュ)のおかげ。多くの読み書きが直近のデータに集中するので、ヒット率が高くディスクアクセスを大幅に減らせます。DMA(入出力とバス・割り込み)と組み合わせ、実際の転送はCPUを介さず行われます。
仕組み② ── 何が危険か(クラッシュと不整合)
遅延書き込みには落とし穴があります。
- 書いたはずのデータが消える:
write()が成功しても、まだバッファ上だけかもしれない。その瞬間に電源断すると、ディスクには届いていない。確実に永続化したいならfsync()でフラッシュを強制する(DBがコミット時に必ず呼ぶ)。 - 更新が途中で止まり不整合になる:1つの操作が複数ブロックの更新を伴うことがある。例:ファイルを新規作成すると「inodeを書く」「データブロックを書く」「ディレクトリエントリを書く」「空きブロックビットマップを更新する」が必要。この途中でクラッシュすると、inodeはあるがディレクトリに名前がない、あるいはブロックが使用中なのに空き扱いといった矛盾が残る。
flowchart TB
op["1つの論理操作(ファイル作成)"] --> a["1) inode書き込み"]
a --> b["2) データブロック書き込み"]
b --> x["ここでクラッシュ!"]
x --> bad["矛盾: ディレクトリに名前なし / ビットマップ未更新"]
古いファイルシステムは起動時に全体を走査して修復(fsck)していましたが、大容量では何時間もかかります。
仕組み③ ── ジャーナリングで整合性を守る
ジャーナリングの発想はシンプル:本体を更新する前に、「これから何をするか」をログ(ジャーナル)に書き切る。
flowchart LR
j1["1) ジャーナルに変更内容を記録"] --> j2["2) ジャーナルにコミット印"]
j2 --> j3["3) 本体(inode/ディレクトリ等)を更新"]
j3 --> j4["4) ジャーナルの該当エントリを破棄"]
クラッシュからの復帰時、ジャーナルを見て判断します。
- コミット印まで書けていた:本体反映が未完でも、ログ内容を**やり直す(redo)**ことで完了させる。
- コミット印が無い(書きかけ):その変更は無かったことにして捨てる。
どちらに転んでも、「全部やった」か「全部やらない」のどちらかになり、中途半端な矛盾状態を避けられます。これが**アトミックな更新(all-or-nothing)**で、データベースのトランザクションと同じ思想です。
実コストを抑えるため、多くのFS(ext4等)はメタデータだけをジャーナリングします(データ本体までやると2回書きで重い)。「ファイルの構造は壊れないが、最後の数秒のデータ内容は失われうる」という現実的な妥協です。
仕組みの直観 ── なぜこの設計か
- 遅延書き込みする理由:書くたびに遅いディスクを待てば実用にならない。まとめ書き・重複排除(同じブロックへの連続書き込みを1回に)で、ディスクI/O回数を激減できる。代償が永続性の遅れ。
- ジャーナルを先に書く理由:本体の複数ブロック更新は不可分にできない(競合状態とクリティカルセクション と似た非アトミック性)。だが「ログに意図を1か所へ書き切る」操作なら、コミット印の有無で完了/未完了を二値に判定でき、復旧を単純化できる。
- メタデータのみジャーナリングする理由:完全な二重書きは重い。構造の整合性(致命的)を守りつつ、データ内容の損失(軽微・数秒分)は許容する、というコストと安全のバランス。
⚠️ よくある誤解・落とし穴
- 「
write()が返れば永続化済み」→ 多くはまだバッファ上。確実な永続化はfsync()。これを怠るDBはクラッシュでデータを失う。 - 「ジャーナリングがあればデータは絶対消えない」→ 守るのは主に整合性(構造の矛盾防止)。メタデータのみジャーナリングなら直近データ内容は失われうる。
- 「キャッシュは万能で速い」→ ダーティページが溜まりすぎると、フラッシュ時にI/Oが集中して急に遅くなる(ライトバックの嵐)。
- 「ジャーナル=バックアップ」→ 別物。ジャーナルはクラッシュ整合性のための短期ログ。バックアップは別途必要。
対応ラボ
cs-foundations-study/labs/05-03_fsync.md(Linux: write のみ vs fsync ありの永続性差、mount でジャーナリングモード(data=ordered 等)を確認する手順)。
関連
- キャッシュの土台(ページ)は 仮想記憶とページング
- まとめ書きを支えるDMAは 入出力とバス・割り込み
- 守る対象の構造は ファイルシステムの構造
- アトミック更新の発想は 競合状態とクリティカルセクション