5.1 檔案系統的設計(FAT、UNIX inode)#
檔案系統要解決什麼問題#
記憶體是易失的(斷電即空),磁碟是持久的但按塊(block)定址——典型 512B/4KB 一塊。檔案系統(file system)的使命,是在「按塊定址的持久裝置」上,實現「按名稱、可變長、可組織、可共享的資料單位」——檔案與目錄。這是 OS 中最「資料結構密集」的子系統:本質上是在塊裝置上實作一棵持久化的樹。
一個檔案系統必須回答四個基本問題:
- 檔案的塊在哪裡?(配置元資料)
- 名字如何對應到檔案?(目錄結構)
- 空間如何分配與回收?(free space 管理)
- 崩潰後如何自保?(一致性——留待 5.2 節)
FAT(1977):檔案配置表的簡單之美#
FAT(File Allocation Table)由 Marc McDonald 於 1976–77 年為微軟的磁碟 BASIC 設計(Stand-alone Disk BASIC),隨 1981 年 IBM PC 的 MS-DOS 統治個人電腦近二十年。它的設計哲學是極簡:
- 磁碟劃成叢集(cluster);檔案由叢集鏈組成。
- 開頭的 FAT 表:每個叢集一格,格內存「下一個叢集的編號」(或 EOF、壞塊、空閒標記)。
- 檔案的元資料(名稱、大小、起始叢集)放在目錄項(directory entry)裡——目錄就是一張線性表。
// FAT 讀檔:沿鏈行走
int cluster = dir_entry->start_cluster;
while (cluster != EOF) {
read_block(cluster_to_offset(cluster));
cluster = FAT[cluster]; // 純陣列跳轉,簡單到極致
}
優點:結構極簡、實作幾十行、救援工具成熟(FAT 表有兩份備份)。 缺點:
- 隨機存取是 O(n):要讀檔案中段,必須從頭沿鏈跳 n 次——每跳一次都是一次(可能隨機的)磁碟讀。檔案越大越痛。
- FAT 表常駐快取:表本身要佔記憶體,且每次塊配置都要寫它(串列尾指標更新),密集小寫入時 I/O 放大。
- 膨脹:FAT32 的叢集最大 32KB,大磁碟上小檔案浪費嚴重(內部碎片)。
- 元資料貧乏:目錄項只有名稱、時間、屬性——無權限、無 link、無 journal。
FAT 至今仍活在 U 隨身碟、SD 卡與韌體(UEFI 系統分割必用 FAT32)中——簡單就是它的永續競爭力。
UNIX inode(1970s):索引節點的革命#
UNIX(Thompson 原始 PDP-7 版已具備檔案系統;1974 年 CACM 論文定稿描述)的檔案系統採取截然不同的路線:把「檔案的元資料」與「名字」分離。
inode:檔案的身分證#
inode(index node)是每個檔案的核心資料結構(名稱來自早期 UNIX 的 index node),固定大小、集中存放在磁碟的 inode 區。它不含檔名,包含:
- 型態(普通檔/目錄/裝置/FIFO)與權限(rwx 三組九位元)、擁有者、群組
- 大小、時間戳(atime/mtime/ctime)
- 連結計數(link count):多少個目錄項指向我
- 塊指標:資料塊的位址陣列
塊指標採多級索引(multi-level indexing)設計(UNIX V6/V7 時代定型):
struct inode {
mode_t mode; uid_t uid; nlink_t nlink;
off_t size;
// 15 個塊指標(UNIX System V 佈局)
blk_t direct[12]; // 直接塊:12 個
blk_t indirect1; // 一級間接:指向一塊,內含 1024 個塊號
blk_t indirect2; // 二級間接
blk_t indirect3; // 三級間接
};
// 容量(1KB 塊、4B 塊號):
// 12KB + 1024KB + 1024²KB + 1024³KB ≈ 16 GB
神來之筆在於取捨:小檔案(絕大多數檔案)只走直接塊——一次元資料查找就拿到資料;大檔案才需要多次間接跳轉。這是「為常見情況最佳化」的統計洞察(1981 年 Satyanarayanan 對 UNIX 檔案尺寸分布的實測研究確認了此設計的合理性)。
目錄:名字與 inode 的解耦#
目錄在 UNIX 中只是「一張 (名字 → inode 號) 的特殊檔案」。這個解耦直接兌現了兩個 UNIX 標誌性功能:
- 硬連結(hard link):多個名字指向同一 inode,刪除名字只是 link count 減一,歸零才回收塊。
ln的實作只是加一條目錄項。 - 符號連結(symbolic link):一個內容是「路徑」的小檔案,跨檔案系統也能指。
/home/alice/report.txt ──┐
/backup/report.txt ──────┤──→ inode #4207 → [資料塊...]
(兩個目錄項,同一個 inode,nlink=2)
「一切皆檔案」也由 inode 的型態位元實現:裝置檔案的 inode 記錄主/次設備號,對其 read/write 即呼叫驅動程式(5.3 節)。
BSD FFS 與磁碟佈局的局部性#
原始 UNIX FS 隨磁碟變大而效能崩壞(塊太碎、inode 與資料相距千里)。McKusick 等人於 1984 年發表 FFS(Fast File System,即 Berkeley Fast File System),引入:
- 柱面群(cylinder group):把磁碟切成群,每群有自己的 inode 區與資料區;同一檔案的 inode 與資料盡量同群,尋道距離驟減。
- 大塊+碎片:8KB 塊+1KB 碎片,兼顧吞吐與小檔案空間效率。
- 叢集寫入(clustering):連續塊合併成大 I/O。
FFS 的「以局部性為第一設計目標」思想,是所有現代檔案系統的直系血統——ext2/ext3/ext4、XFS、UFS 全部承襲。
對照:兩條設計路線的譜系#
graph LR
FAT[1977 FAT<br>鏈式配置] --> FAT32[FAT32 1996]
FAT --> exFAT[exFAT 2006]
UFS1[1974 原始 UNIX FS] --> FFS[1984 FFS<br>柱面群] --> ext2[1993 ext2]
FFS --> XFS[1994 XFS<br>B+樹延展性]
ext2 --> ext3[2001 ext3 日誌] --> ext4[2008 ext4 extents]
NTFS[1993 NTFS<br>MFT/B+樹]
FFS -.-> ZFS[2005 ZFS COW] -.-> btrfs[2009 btrfs]- FAT 路線(塊鏈):極簡、可攜,隨機存取差。
- FFS 路線(索引+局部性):複雜、高效,成為伺服器與桌面主流。
- 日誌與 COW 路線(ext3/ZFS/btrfs):把「崩潰一致性」當第一目標(5.2 節)。
空間管理:位圖 vs. 鏈#
空閒塊追蹤也是設計分野:
- 位圖(bitmap):每塊一位元。查詢 O(n) 但可被 SIMD/位運算加速;FFS/ext2/NTFS 用之。優點:容易找到「近鄰」的空閒塊(局部性)。
- 空閒鏈(free list):空閒塊互鏈。FAT 隱含、早期系統用。缺點:鏈越走越散、無法挑局部性。
近代演進:extent(區段)——ext4/XFS 記錄「起始塊+連續長度」而非單塊,一個元資料項覆蓋上百 MB,把大檔案的元資料從「百萬條間接塊」壓縮成幾條 extent。這是 2000 年代對「多級索引已不合時宜」的直接回應。
與編譯器/虛擬機的關聯#
- 編譯器的連結器本質上是「檔案系統元資料」的消費者:ELF/Mach-O 目標檔的 symbol table、section 布局是「miniature inode」——連結器對多個物件檔做「塊合併與重定位」,如同 FS 對碎片做緊實。
- 資料庫=自建檔案系統:InnoDB 的 tablespace/extent/頁三層、PostgreSQL 的 segment,都是繞過 OS 檔案系統的抽象、直接管理塊(O_DIRECT)以掌控配置與快取——當通用抽象不合用時,系統軟體會下沉自建。
- JVM 的類別載入:class path 尋找
.class檔、jar(zip)內目錄解析,本質是「使用者態的檔案系統目錄遍歷」;OSGi/模組系統進一步在 JVM 內重建了「命名空間 → 資源 → 版本」的樹。 - 疊加式檔案系統(overlayfs):Docker 容器映像層的基礎,把多個目錄樹聯合成一棵——如同 JIT 把直譯器疊加到硬體上,分層合成是兩界共享的方法論。
重點整理#
- 檔案系統 = 在塊裝置上實作持久樹;核心問題是塊定位、命名、空閒管理與崩潰安全。
- FAT(1977)以叢集鏈求極簡,隨機存取 O(n) 是其原罪,仍統治可攜媒體。
- UNIX inode(1970s)以「元資料與名字解耦+多級索引」兌現硬連結與一切皆檔案;FFS(1984)引入柱面群把局部性變成第一設計目標。
- extent、B+ 樹、COW 是 1990–2010 年代的三條現代化主線,將在 5.2 節與日誌匯流。