基礎●●○○○

ファイルシステム

File System ・ ふぁいるしすてむ

ブロックデバイスの上に「名前付きの階層とファイル」という抽象を築く仕組み。永続化の土台。

✦ 30秒で試す — 航海ファイル名は、ファイルの中には書かれていない電源を入れ直すと、ディスクはどうなっている?予想してみる

概要

ファイルシステムは、ディスクやSSDのような「番号の付いたブロックがひたすら並んでいるだけの記憶装置」の上に、「名前の付いた階層と、ファイルという扱いやすい単位」という抽象を築く仕組みです。私たちが /home/user/report.txt のようなパスを指定してファイルを開いたり保存したりできるのは、この層がブロックの割り当てと帳簿付けを一手に引き受けてくれているからです。

普段はOSの機能として存在を意識しませんが、「フォルダを作る」「ファイルをコピーする」「空き容量を確認する」といった当たり前の操作はすべてファイルシステムの仕事です。ディスクは本来、電源を切っても内容が消えない「永続化」の担い手であり、ファイルシステムはその永続化を人間とプログラムが使える形に翻訳する土台にあたります。

ext4・APFS・NTFS といった名前を聞いたことがあるかもしれません。これらはすべてファイルシステムの実装であり、同じ「名前付き階層」という抽象を提供しながら、内部の帳簿の付け方やクラッシュへの強さで違いを持っています。

なぜ生まれたか

記憶装置は、ハードウェアのレベルでは「0番から順に番号が振られた、固定長のブロックの羅列」でしかありません。この素のままの世界には「ファイル」も「フォルダ」も存在せず、あるのは「何番のブロックに何バイト書く/読む」という操作だけです。もしこの状態でプログラムがデータを保存しようとすると、「自分のデータをどのブロックに置いたか」「どこがまだ空いているか」「他のプログラムと領域がぶつかっていないか」を、すべてのプログラムが自前で管理しなければなりません。プログラムが増えれば台帳の食い違いでデータを上書きし合う事故が避けられません。

そこで、この面倒な帳簿付けを一箇所に集約し、プログラムには「名前」と「階層」という人間にわかりやすいインターフェースだけを見せることにしました。空きブロックの管理も、名前からデータの位置を引く対応表も、すべてファイルシステムが引き受けます。プログラムは「report.txt を開いて 100 バイト読む」と頼むだけでよく、それが物理的にどのブロックに散らばっているかを知る必要がなくなったのです。この抽象と帳簿管理をOSのカーネルが担うことで、多数のプログラムが同じディスクを安全に共有できるようになりました。

詳細

inode — ファイルの実体はメタデータにある

多くのファイルシステムで、1つのファイルの実体は「inode(アイノード)」と呼ばれる管理レコードで表されます。inode が持つのはファイルの中身そのものではなく、そのファイルについての情報(メタデータ)です。サイズ・所有者・アクセス権限・作成/更新時刻、そして「実際のデータがディスク上のどのブロックに置かれているか」を指すポインタの一覧です。ファイルの中身は別のデータブロックにあり、inode がそこへの地図の役割を果たします。

ここで見落としがちなのが、ファイル名は inode の中に入っていないという点です。名前を管理しているのはディレクトリ(フォルダ)で、ディレクトリの正体は「名前 → inode番号 の対応表」にすぎません。/home/user/report.txt を開くとき、システムはルートディレクトリから順に、home の対応表を引いて次のディレクトリの inode を得て、その中で user を引き、さらにその中で report.txt を引く、というように一段ずつ名前を inode に変換していきます。この解決の流れを図にすると次のようになります。

パス /home/user/report.txt を解決するルート /home → inode 12ルート自身は inode 2home ディレクトリuser → inode 9user ディレクトリreport.txt → inode 42inode 42サイズ・権限・時刻データ位置の地図データブロック #180データブロック #181データブロック #402
パス解決 — ディレクトリという対応表を一段ずつたどり、inode を経てデータブロックに至る

名前と実体が分かれていることから、いくつか実務で効く性質が導かれます。1つの inode を複数の名前から指すことができ、これをハードリンクと呼びます(ln a b)。どちらの名前も対等な「入口」で、inode は自分を指す名前の数(リンク数)を数えていて、最後の名前が消えて誰も開いていない状態になったときに初めて中身が解放されます。同じファイルシステムの中の mv が一瞬で終わるのも、中身を動かさずにディレクトリの対応表を書き換えるだけだからです。ext4 では慣例としてルートディレクトリ自身が inode 2 番で、パス解決はいつもそこから始まります。

ブロック割り当てと空き管理

