Mímisbrunnr知恵の泉

← コンピュータ基礎 一覧

🎓 レベル:標準 | 重要度:A(必須)

📎 前提:ファイルシステムの構造ディスクとストレージ階層 | 関連:仮想記憶とページング入出力とバス・割り込み

要点(BLUF)

概念 ── 速さと永続性は対立する

ファイルI/Oには相反する2つの願いがあります。速くしたい(遅いディスクを待ちたくない)と、確実に残したい(書いたデータは電源が落ちても消えてほしくない)。OSは前者のためにメモリでキャッシュ/バッファし、それが後者(永続性)を危うくする――この緊張が本トピックの核です。

仕組み① ── ページキャッシュと遅延書き込み

OSは空いている主記憶を使って、ディスクのブロックをページキャッシュとして保持します(仮想記憶とページング のページと同じ仕組み)。

flowchart LR
    app["アプリ(write())"] --> pc["ページキャッシュ(ダーティページ)"]
    pc -.->|"後でまとめて (flush/fsync)"| disk["ディスク(永続)"]
    disk -->|"read(初回のみ)"| pc
    pc -->|"read(2回目以降は即返す)"| app

これが効くのは局所性(メモリ階層とキャッシュ)のおかげ。多くの読み書きが直近のデータに集中するので、ヒット率が高くディスクアクセスを大幅に減らせます。DMA(入出力とバス・割り込み)と組み合わせ、実際の転送はCPUを介さず行われます。

仕組み② ── 何が危険か(クラッシュと不整合)

遅延書き込みには落とし穴があります。

  1. 書いたはずのデータが消えるwrite() が成功しても、まだバッファ上だけかもしれない。その瞬間に電源断すると、ディスクには届いていない。確実に永続化したいなら fsync() でフラッシュを強制する(DBがコミット時に必ず呼ぶ)。
  2. 更新が途中で止まり不整合になる: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) ジャーナルの該当エントリを破棄"]

クラッシュからの復帰時、ジャーナルを見て判断します。

どちらに転んでも、「全部やった」か「全部やらない」のどちらかになり、中途半端な矛盾状態を避けられます。これが**アトミックな更新(all-or-nothing)**で、データベースのトランザクションと同じ思想です。

実コストを抑えるため、多くのFS(ext4等)はメタデータだけをジャーナリングします(データ本体までやると2回書きで重い)。「ファイルの構造は壊れないが、最後の数秒のデータ内容は失われうる」という現実的な妥協です。

仕組みの直観 ── なぜこの設計か

⚠️ よくある誤解・落とし穴

対応ラボ

cs-foundations-study/labs/05-03_fsync.md(Linux: write のみ vs fsync ありの永続性差、mount でジャーナリングモード(data=ordered 等)を確認する手順)。

関連

第5章 ファイルシステムとI/O 目次