マウントとジャーナリング
小さなファイルシステム像で、マウントによる見え方と更新途中の障害からの復旧を確かめます。
説明
マウントは、別のファイルシステムの内容を既存のディレクトリ位置へ接続する操作です。一方、ジャーナリングは複数の管理情報を更新する途中で障害が起きても、整合した状態へ戻すために更新記録を使う仕組みです。
16バイトのブロック4個からなる教材用のファイルシステム像を使います。データと管理情報の場所を確認し、マウント前後の名前空間を表で比べます。その後、更新を確定する前と後で処理を中断し、復旧処理がどちらを採用するかをバイト列で確かめます。
1ブロックと管理情報の配置
64バイトの像を作ります。先頭4バイトは識別子LBX1、位置4は使用ブロックのビットマップ、位置5はファイルのデータブロック番号、位置6はファイルサイズです。データDATAをブロック1へ置きます。
image = bytearray(64)4ブロックの像; image[:7] = b'LBX1'+bytes([0b0011,1,4])識別子と管理情報; image[16:20] = b'DATA'ブロック1の内容; print(image[:16].hex(' '))管理ブロック; print(image[16:32].hex(' '))データブロック使用ビットは0011でブロック0と1が使用中です。ファイルはブロック1の先頭から4バイトを指します。ブロック0は管理情報、ブロック3は今回のジャーナル用として予約し、ビットマップの割当て操作対象から除きます。
2マウントで見える内容が変わる
ルート側の/mnt/dataにはlocal.txtがあるとし、そこへreport.txtを持つ別のファイルシステムを接続するモデルです。二つの一覧を別々に保持します。
underlying = {'local.txt':b'LOCAL'}元のディレクトリ; volume = {'report.txt':bytes(image[16:20])}別のファイルシステム; mounts = {'/mnt/data':volume}接続先の対応; print(sorted(underlying)); print(sorted(mounts['/mnt/data']))接続前後の一覧接続前はlocal.txt、接続後はreport.txtがその位置から見えます。下にあったlocal.txtが削除されたのではなく、マウントがその位置で別の名前空間を見せるため隠れています。
3マウントを外した後
接続の表から/mnt/dataを外し、元のディレクトリと外したボリュームをそれぞれ表示します。
del mounts['/mnt/data']接続を外す; print(underlying)元の名前空間; print(volume)ボリューム側の内容元のlocal.txtと、外した側のreport.txtは両方残ります。アンマウントは接続の解除であり、ファイル内容を消去する操作ではありません。ここからは外したボリュームの管理情報の更新を扱います。
4新データと未確定の更新記録
ファイルを新しいブロック2のNEXTへ置き換えます。先にデータを保存し、続いて管理情報の更新予定をジャーナルへ記録します。記録はTXN1・ビットマップ5・ブロック2・サイズ4・確定印0の8バイトです。
pending = bytearray(image)元の状態を残してコピー; pending[32:36] = b'NEXT'新データを先に保存; pending[48:56] = b'TXN1'+bytes([0b0101,2,4,0])未確定の更新記録; print(pending[48:56].hex(' '))ジャーナルのバイト; print(list(pending[4:7]))通常の管理情報ジャーナルはありますが、確定印は0です。通常の管理情報はまだ[3,1,4]で古いDATAを指します。この演習では示した書込みが順に永続化済みとし、記録の破損や途中で裂けた書込みは別の検査が必要な条件として扱います。
5確定した記録だけを再適用する
復旧処理は識別子と確定印を確認し、確定した更新後の管理情報を通常位置へコピーします。状態は引数のコピーに適用します。
def recover(source):復旧関数
restored = bytearray(source)像のコピー
if restored[48:52]==b'TXN1' and restored[55]==1:確定済みか
restored[4:7] = restored[52:55]管理情報を再適用
return restored復旧した像確定印がない記録は採用しません。これはredo型の復旧の要点を絞ったモデルです。実際のジャーナルではトランザクション識別やチェックサム、記録の完全性、保存順序も検査します。
6確定前に止まった場合
未確定のpendingを復旧します。ファイルのブロック番号とサイズから実際の内容を取り出します。
before_commit = recover(pending)確定前の復旧; block,size = before_commit[5:7]参照先とサイズ; print(list(before_commit[4:7]))採用した管理情報; print(bytes(before_commit[block*16:block*16+size]))ファイル内容管理情報は[3,1,4]、内容はDATAのままです。NEXTのバイトはブロック2にありますが、確定したファイルの内容としては採用されません。未使用ブロックに残るバイトと、ファイルから参照されるデータは別です。
7確定後に管理情報の更新途中で止まる
確定印を1へ変え、その後の通常位置への反映がビットマップだけで止まった状態を作ります。
crashed = bytearray(pending)障害直前の像; crashed[55] = 1記録を確定; crashed[4] = crashed[52]一部だけ通常位置へ反映; print(list(crashed[4:7]))不一致の管理情報; print(bool(crashed[4] & (1<<crashed[5])))参照先が使用中か管理情報は[5,1,4]です。ファイルはまだブロック1を指すのに、ビットマップでは1が未使用なので整合していません。最後のFalseがその不一致です。ジャーナルの確定記録には、完成させるべき[5,2,4]が残っています。
8確定記録から整合した状態へ復旧
crashedを復旧し、参照先とビットマップ、内容をもう一度読みます。
restored = recover(crashed)確定した更新を再適用; block,size = restored[5:7]新しい参照先; print(list(restored[4:7]),bool(restored[4]&(1<<block)))管理情報の整合; print(bytes(restored[block*16:block*16+size]))新しいファイル内容管理情報は[5,2,4]で参照先は使用中、内容はNEXTです。確定前の障害では古い状態、確定後の障害では確定した更新を採用することで、途中の組合せを残さないようにしています。
9復旧を繰り返しても内容が増えないこと
復旧済みの像へ同じ記録をもう一度適用します。ジャーナルから得た内容をマウント後の一覧へ反映します。
again = recover(restored)同じ更新を再適用; print(again==restored)冪等性の確認; block,size = again[5:7]復旧後の参照先; volume['report.txt'] = bytes(again[block*16:block*16+size])名前から見える内容; mounts['/mnt/data'] = volume復旧済みボリュームを接続; print(mounts['/mnt/data'])接続後の内容一致はTrueで、report.txtはNEXTです。更新後の値を同じ場所へ再設定するため、復旧を繰り返しても処理が二重加算されません。整合性を保つことは、アプリが望む保存時点まで必ず永続化したという保証とは別で、必要な同期処理も設計します。
まとめ
マウントは名前空間を接続する操作で、下にあった内容の削除とは異なります。
複数の管理情報の一部だけが保存されると不整合になり、確定済みの更新記録から復旧できます。
復旧の再実行に耐える記録方式と、データ・記録・確定印の保存順序が必要です。
操作履歴と解説を振り返れます。
Python 3
image = bytearray(64); image[:7] = b'LBX1'+bytes([0b0011,1,4]); image[16:20] = b'DATA'; print(image[:16].hex(' ')); print(image[16:32].hex(' '))4c 42 58 31 03 01 04 00 00 00 00 00 00 00 00 00 44 41 54 41 00 00 00 00 00 00 00 00 00 00 00 00
underlying = {'local.txt':b'LOCAL'}; volume = {'report.txt':bytes(image[16:20])}; mounts = {'/mnt/data':volume}; print(sorted(underlying)); print(sorted(mounts['/mnt/data']))['local.txt'] ['report.txt']
del mounts['/mnt/data']; print(underlying); print(volume){'local.txt': b'LOCAL'}
{'report.txt': b'DATA'}pending = bytearray(image); pending[32:36] = b'NEXT'; pending[48:56] = b'TXN1'+bytes([0b0101,2,4,0]); print(pending[48:56].hex(' ')); print(list(pending[4:7]))54 58 4e 31 05 02 04 00 [3, 1, 4]
def recover(source):
restored = bytearray(source)
if restored[48:52]==b'TXN1' and restored[55]==1:
restored[4:7] = restored[52:55]
return restoredbefore_commit = recover(pending); block,size = before_commit[5:7]; print(list(before_commit[4:7])); print(bytes(before_commit[block*16:block*16+size]))[3, 1, 4] b'DATA'
crashed = bytearray(pending); crashed[55] = 1; crashed[4] = crashed[52]; print(list(crashed[4:7])); print(bool(crashed[4] & (1<<crashed[5])))[5, 1, 4] False
restored = recover(crashed); block,size = restored[5:7]; print(list(restored[4:7]),bool(restored[4]&(1<<block))); print(bytes(restored[block*16:block*16+size]))[5, 2, 4] True b'NEXT'
again = recover(restored); print(again==restored); block,size = again[5:7]; volume['report.txt'] = bytes(again[block*16:block*16+size]); mounts['/mnt/data'] = volume; print(mounts['/mnt/data'])True
{'report.txt': b'NEXT'}演習完了