新しくデータを書き込むとき、ファイルシステムは「空いているブロックを探して割り当て、inode の地図に書き足す」という作業をします。どのブロックが使用中でどこが空いているかは、ビットマップ(1ブロックにつき1ビットで使用中か否かを記録する表)などで管理されます。ファイルが飛び飛びのブロックに散らばると読み出しが遅くなる「断片化(フラグメンテーション)」が起きるため、ext4 ではエクステント(連続したブロックの範囲を「開始位置と長さ」でまとめて指す方式)や、書き込みをまとめてから置き場所を決める遅延割り当てといった工夫が凝らされています。

クラッシュ一貫性とジャーナリング

ファイルシステムが最も神経を使うのが、書き込みの途中で電源が落ちたりクラッシュしたりする場面です。たとえば新しいファイルを1つ作るだけでも、「空きブロックの印を付ける(ビットマップ)」「中身を書く(データブロック)」「inode を書く」「ディレクトリに名前を足す」と、ディスク上の離れた4か所を書き換えます。ディスクはこれを一度には書けないので、途中で中断されると、使用中の印なのにどこからも参照されない迷子のブロックや、どの名前からもたどれない inode が残ります。かつては再起動のたびに fsck(ファイルシステム検査)がディスク全体を走査してこうした食い違いを探して直しており、大きなディスクでは数十分から数時間かかりました。

これを防ぐのがジャーナリング(journaling)です。本来の場所を書き換える前に、「これからこういう変更をします」という予定をジャーナル(専用の記録領域)に書き、最後にコミット記録を書いて確定させます。そのあとで本来の場所を書き換え、終わったらジャーナルを解放します。途中で落ちても、再起動時にジャーナルを見て、コミット済みの予定は最後までやり直し(再生)、コミットされていない予定は捨てればよく、どこで切れても「全部できた」か「何もしていない」のどちらかに戻ります。ext4 の既定(ordered モード)での書き込みの順番は次のとおりです。

本来の場所ジャーナルデータブロックファイルシステムアプリ本来の場所ジャーナルデータブロックファイルシステムアプリここで確定。以後に落ちても再生できるreport.txt を作って書く1 中身を書く2 予定を書く — 空き表・inode・名前3 コミット記録を書く4 空き表・inode・ディレクトリを書き換える5 ジャーナルを解放

下の部品では、ファイルを作る書き込みの好きな境目で電源を切れます。ジャーナルなしでは何が残り、ジャーナルありではどう戻るかを確かめてください。

⚡ 体験: 書き込みの途中で電源を切る — ジャーナルの有無で比べる
方式
ディスクの中身 — report.txt を作るビットマップ空き/使用中の印0:11:12:13:04:05:06:07:0inode 表ファイルの管理票5: notes.md → #17: 空き8: 空きディレクトリ名前 → inode 番号notes.md → 5(空き)データブロック#0#1#2#3#4#5#6#7ジャーナルなし(本来の場所を直接書き換える)書き込みの手順 — ⚡ をつかんで、電源が落ちる位置に置く手順1 空き表2 データ3 inode4 名前⚡手順2のあとで停電
メタデータ(空き表・inode・名前)データジャーナル停電・食い違い
食い違いが残った0 回一貫した状態0 回

report.txt を新しく作るには、ディスク上の4か所を書き換える必要があります。下の帯の ⚡(停電)をつかんで好きな手順の境目に置いてください。離すと書き込みが始まり、そこで電源が落ちます。

注意したいのは、ext4 の既定のジャーナルが守るのはメタデータ(空き表・inode・ディレクトリ)の一貫性だけで、ファイルの中身まではジャーナルに書かないことです。ordered モードは「中身を先に書いてからメタデータをコミットする」順番を守ることで、inode が書きかけのゴミを指す事態は防ぎますが、上書き中に落ちたファイルの中身が古い版と新しい版の混ざったものになることは防ぎません(中身まで記録する data=journal モードもありますが、書き込み量が倍になります)。これはデータベースの WAL(Write-Ahead Logging、先行書き込みログ)とまったく同じ発想で、トランザクションの原子性を保証する仕組みや、変更の記録を残すログの考え方と地続きです。「変更する前に、変更の意図を耐久性のある場所に書いておく」という原則は、永続化を扱うあらゆる層で繰り返し現れます。

書いたのに消える — ページキャッシュと fsync

実務でいちばん踏みやすい落とし穴は、「write が成功した=ディスクに書かれた」ではないことです。OS(Linux など)は書き込みをいったんメモリ上のページキャッシュに受け取ってすぐ返し、ディスクへはあとでまとめて書き出します。そのため、書いた直後に電源が落ちると、アプリは成功を受け取ったのにデータが消えています。確実にディスクへ届けたいときは fsync(そのファイルの内容をディスクまで書き切るまで待つシステムコール)を呼ぶ必要があり、データベースがコミットのたびに fsync するのはこのためです。

