這一階要補上的那道落差
在儲存那一階,你看著磁碟卸下它所有的機械神祕感,變成一個長長的、扁平的編號區塊陣列:讀第 0 號區塊、讀第 42891 號區塊,眼前沒有半個磁柱。在檔案介面那一階,你從外側使用了檔案與資料夾——你對 report.txt 這樣的名字呼叫了 open、read、write,卻從沒想過區塊號碼。這一階,正是這兩幅圖像之間的橋。總得有人把「有名字的檔案」這個親切的概念,變成編號區塊上真正的記號,而那個人就是檔案系統:磁碟上的資料結構,加上維護它們的程式碼。
這項工作是分層的,就和你可能已經見過的檔案系統分層一樣。靠近頂端,邏輯層懂名字、資料夾與權限;再往下,組織層把檔案的邏輯區塊 3 對映到實體區塊 1574;最底層,驅動程式與中斷真正把位元搬進、搬出裝置。這第一篇導覽大多停在那疊結構的最底部、停在磁碟上,問一個看似簡單的問題:當你格式化一個磁碟區、開始存放檔案時,到底有哪些實體結構被寫了下來,而每一個又裝著什麼?
一張磁碟區的地圖
格式化一個磁碟區——也就是磁碟格式化真正在做的事——大多是把幾個固定的結構,按一個已知的佈局寫下來,好讓作業系統日後能在裡面找到路。把整個磁碟區從頭到尾想成一條編了號的區塊帶,分成幾個有名字的區域。最前面的那個區塊(或前幾個)被保留為開機區塊(boot block):一塊韌體在啟動時會去讀的小區域,存放著開始載入作業系統的啟動程式碼。它和你的檔案毫無關係;它在每個磁碟區上都這樣被保留下來,好讓機器永遠知道第一步該往哪裡看。
緊接在開機區塊之後,坐著最重要的一筆記錄:超級區塊(Unix 的稱呼;一般性的名稱是卷宗控制區塊)。翻開一本書,你會在讀任何章節前先看書名頁與目錄——超級區塊正是檔案系統的那一頁書名頁。它存放整個磁碟區層級的事實:區塊大小(例如 4 KB)、總共有多少區塊與還剩多少空閒、總共有多少索引節點與還剩多少、索引節點表與可用空間圖放在哪、辨識檔案系統型別的魔術數字,以及像「上次是否乾淨卸載」這類旗標。作業系統一掛載這個磁碟區,它幾乎做的第一件事,就是把這一筆記錄讀進記憶體,因為之後每一個操作,都需要這些數字才能知道東西放在哪、每一塊有多大。
low block numbers ------------------------------------> high block numbers
+--------+------------+-------------+------------+----------------------+
| boot | super- | free-space | inode | data blocks |
| block | block | bitmap(s) | table | (file contents) |
+--------+------------+-------------+------------+----------------------+
^ ^ ^ ^ ^
start- describes which one slot the actual
up code the whole blocks are per file: bytes of your
volume free/used the inodes files & folders索引節點:你的檔案,但少了名字
在超級區塊與各種圖之後,來到一個叫索引節點表(inode table)的區域,而檔案真正從這裡開始。想像一座圖書館,每本書都有一張索引卡。那張卡片上沒有故事內容——它記錄誰寫的、何時入庫、誰可以借閱,以及它的各章節究竟放在哪幾個書架。書背上的書名是分開的;它存在於目錄抽屜裡。在 Unix 風格的檔案系統中,那張索引卡就是索引節點:每個檔案一份的小結構,存放關於這個檔案的一切,但「除了」它的名字和它真正的資料。
具體來說,索引節點儲存檔案的中繼資料:型別(一般檔案、目錄、符號連結、裝置)、擁有者與群組、權限位元(你用 chmod 755 設定的那個 rwxr-xr-x)、以位元組計的大小、各種時間戳記、連結計數(有多少名字指向它),以及最核心的——指出「哪些實體資料區塊存放此檔案內容」的指標。那些指標如何排列,本身就是一個有深度的題目:小檔案用少數幾個直接指標,較大的檔案再用單層、雙層、三層間接區塊。那是下一篇導覽的故事;現在只要先記住這幅圖:索引節點就是那一筆小記錄,從它出發,一個檔案的所有資料區塊都找得到。
索引節點的總數,通常在磁碟區格式化時就固定了——索引節點表的大小一次決定,之後不會增長。這帶來一個初學者很少預料到的後果:磁碟可能在「還有空閒資料區塊」時就用光索引節點(數百萬個極小的檔案把表耗盡),也可能在「還有空閒索引節點」時就用光空間。兩者都讓人覺得「磁碟滿了」,但它們是兩種不同的短缺,而其中只有一種能靠刪除大檔案解決。
名字住在哪裡:目錄
如果索引節點存放了除了名字以外的一切,那名字就一定住在某處——它住在目錄裡。從使用者那一側看,目錄不過是裝著一堆有名字檔案的資料夾,但在底層,目錄本身就是一個檔案,它的內容是一張小表,把每個名稱對映到一個索引節點號碼。一個目錄項目,最單純的形式,就是一個(名稱, 索引節點號碼)配對。所以目錄裡裝的不是檔案;它裝的是名稱,連同那些名稱指向的索引節點號碼。靠那個號碼在索引節點表裡找到的索引節點,才是真正通往資料的東西。
現在你可以追蹤一下,當程式開啟目前資料夾裡的 report.txt 時,真正發生了什麼。核心讀取那個資料夾的目錄檔案,掃描它的項目找名稱 report.txt;相符的項目給出一個索引節點號碼,比如 1048577;核心把那個索引節點從索引節點表裡讀出來;它拿那裡的權限位元對照「你是誰」;之後才順著索引節點的區塊指標去取資料。每一次路徑查找都是同一支舞——名稱到索引節點號碼、到索引節點、到區塊——當你沿著像 /home/ann/report.txt 這樣的路徑往下走時,每一段路徑就重複跳一次。
- 讀取目前資料夾的目錄檔案,掃描它的項目,尋找名稱 report.txt。
- 相符的項目給出一個索引節點號碼,例如 1048577。
- 從索引節點表讀取第 1048577 號索引節點;它存有型別、擁有者、權限位元、大小與區塊指標。
- 拿索引節點的權限位元對照呼叫者;若允許,就順著它的區塊指標去讀取檔案的資料。
一個檔案,多個名字
把名字和索引節點拆開,不只是整潔的記帳;它解鎖了一個真實的功能。因為一個目錄項目只是一個(名稱, 索引節點號碼)配對,兩個不同的項目——甚至在兩個不同的資料夾裡——可以帶著相同的索引節點號碼。於是兩個名字都指向同一個索引節點、同一個檔案、同一組資料區塊。這就是硬連結:同一個檔案的第二個名字,和第一個完全對等,不是副本。這正是為什麼索引節點要保有一個連結計數:它不過就是「目前有多少名字指向它」。
這重新定義了「刪除檔案」到底是什麼意思。移除一個名字並不會摧毀檔案——它只是移除一個目錄項目,並把連結計數減一。索引節點與它的資料區塊,只有在「連結計數降到零」而且「沒有任何行程還開著這個檔案」時才會被釋放。所以刪掉好幾個硬連結中的一個,檔案仍以它其他的名字存活;而程式可以繼續讀一個你已經從每個資料夾裡都解除連結的檔案,直到它把檔案關掉為止。檔案其實是靠它的索引節點存活,而不是靠它任何一個名字。
這一階接下來要蓋的東西
你現在握住了其他一切所依附的構造:一個以開機區塊與超級區塊開場的磁碟區、一張每個檔案就是一筆小中繼資料記錄的索引節點表、一個把名稱對映到索引節點號碼的目錄,以及一張追蹤「哪些資料區塊是空閒的」的可用空間位元圖。請注意,光是建立一個檔案,就已經同時碰到了好幾個這些結構——在位元圖裡把一個區塊標記為已用、寫入索引節點、加入目錄項目——這悄悄埋下了我們在這一階最後會遇到的「當機一致性」問題的種子。
接下來四篇導覽,會把這一篇只點到名字的部件一一補齊。第二篇問索引節點究竟怎麼指向資料——也就是配置方法:連續式、鏈結式(以及它被整理過的形式,FAT),與索引式。第三篇深入索引式方案的間接區塊,以及位元圖如何管理可用空間。第四篇爬升到虛擬檔案系統,它讓一台機器能用同一組呼叫去說 ext4、NTFS 與一個網路分享,並談到那個隱藏磁碟緩慢的緩衝快取。第五篇正面迎向我們剛剛瞥見的危險:靠日誌與寫入時複製,撐過一次當機。它們每一篇,都立基於你在這裡蓋好的東西——區塊、索引節點、名稱,以及把它們綁在一起的那些圖。