ファイルシステムを1から — 「保存した」は何を保証するか
エディタが「保存しました」と言った直後に電源が落ちたら、そのファイルは残るのか。図書館の目録という比喩から出発して、ブロック・inode・ディレクトリ・ジャーナリング・fsyncまでを前提知識ゼロで積み上げ、なぜデータベースがファイルシステムを信用しないのか、現場で何を触ると事故になるのかまで降ろします。
「保存しました」の直後に電源を抜いたら
エディタの右下に「保存しました」と出ます。その0.2秒後に停電したとして、さっき書いた文章は残っているでしょうか。
答えは「たぶん残る。ただし保証はない」です。しかも、残るかどうかを決めているのはエディタではありません。アプリとディスクの間には、ページキャッシュ・ファイルシステム・デバイスの内蔵キャッシュという「まだ書いていないが書いたことにしておく」層が何段も挟まっていて、そのどれもが速さのために嘘をつきます。ファイルシステムを学ぶとは、この嘘の分布を知ることです。
比喩: 目録カードと書架
大きな図書館を想像してください。本そのものは奥の書架にあり、棚には番号が振ってあるだけです。代わりに目録カードがあって、「この本は3棚の12番から7冊分の幅を占める」「420ページ」「誰が閲覧できるか」が書いてある。ただしカードに題名は書かれていません。
題名を扱うのは3つ目の帳面、名前の一覧です。ここには「『銀河ヒッチハイク・ガイド』→ カード番号 8123」という対応だけが並びます。
この3層がファイルシステムのほぼ全部です。書架がデータブロック、目録カードがinode、名前の一覧がディレクトリ。奇妙な分け方に見えますが、分けたからこそ「同じ本に2つの題名を付ける」「題名を変えても本を1ミリも動かさない」が一瞬で終わります。
ディスクはバイト単位では書けない
物理の制約から入ります。ストレージは1バイトずつ書き換えられません。デバイスが扱う最小単位はセクタ(512バイト、最近の機種は4096バイト)で、ファイルシステムはそれを束ねたブロック(多くのLinux環境で4KiB)で管理します。
結果、1バイトのファイルもディスク上の4KiBを占めます(内部断片化)。ls -l のサイズと du の占有量がずれるのはこれが理由です。ファイルが大きくなると位置の記録自体が重くなるので、現代のext4やXFSはエクステント、つまり「45000番から連続で2048ブロック」という範囲でまとめて記録します。
inode — 名前を持たない「ファイルの本体」
目録カードにあたるのが inode(index node)です。ファイル種別とパーミッション、所有者、サイズ、タイムスタンプ、そしてデータの位置。stat を叩くとほぼそのまま見えます。
$ stat notes.txt
Size: 1240 Blocks: 8 IO Block: 4096 regular file
Device: 259,2 Inode: 8123 Links: 2
Access: (0644/-rw-r--r--) Uid: (1000/k) Gid: (1000/k)
効いてくるのは Links: 2 です。これはこのinodeを指している名前の数で、ln notes.txt memo.txt とすれば3になり、どの名前も完全に対等な「本物」になります(コピーではないので、片方を編集すると全部が変わります)。
だから削除のシステムコールは delete ではなく unlink です。やっているのは名前を1つ外してカウントを1減らすことだけ。カウントが0になり、かつ誰もそのファイルを開いていないときに初めてブロックが解放されます。この「かつ」が、後で現場の事故として戻ってきます。
ディレクトリは「表」でしかない
ディレクトリは魔法ではなく、「名前 → inode番号」の組を並べたただのファイルです。表の先頭には必ず自分自身を指す . と親を指す .. が入っているので、どのディレクトリもリンク数が2から始まります。
素朴に作れば、この表は先頭から舐めるしかありません。10万個のファイルがあれば1個を探すのに平均5万件の比較が要ります。そこでext4は dir_index(htree=ハッシュ化したB木)、XFSはB+木を使い、名前から一発で引けるようにしています。この差は「倍」ではなく「桁」で効きます。
コメント
コメントにはログインが必要です