設定ファイルのように「丸ごと置き換えたい」ファイルには、定番の書き方があります。まず一時ファイルに新しい中身を書いて fsync し、それから rename で本来の名前に付け替えます(厳密には、付け替えを確定させるためにディレクトリも fsync します)。rename は同じファイルシステムの中では原子的なので、読む側からは「古い版」か「新しい版」のどちらかしか見えず、書きかけの版を読まれることがありません。直接上書きすると、途中で落ちたときに半分だけ新しいファイルが残ります。

代表的な実装とデータ構造の接点

Linux で標準的な ext4 と XFS、Apple の APFS、Windows の NTFS などが代表的な実装です。基本の考え方は共通ですが、スナップショット(ある時点の状態を丸ごと保存する機能)への対応や、大量のファイルを速く探すための工夫に違いがあります。ディレクトリ内のファイル数が膨大になると、名前を線形に探すのでは遅すぎるため、索引のデータ構造が使われます。NTFS・APFS・XFS・Btrfs はB木(B-tree)系の構造で名前を索引化し、ext4 はファイル名のハッシュ値で引く HTree(ハッシュを鍵にした浅い木)を使います。B木はデータベースのインデックスが使うのと同じ構造で、「ディスクという遅くて大きい記憶の上で、少ない読み出し回数で目的にたどり着く」という共通の課題への共通の答えになっています。APFS や Btrfs、ZFS は、書き換えのたびに新しい場所へ書いて最後に指し先を切り替える「コピーオンライト」で、ジャーナルとは別の道からクラッシュ一貫性とスナップショットを得ています。

コンテナ・クラウドストレージとの対比

現代のインフラでは、ファイルシステムの応用形が随所に現れます。コンテナのイメージは「レイヤ化ファイルシステム」(OverlayFS など)で構成されており、ベースイメージの上に差分だけを重ねていく仕組みで、複数のコンテナが土台を共有しつつ、書き込みは自分のレイヤに閉じ込めます。一方、クラウドのオブジェクトストレージ(Amazon S3 など)は、あえて階層やファイルの部分書き換えという抽象を捨て、「キーに対して塊(オブジェクト)を丸ごと出し入れする」という単純なモデルに割り切ることで、桁違いの規模と分散への強さを得ています。ファイル単位で部分更新できる従来のファイルシステムと、更新は丸ごと置き換えと割り切るオブジェクトストレージ — どちらが優れているかではなく、「何を捨てて何を得るか」の設計判断の違いとして眺めると、永続化という土台の奥行きが見えてきます。

試問

report.txt を新しく作るには、ディスクの4か所(ビットマップ=空き表・inode=ファイルの管理票・ディレクトリ=名前の一覧・データブロック=中身)を書き換える必要があります。下の帯の ⚡ をつかんで手順の境目に置くと、書き込みが始まってそこで電源が落ち、再起動後のディスクの状態が表示されます。方式は「ジャーナルなし(本来の場所を直接書き換える)」と「ジャーナルあり(先に変更の予定を別の場所に記録する)」から選べます。

✦ 命題挑戦中

report.txt を作る書き込みの途中で電源が落ちても、再起動したら report.txt が食い違いなく完成して残っているようにせよ

  • 書き込みの途中(最初の手順より後・最後の手順より前)で電源を切る
  • 再起動後、表どうしの食い違いが残らない
  • しかも report.txt が完成した形で残っている
⚡ 体験: 書き込みの途中で電源を切る — ジャーナルの有無で比べる
方式
ディスクの中身 — report.txt を作るビットマップ空き/使用中の印0:11:12:13:04:05:06:07:0inode 表ファイルの管理票5: notes.md → #17: 空き8: 空きディレクトリ名前 → inode 番号notes.md → 5(空き)データブロック#0#1#2#3#4#5#6#7ジャーナルなし(本来の場所を直接書き換える)書き込みの手順 — ⚡ をつかんで、電源が落ちる位置に置く手順1 空き表2 データ3 inode4 名前⚡手順2のあとで停電
メタデータ(空き表・inode・名前)データジャーナル停電・食い違い
食い違いが残った0 回一貫した状態0 回

report.txt を新しく作るには、ディスク上の4か所を書き換える必要があります。下の帯の ⚡(停電)をつかんで好きな手順の境目に置いてください。離すと書き込みが始まり、そこで電源が落ちます。