🎓 レベル:標準 | 重要度:A(必須)
📎 前提:OSの役割とカーネル | 関連:ディスクとストレージ階層・バッファリング・キャッシュ・ジャーナリング
要点(BLUF)
- ファイルの実体は inode:サイズ・権限・更新日時などのメタデータと、データが入ったブロックへのポインタを持つ構造体。ファイル名はinodeには含まれない。
- ポインタは 直接+単/二重/三重間接の階段構造。小さなファイルは直接ポインタで即アクセスでき、巨大ファイルは間接ブロックで容量を稼ぐ。
- ディレクトリは「ファイル名 → inode番号」の対応表にすぎない。だから同じ実体に複数の名前を付けられる(ハードリンク)。
概念 ── ディスクは「番号付きブロックの集まり」
ストレージ(ディスクとストレージ階層)は物理的には、固定サイズのブロック(典型4KB)に番号を振った巨大な配列です。「ファイル」「フォルダ」という概念はそこにありません。それをユーザに見せるのがファイルシステムの仕事(OSの役割とカーネル の抽象化)。
中核データ構造が inode(index node)。1ファイルに1つ対応し、次を持ちます。
- メタデータ:ファイルサイズ、所有者、権限(rwx)、作成/更新/アクセス時刻、リンク数、種別(通常ファイル/ディレクトリ/デバイス…)
- データブロックへのポインタ:実データがディスクのどのブロックにあるか
注目すべきは、inodeにファイル名が入っていないこと。名前は別管理(ディレクトリ)です。
仕組み① ── 直接・間接ブロック(マルチレベルインデックス)
ファイルは小さいものも巨大なものもあります。inodeのポインタを全ファイル分の最大数だけ持つのは無駄。そこで階段状のインデックスを使います(古典Unix系の例)。
flowchart LR
inode["inode"] -->|"直接 x12"| d["データブロック(直接)"]
inode -->|"単間接 x1"| s["ポインタブロック"] --> sd["データブロック群"]
inode -->|"二重間接 x1"| dd1["ポインタブロック"] --> dd2["ポインタブロック群"] --> dddata["データブロック群"]
inode -->|"三重間接 x1"| t["…さらに1段深い…"]
- 直接ポインタ(12本):最初の12ブロックを直接指す。小ファイルはこれだけで足り、1回の参照でデータに届く(速い)。
- 単間接:ポインタだけが詰まったブロックを1つ指す(1024本ぶん)。
- 二重間接:ポインタブロックを指すポインタブロック(1024×1024本)。
- 三重間接:さらにもう1段。
この設計の最大ファイルサイズを計算します([[#対応ラボ]] の 05-01_inode_capacity.py。ブロック4KB・ポインタ4バイト=1ブロックに1024ポインタ)。
BLOCK, PTRS = 4096, 4096//4 # 1ブロックに1024ポインタ
total = (12 + PTRS + PTRS**2 + PTRS**3) * BLOCK
実行結果(実機):
直接(12) : 12 ブロック -> 0.00 GB
単間接 : 1,024 ブロック -> 0.00 GB
二重間接 : 1,048,576 ブロック -> 4.00 GB
三重間接 : 1,073,741,824 ブロック -> 4096.00 GB
表せる最大ファイルサイズ = 約 4.00 TB
容量のほとんどを三重間接が稼ぐ一方、ほとんどのファイルは小さく直接ポインタだけで完結します。10KB のファイル -> 3 ブロック(直接で足りる)。つまりよくある小ファイルは速く、稀な巨大ファイルも表せるという、頻度に応じた賢い非対称設計です。
仕組み② ── ディレクトリは対応表
ディレクトリも実体はファイル(専用のinodeを持つ)で、中身は**「ファイル名 → inode番号」のエントリの並び**です。
flowchart LR
dir["ディレクトリ(/home/zack)"] -->|"'memo.txt' -> inode 1287"| i1["inode 1287(メタ+ブロック)"]
dir -->|"'photo.jpg' -> inode 9041"| i2["inode 9041"]
パス /home/zack/memo.txt を開く流れ:ルート / のinode → その中で home のinode番号を引く → home の中で zack → zack の中で memo.txt のinode番号 → そのinodeのブロックを読む。名前解決はディレクトリを辿るルックアップの連鎖です。
名前とinodeが分離しているので、1つのinodeに複数の名前を付けられます(ハードリンク:別々のディレクトリエントリが同じinode番号を指す)。inodeの「リンク数」が0になって初めて実データが解放されます。rm がファイルでなくリンクの削除である理由がこれです。
仕組みの直観 ── なぜこの設計か
- メタデータと名前を分ける理由:1つの実体に複数の名前(ハードリンク)を許し、名前変更を「対応表の書き換え」だけで済ませる(データ移動不要)。
- 直接+間接の階段にする理由:ファイルサイズの分布が極端に偏っている(大半は小さい)。小ファイルを速く、巨大ファイルも可能に、という頻度最適化。固定の大きな表を全ファイルに持たせる無駄を避ける。
- inodeを固定領域に並べる理由:inode番号からディスク位置を即計算でき、メタデータアクセスが速い。代わりにファイルシステム作成時にinode数の上限が決まる(小ファイルを大量に作るとinode枯渇がデータ容量より先に来ることがある)。
⚠️ よくある誤解・落とし穴
- 「ファイル名はファイルの一部」→ 名前はディレクトリ側。inodeは名前を知らない。
- 「ファイルを消すと即データが消える」→ 消えるのはリンク(名前)。リンク数が0になって初めて解放。開いているプロセスがあればさらに延びる。
- 「inodeは無限」→ 作成時に数が決まる。小ファイル大量作成でinode枯渇しうる(
df -iで確認)。 - 「大きいファイルも小さいファイルも同じ速さ」→ 巨大ファイルの末尾は間接参照が増え、メタデータアクセスが多い。
対応ラボ
cs-foundations-study/labs/05-01_inode_capacity.py(実行して最大ファイルサイズと小ファイルのブロック数を確認済み)。labs/05-01_inode_observe.md に Linux の stat/ls -i/ln でinode番号とリンクを観察する手順。
関連
- 実体が乗るストレージの物理は ディスクとストレージ階層
- 書き込みの永続性・整合性は バッファリング・キャッシュ・ジャーナリング
- ファイル抽象を提供するシステムコールは OSの役割とカーネル