7.2 UNIX inode 與日誌式檔案系統#
概念說明#
UNIX 檔案系統(UFS、ext2/3/4)用 inode(index node) 描述檔案:每個檔案一個 inode,內含元資料(大小、權限、時間戳)與資料區塊指標,配置採「索引式」而非 FAT 的鏈結式:
- 12 個直接指標:直接指向 12 個資料區塊(以 4KB 區塊計,覆蓋 48KB)。
- 1 個單間接指標:指向一個滿是區塊指標的區塊(4KB/8bytes = 512 個指標 → +2MB)。
- 1 個雙間接:→ 512 × 2MB = +1GB。
- 1 個三間接:→ +512GB。
隨機讀取第 k 個區塊,最多走 3 層間接(通常間接塊已快取),遠優於 FAT 的線性走鏈。目錄則是特殊檔案:內容是「(檔名 → inode 編號)」的對照表。
當機一致性問題:一次檔案建立要寫多個地方(inode、目錄區塊、資料區塊、位圖),順序中斷就會出現孤兒 inode 或指向未初始化區塊的指標。日誌式檔案系統(journaling)的解法:先把「即將進行的區塊寫入」記到日誌區(順序寫),日誌完整落盤後(commit record)才真正改動原地;當機重啟後重播(replay)日誌即可把檔案系統帶回一致狀態。ext4 的資料模式可選 journal / ordered / writeback,差別在是否把「檔案資料」也寫進日誌。
演算法步驟:日誌式寫入(write-ahead)#
- 把將寫入的元資料(與視模式包含的資料)區塊複製進日誌。
- 寫入 commit record(代表交易完整)。
- checkpoint:把日誌中的區塊寫到最終位置。
- 釋放日誌空間。當機後:日誌中有 commit → 重播;無 commit → 丟棄。
Python 範例#
BLOCK = 4096
PTR_PER_BLOCK = BLOCK // 8 # 8 bytes/指標 → 512
class Inode:
def __init__(self):
self.direct = [None] * 12
self.single, self.double, self.triple = None, None, None
def block_ptr(self, k):
"""回傳第 k 區塊的指標所在層級(模擬定址)"""
if k < 12: return ("direct", k)
k -= 12
if k < PTR_PER_BLOCK: return ("single", k)
k -= PTR_PER_BLOCK
d1, d2 = divmod(k, PTR_PER_BLOCK)
if d1 < PTR_PER_BLOCK: return ("double", (d1, d2))
k -= PTR_PER_BLOCK ** 2
d1, rem = divmod(k, PTR_PER_BLOCK ** 2)
d2, d3 = divmod(rem, PTR_PER_BLOCK)
return ("triple", (d1, d2, d3))
ino = Inode()
cap_direct = 12
cap_single = PTR_PER_BLOCK
cap_double = PTR_PER_BLOCK ** 2
cap_triple = PTR_PER_BLOCK ** 3
total = cap_direct + cap_single + cap_double + cap_triple
def fmt(n_blocks):
kb = n_blocks * 4
if kb < 1024: return f"{kb}KB"
if kb < 1024**2: return f"{kb/1024:.0f}MB"
if kb < 1024**3: return f"{kb/1024**2:.0f}GB"
return f"{kb/1024**3:.0f}TB"
print(f"4KB 區塊:直接 {fmt(cap_direct)} + 單間接 {fmt(cap_single)} "
f"+ 雙間接 {fmt(cap_double)} + 三間接 {fmt(cap_triple)}")
print("第 100 區塊的定址:", ino.block_ptr(100))
class Journal:
"""簡化日誌:write-ahead + commit + replay"""
def __init__(self):
self.log, self.committed, self.disk = [], False, {}
def begin(self): self.log, self.committed = [], False
def write(self, blk, data): self.log.append((blk, data))
def commit(self): self.committed = True
def checkpoint(self):
if self.committed:
for blk, data in self.log: self.disk[blk] = data
self.log = []
j = Journal()
j.begin()
j.write(100, "inode 資料"); j.write(101, "目錄項")
j.commit(); j.checkpoint()
print("正常流程後磁碟:", j.disk)
j2 = Journal()
j2.begin(); j2.write(200, "孤兒寫入") # 未 commit 就當機
j2.checkpoint()
print("未 commit 不落地:", j2.disk)
複雜度與 I/O 成本分析#
- 隨機讀第 k 區塊:1 次資料 I/O + 至多 3 次間接塊 I/O(實務上間接塊常快取)。
- 寫入:日誌模式下每個區塊寫兩次(日誌一次 + 原地一次),寫放大 2×,但換取當機一致性。
- inode 大小固定(如 256 bytes),支援的檔案大小上限由指標層級與區塊大小決定。
- ext4 用 extent(範圍描述)取代多層間接,一個 extent 記錄「起點 + 長度」,連續大檔案只需一個 extent,定址更省。
練習#
- 區塊 4KB、指標 4 bytes 時,重新計算各層容量與單檔上限。
- 用 Journal 類別模擬「commit 後、checkpoint 前當機」,說明重播(replay)該做什麼。
- 比較 extent 與三層間接在「讀取 1GB 連續檔案的元資料」所需 I/O 次數。