Dev Study
OSと低レイヤのしくみ コース

20. ジャーナリング — 途中で電源が落ちても壊れない仕組み

ファイルシステムが「書いている途中で電源が落ちても壊れない」ようにジャーナルを使う仕組みを、図で追っていきましょう。

① 1つのファイル操作は「複数ブロックの更新」に分解される

ファイルに1ブロック追記するとき、ディスク上で書き換わる場所
アプリの操作write() でファイルを1ブロック分伸ばす
ファイルシステムが内部で3か所を更新する
ブロックビットマップ「このブロックは使用中」の印を立てる
inodei_size とブロック番号(エクステント)を更新
データブロック本体実際のファイル内容を書き込む
3つが揃って初めて「正しい状態」
整合の取れたファイルシステム
  • ・ディスクへの書き込みはブロック単位で、3か所を同時に原子的に書く手段はない
  • ・順番に1つずつ書くしかない、というのがこの問題の出発点です

ユーザーから見れば「1回の書き込み」でも、ディスク上では複数の独立したブロック更新に分かれます。

② 途中で電源が落ちると不整合が残る

3つの更新のうち一部だけ書けた状態でクラッシュした場合
ビットマップ 書き込み済使用中の印は立った
inode 書き込み済ファイルは伸びたことになっている
データ 未書き込み中身は古いゴミのまま
ここで電源断
再起動後のファイルシステムinode は「中身がある」と言うが実体は無い
届いたブロックの組み合わせが違えば
リークビットマップだけ届き、誰も使わないブロックが埋まる
二重割り当てinode だけ届き、未使用扱いのブロックが別ファイルにも配られる
  • ・どのブロックまで届いたかはクラッシュのタイミング次第で、事前には決められない
  • ・全部を走査して直す fsck は、容量が大きいほど時間がかかります

どの順番で書いても「途中まで書けた状態」は必ず存在し、それがファイルシステムの不整合になります。

③ 解決策 — 先に「これから何をするか」をジャーナルへ書く

ジャーナル領域と本体領域の役割分担
ジャーナル領域(jbd2)これから行う更新の予告を順番に書き足す場所
ジャーナル inode は通常 inode 8
予告が確定したら
本体領域inode・ビットマップ・データが実際に置かれる場所
ジャーナルの中身は3種のブロックで構成される
ディスクリプタブロックh_blocktype = 1
更新後のブロック群本体のどこへ書くかをタグが指す
コミットブロックh_blocktype = 2
  • ・各ジャーナルブロックの先頭12バイトは journal_header_s で、h_magic は 0xC03B3998
  • ・ディスクリプタ内の journal_block_tag_s の t_blocknr が「本体のどのブロック向けか」を示します
  • ・h_sequence が同じものが1つのトランザクションとしてまとまります

本体をいきなり触らず、まずジャーナルに更新内容を追記します。追記なので途中で切れても本体は無傷です。

④ 書き込みの順序 — コミットが「境界線」になる

ジャーナル書き込みからチェックポイントまでの時系列
1. ジャーナルへ更新内容を書くディスクリプタ + 更新後ブロックを追記
ここまでは本体に一切影響しない
2. コミットブロックを書くh_blocktype = 2 が届いた瞬間に「確定」
ここが原子性の境界
確定後、ゆっくり本体へ
3. チェックポイントジャーナルの内容を本体の inode・ビットマップ・データへ反映
反映が済んだら
4. ジャーナル領域を解放反映済みの区間を再利用可能にする
  • ・コミットブロックは1ブロックなので、書けたか書けなかったかの二択になります
  • ・ext4(5) によると commit=nrsec でコミット間隔を指定でき、既定値は5秒です
  • ・ジャーナルは循環バッファで、古い区間は反映済みになれば上書きされます

「1ブロックのコミットが届いたか」という単純な判定に、複数ブロック更新の全か無かを縮約しているのが要点です。

⑤ クラッシュ後の再起動 — replay で拾うか捨てるか

マウント時にジャーナルを読み直して判断する
再起動してマウントファイルシステムが dirty ならジャーナルを走査
各トランザクションのコミットブロックを確認
コミット有りh_sequence の順に本体へ再適用(replay)
コミット無しそこで走査を打ち切り、以降は破棄
replay の結果
整合の取れた状態に復帰操作は「全部やった」か「何もやってない」のどちらか
  • ・replay は本体へ書き直すだけなので、同じ内容を何度適用しても結果は同じです
  • ・revoke ブロック(h_blocktype = 5)は、後で書き換えられたブロックの再適用を省いて復旧を速めます
  • ・mount(8) は「ext3 と ext4 は dirty ならジャーナルを replay する」ため ro でも書き込みが起きうると注記しています

コミット済みは再適用、コミット前は破棄。この二択で「途中まで書けた状態」が消えます。

⑥ なぜデータ本体は既定でジャーナルに入れないのか

data= モードごとの、ジャーナルを通るものの違い
data=ordered(既定)ジャーナルを通るのはメタデータのみ
データはコミット前に本体へ直接書き出す
順序を強制する理由
データを先に本体へinode がブロックを指す前に中身を確定させる
その後にメタデータをコミット古いゴミが新ファイルから見えるのを防ぐ
他のモードと比べると
data=journalデータもジャーナルへ書いてから本体へ
同じ内容を2回書くので書き込み量が倍
data=writeback順序保証なし、メタデータ確定後にデータが届きうる
  • ・ext4(5) は data=ordered を「メタデータがジャーナルにコミットされる前に、全データを本体へ強制的に書き出す」既定モードと記しています
  • ・kernel.org の ext4 文書は、ordered モードが XFS や JFS の既定と同様の「メタデータジャーナリング」に相当すると述べています
  • ・ext4(5) の data=journal は「全データがジャーナルへコミットされてから本体へ書かれる」モードで、書き込みが二重になります

全部をジャーナルに通せば安全ですが書き込みが倍になります。既定はデータを先に書く順序制約だけで、安全性と性能を両立させています。

公式ドキュメントで詳しく ↗