1. 程式人生 > 其它 >作業系統:檔案系統

作業系統:檔案系統

目錄

檔案系統的實現

檔案系統的結構

檔案系統在磁碟上將磁碟分成塊(block),這些塊大多數用於儲存使用者資料,將用於存放使用者資料的磁碟區域稱為資料區域(data region)。檔案系統必須記錄每個檔案的資訊,這類資訊在檔案系統中使用 inode 結構來儲存。
除了儲存檔案的資訊,還需要一些分配結構

來記錄 inode 或資料塊是空閒還是已分配,例如空閒列表或資料點陣圖。檔案系統中還有一些超級塊(superblock),包含關於該特定檔案系統的資訊,例如檔案系統中有多少個 inode 和資料塊、inode 表的位置和檔案系統型別等。

inode

inode 是 index node(索引結點)的縮寫,是許多檔案系統中使用的通用名稱,用於描述儲存給定檔案的元資料的結構。在每個 inode 中儲存了所有關於檔案的資訊,這些資訊稱之為元資料(matadata)。主要有:檔案型別、檔案大小、分配給它的塊數、檔案許可權、時間資訊,以及有關其資料塊駐留在磁碟上的位置的資訊(如某種型別的指標)。

設計 inode 時最重要的問題是:如何引用資料塊的位置,一種簡單的方法是在 inode 中有一個或多個直接指標,指向屬於該檔案的一個磁碟塊,但是如果檔案太大則直接指標的指向範圍會很有限。為了支援更大的檔案,檔案系統可以使用間接指標(indirect pointer)

,它不是指向包含使用者資料的塊,而是指向包含更多指標的塊,每個指標指向使用者資料。

目錄組織

通常檔案系統將目錄視為特殊型別的檔案,因此目錄有一個 inode。目錄具有由 inode 指向的資料塊,也一樣存在於檔案系統的資料塊區域中。我們的磁碟結構因此保持不變。一個目錄基本上只包含一個二元組(條目名稱,inode 號)的列表,通常還有會記錄長度欄位。

刪除一個檔案是,會在目錄中間留下一段空白空間,因此應該有一些方法來標記它,使用記錄長度欄位更便於刪除操作的執行。

空閒空間管理

空閒空間管理(free space management)是很重要的功能,一位檔案系統必須記錄哪些 inode 和資料塊是空閒的,這樣在分配新檔案或目錄時,就可以為它找到空間。管理空閒空間可以有很多方法,例如點陣圖、空閒列表和 B 樹(B-tree)等。
當建立一個檔案時,必須為該檔案分配一個 inode,檔案系統將通過搜尋一個空閒的內容,並將其分配給該檔案。然後將這個 inode 標記為已使用,並最終用正確的資訊更新磁碟上的資料結構。

一些 Linux 檔案系統(如 ext2 和 ext3)在建立新檔案並需要資料塊時,會採用預分配(pre-allocation)策略

。首先先尋找一系列空閒塊,然後將它們分配給新建立的檔案,檔案系統保證檔案的一部分將在磁碟上並且是連續的,從而提高效能。

檔案讀寫

讀取檔案

讀取檔案時提供給檔案系統的是完整的路徑名,因此檔案系統必須遍歷(traverse)路徑名來查詢所需的 inode。所有遍歷都從檔案系統的根開始,即根目錄(root directory)。具體的過程如下:

  1. 從根目錄的 inode 開始遍歷,從而獲取一些基本資訊;
  2. 在 inode 查詢指向資料塊的指標,用這些磁碟上的指標來讀取目錄;
  3. 遞迴遍歷路徑名,直到找到所需檔案的 inode;
  4. 進行許可權檢查,在每個程序的開啟檔案表中,為此程序分配一個檔案描述符返回給使用者。

當檔案讀取完畢需要關閉時,只需要釋放檔案描述符即可。

寫入檔案

寫入檔案是一個類似的過程,首先先開啟檔案,接著應用程式更新檔案,最後關閉該檔案。當寫入一個新檔案時,每次寫入操作不僅需要將資料寫入磁碟,還必須首先決定將哪個塊分配給檔案,從而相應地更新磁碟的其他結構。因此每次寫入檔案在邏輯上會導致 5 個 I/O:

  1. 讀取資料點陣圖,標記新分配的塊被使用;
  2. 寫入點陣圖,將它的新狀態存入磁碟;
  3. 讀取 inode;
  4. 寫入 inode,用新塊的位置更新;
  5. 寫入真正的資料塊本身。

建立檔案

檔案建立是,寫入的工作量更大,因為檔案系統不僅要分配一個 inode,還要在包含新檔案的目錄中分配空間。

  1. 讀取 inode 點陣圖,查詢空閒 inode;
  2. 寫入 inode 點陣圖,將其標記為已分配;
  3. 寫入新的 inode,進行初始化;
  4. 寫入目錄的資料,將檔案的高階名稱連結到它的 inode 號;
  5. 讀寫目錄 inode,進行更新;
  6. 如果目錄需要增長以容納新條目,則還需要額外的 I/O。

快取和緩衝

讀取和寫入檔案的開銷很大,會導致磁碟的增加許多 I/O 請求,大多數檔案系統積極使用系統記憶體(DRAM)來快取重要的塊。引入快取後,第一次開啟可能會產生很多 I/O 請求,來讀取目錄的 inode 和資料,但是隨後開啟同一檔案時大部分會命中快取,因此不需要 I/O。
儘管可以通過足夠大的快取完全避免讀取 I/O,但寫入的內容必須進入磁碟才能儲存。寫緩衝(write buffering)技術是通過延遲寫入,檔案系統可以將一些更新編成一批(batch),放入一組較小的 I/O 中,以減少 I/O 的次數。同時如果應用程式建立檔案並將其刪除,則將檔案建立延遲寫入磁碟,可以完全避免寫入。不過如果系統在更新傳遞到磁碟之前崩潰,更新就會丟失。

快速檔案系統

伯克利的一個小組設計了快速檔案系統(Fast File System,FFS),思路是讓檔案系統的結構和分配策略考慮磁碟的特性,從而提高效能。

效能問題

直接實現的檔案系統將磁碟當成隨機存取記憶體,資料遍佈各處,而不考慮儲存資料的介質特點。例如檔案的資料塊通常離其 inode 非常遠,因此每當第一次讀取 inode 然後讀取檔案的資料塊時,就會導致昂貴的尋道。
第二個問題是檔案系統最終會變得非常碎片化(fragmented),缺乏空閒空間管理的空閒列表最終會指向遍佈磁碟的一堆塊。結果導致在磁碟上來回訪問邏輯上連續的檔案,從而降低了效能。例如以下是 3 個檔案在系統中的分配情況:

此時如果刪除了檔案 B,並存放佔用 4 個塊的檔案 D,這個連續的檔案將被拆成 2 部分放入不相鄰的空閒區域中。

柱面組

FFS 將磁碟劃分為一些柱面組(cylinder group),對於在同一組中的兩個檔案,FFS 可以使先後訪問它們不會導致穿越磁碟的長時間尋道。出於可靠性原因,每個組中都有超級塊(super block),在每個組中也同樣需要分配結構、資料區域和 inode。

FFS 將相關的東西放一起來提高效能,也就是將相關的東西置於同一個區塊組內。對於是目錄的放置,FFS 優先選擇分配數量少的柱面組和大量的自由 inode 來放置。它將檔案的資料塊儘量分配到與其 inode 相同的組中,從而防止 inode 和資料之間的長時間尋道(如在老檔案系統中)。其次它將位於同一目錄中的所有檔案,放在它們所在目錄的柱面組中。

大檔案儲存

對於大檔案的放置需要特別的規則,如果特別對待,大檔案將填滿它首先放入的塊組。以這種方式妨礙了隨後的“相關”檔案放置在該塊組內,可能破壞檔案訪問的區域性性。

對於大檔案,FFS 將其分為多個部分,分別放入多個柱面組中。如果大塊大小足夠大,大部分時間仍然花在從磁碟傳輸資料上,而在大塊之間尋道的時間相對較少。每次開銷做更多工作,從而減少開銷,這個過程稱為攤銷(amortization)

子塊

為了儘可能減少內部碎片(internal fragmentation),FFS 引入子塊(sub-block)來解決。假設每個塊大小為 4KB,而每個子塊有 512 位元組。一開始若檔案大小為 1KB,則它將佔據 2 個子塊。如果檔案發生了增長,檔案系統將繼續為其分配 512 位元組的子塊,直到它達到完整的 4KB 資料。此時 FFS 將找到一個 4KB 塊,將子塊的內容複製到其中,然後釋放子塊。
這個過程效率低下,因為檔案系統需要大量的額外工作。因此 FFS 修改 libc 庫來實現緩衝寫入,以 4KB 塊的形式將它們傳送到檔案系統。

優化磁碟佈局

對於未優化的磁碟佈局,磁碟在順序讀取期間可能會有較長的旋轉時延。例如 FFS 讀取塊 0,當讀取完成時如果要繼續讀取塊 1,往往此時塊 1 已在磁頭下方旋轉,不得不等磁碟轉完一圈。

FFS 使用引數化技術優化磁碟佈局,該技術會找出磁碟的特定效能引數,確定特定磁碟在佈局時應跳過多少塊,以避免額外的旋轉。

不過現代磁碟更加智慧了,它們在內部讀取整個磁軌並將其緩衝在內部磁碟快取中,在對軌道的後續讀取中,磁碟就從其快取記憶體中返回所需資料。

其他改進

  1. FFS 是允許長檔名的第一個檔案系統之一,而不是傳統的固定大小;
  2. 引入了符號連結的新概念,允許使用者為系統上的任何其他檔案或目錄建立“別名”,更加靈活;
  3. 引入了一個原子 rename() 操作,用於重新命名檔案。

檔案系統檢查程式

崩潰一致性

檔案系統面臨的一個主要挑戰在於,如何在出現斷電(power loss)或系統崩潰(systemcrash)的情況下,更新持久資料結構。由於磁碟一次只為一個請求提供服務,如果在一次寫入完成後系統崩潰或斷電,則磁碟上的結構將處於不一致(inconsistent)的狀態。
例如寫入一個檔案,就需要分別寫入 inode、點陣圖和資料塊,此時可以排列組合得到 6 種崩潰場景:

  1. 只將資料塊寫入磁碟:資料在磁碟上,但是沒有指向它的 inode 和表示塊已分配的點陣圖;
  2. 只有更新的 inode 寫入了磁碟:由於 DB 未寫入,如果我們信任該 inode,將從磁碟讀取垃圾資料;
  3. 只有更新後的點陣圖寫入了磁碟:由於沒有指向它的 inode,檔案系統永遠不會使用這個塊;
  4. 寫入了 inode 和點陣圖,但沒有寫入資料:該 inode 將從磁碟讀取垃圾資料;
  5. 寫入了 inode 和資料塊,但沒有寫入點陣圖:inode 和點陣圖的舊版本之間存在不一致;
  6. 寫入了點陣圖和資料塊,但沒有寫入 inode:不知道資料塊屬於哪個檔案,因為沒有 inode 指向。

在檔案系統資料結構中可能存在不一致性,理想的做法是將檔案系統從一個一致狀態,原子地(atomically)移動到另一個狀態,這個一般問題稱為崩潰一致性問題(crash-consistency problem)

檔案系統檢查程式功能

早期的檔案系統採用了 fsck 來處理崩潰一致性,它決定讓不一致的事情發生,然後再修復它們。fsck 的檢查功能包括:

  1. 首先檢查超級塊是否合理,若有故障修復方式是使用超級塊的備用副本;
  2. 掃描 inode 生成分配點陣圖,如果點陣圖和 inode 之間存在不一致,則通過信任 inode 內的資訊來解決;
  3. 檢查每個 inode 是否存在損壞或其他問題,如果 inode 不易修復,則 inode 會被 fsck 清除,inode 點陣圖相應地更新;
  4. 驗證每個已分配的 inode 的連結數,fsck 從根目錄開始掃描整個目錄樹,若不匹配則修改為正確的計數;
  5. 檢查重複指標,如果重複的 inode 中由某一個有明顯故障,會被清除;
  6. 檢查壞塊指標,如果指標顯然指向超出其有效範圍的某個指標,fsck 就將其刪除;
  7. 對每個目錄的內容執行額外的完整性檢查,確保層次和 inode 都是正確的。

fsck 的不足

  1. 構建 fsck 需要複雜的檔案系統知識,編碼難度大;
  2. 執行速度慢,對於非常大的磁碟卷,掃描整個磁碟可能需要幾分鐘或幾小時;
  3. 掃描整個磁碟,僅修復某些小部分出現的問題的開銷是極其大的。

其他恢復技術

除了日誌在後文描述,還有一些恢復技術如下:

技術 簡介
軟更新 對檔案系統的所有寫入排序,以確保磁碟上的結構永遠不會處於不一致的狀態
寫時複製 永遠不覆寫檔案或目錄,而是對磁碟上以前未使用的位置進行新的更新
基於反向指標的一致性 每個塊新增一個反向指標,訪問檔案時檢查正向指標是否指向引用它的塊
樂觀崩潰一致性 儘可能多地向磁碟發出寫入,並利用事務校驗和等技術來檢測不一致

日誌

日誌的思想源於資料庫系統,基本思路是在更新磁碟時,在覆寫結構之前寫下一些資訊描述狀態,這些資訊構成的集合就是日誌。通過將這些資訊寫入磁碟,可以保證在更新(覆寫)正在更新的結構期間發生崩潰時,能夠返回並檢視你所做的註記,然後重試。

加檢查點

資料日誌(data journaling)的工作原理是,在將 inode、點陣圖和資料塊寫入磁碟位置之前,先將它們寫入日誌。對於這個請求新增事務標記構成一個事務,一旦這個事務安全地存在於磁碟上,就可以覆寫檔案系統中的舊結構,這個過程稱為加檢查點(checkpointing)
但是如果在寫入日誌期間崩潰,則日誌裡面的資訊也是錯誤的,系統恢復後不應該繼續工作。為避免該問題,檔案系統分兩步發出事務寫入,首先先所有要修改的資訊寫入日誌,寫入完成時再進行提交,保證日誌的資訊是完整正確的。

恢復

如果崩潰發生在事務被安全地寫入日誌之前,那隻需要簡單地跳過待執行的更新。如果在事務已提交到日誌之後,但在加檢查點完成之前發生崩潰,可以在系統引導時,檔案系統恢復過程將掃描日誌,並查詢已提交到磁碟的事務。然後將這些事務重放(replayed),檔案系統再次嘗試將事務中的塊寫入它們最終的磁碟位置,稱為重做日誌(redo logging)

日誌空間管理

日誌的大小有限,如果不斷向它新增事務將很快佔滿日誌的空間,這樣會引發 2 個問題:

  1. 日誌越大恢復時間越長,恢復過程必須重放日誌中的所有事務才能恢復;
  2. 當日志已滿或接近滿時,不能向磁碟提交進一步的事務,從而影響檔案系統的功能。

為了解決這些問題,日誌檔案系統將日誌視為迴圈資料結構,也就是迴圈日誌。一旦事務被加檢查點,檔案系統應釋放它在日誌中佔用的空間,允許重用日誌空間。

有序日誌

儘管恢復很快,但檔案系統的正常操作還是很慢,例如在寫入日誌和寫入主檔案系統之間存在代價高昂的尋道,這增加了顯著的開銷。一種更簡單的日誌形式為有序日誌(ordered journaling),它沒有將使用者資料寫入日誌,而是先將使用者資料寫入檔案系統,避免重複寫入。
至此可以總結出日誌的工作步驟:

  1. 資料寫入:將資料寫入最終位置,等待完成;
  2. 日誌元資料寫入:將開始塊和元資料寫入日誌,等待寫入完成;
  3. 日誌提交:將事務提交,等待寫入完成;
  4. 加檢查點元資料:將元資料更新的內容寫入檔案系統中的最終位置;
  5. 釋放:一段時間後,在日誌超級塊中將事務標記為空閒。

日誌結構檔案系統

系統原理

日誌結構檔案系統(Log-structured File System, LFS)的原理是在寫入磁碟時,LFS 首先將所有更新緩衝在記憶體段中。當段已滿時,它會在一次長時間的順序傳輸中寫入磁碟,並傳輸到磁碟的未使用部分。LFS 永遠不會覆寫現有資料,而是始終將段寫入空閒位置,由於段很大,因此可以有效地使用磁碟。
日誌結構檔案系統的設計主要源自以下 4 個方面:

  1. 記憶體大小不斷增長,可以在記憶體中快取更多資料;
  2. 隨機 I/O 效能與順序 I/O 效能之間存在巨大的差距,且不斷擴大;
  3. 現有檔案系統在許多常見工作負載上表現不佳;
  4. 檔案系統不支援 RAID。

寫入緩衝

順序寫入磁碟並不足以保證高效寫入,假設先向地址 A 寫入一個塊,然後等待一會兒再向磁碟寫入地址 A+1。但是往往在第一次和第二次寫入之間,磁碟已經旋轉。為了提高效能,LFS 使用寫入緩衝(write buffering)技術。在寫入磁碟之前,LFS 會跟蹤記憶體中的更新,收到足夠數量的更新時,會立即將它們寫入磁碟,從而確保有效使用磁碟。

inode 查詢

老 UNIX 檔案系統將所有 inode 儲存在磁碟的固定位置,因此要查詢特定的 inode 可以基於陣列的索引直接找到。而 FFS 將 inode 表拆分為塊並在每個柱面組中放置一組 inode,因此在 FFS 中查詢給定 inode 號時必須知道每個 inode 塊的大小和每個 inode 的起始地址,也很容易。
在 LFS 中查詢 inode 比較艱難,因為 inode 分散在整個磁碟上且永遠不會覆蓋,因此最新版本的 inode 會不斷移動。為了解決這個問題,LFS 使用 inode 對映(inode map, imap)這種資料結構,在 inode 號和 inode 之間引入了一個間接層,在此處 imap 將 inode 號作為輸入,並生成最新版本的 inode 的磁碟地址。

每次將 inode 寫入磁碟時,imap 都需要對 inode 的新位置進行更新,imap 在磁碟上的位置選擇也很重要。LFS 將 inode 對映的塊放在它寫入所有其他新資訊的位置旁邊,當修改檔案塊時,inode 和一段 inode 對映會一起寫入磁碟。
檔案系統必須在磁碟上有一些固定且已知的位置,才能開始檔案查詢,LFS 在磁碟上設定了固定的檢查點區域(checkpoint region, CR)。檢查點區域包含指向最新的 inode 對映片段的指標,因此可以通過首先讀取 CR 來找到 inode 對映片段。

垃圾收集

LFS 存在會反覆將最新版本的檔案寫入磁碟上的新位置的問題,這意味著 LFS 會在整個磁碟中分散舊版本的檔案結構,稱為垃圾(garbage)。例如一個檔案加入了新的資料,在磁碟上佔有 2 個數據塊,同時會寫入一個新的 inode 指向該資料。但是由於舊版本的 inode 依然存在,因此檔案之前佔有的資料塊仍然被舊的 inode 指向。因此 LFS 必須定期清理索引節點和其他結構的舊版本,使磁碟上的塊再次空閒,以便在後續寫入中使用。

但是如果 LFS 清理程式在清理過程中只是簡單地並釋放單個數據塊,會導致檔案系統在磁碟上分配的空間之間混合了一些空閒洞(hole)。此時 LFS 無法找到一個大塊連續區域寫入,所以寫入效能會大幅下降。
所以 LFS 清理程式按段工作,程式定期讀入許多舊的段,確定哪些塊在這些段中還活著,然後寫出一組新的段存放其中活著的塊,然後釋放舊塊用於寫入。為了檢驗是否是舊版本,LFS 在使用了版本號來幫助實現。

崩潰恢復

在正常操作期間 LFS 在日誌(log)中組織寫入操作,將一些寫入緩衝在段中,然後當段已滿或經過一段時間後將段寫入磁碟。如果寫入段時發生了崩潰,在重新啟動時 LFS 可以通過簡單地讀取檢查點區域、它指向的 imap 片段以及後續檔案和目錄,從而實現恢復。
但是在一個檢查點更新的週期中發生的更新無法通過上述方式恢復,所以 LFS 嘗試前滾(roll forward)*的技術重建段。基本思想是從最後一個檢查點區域開始,找到日誌的結尾,然後使用它來讀取下一個段,並檢視其中是否有任何有效更新。如果有,LFS 會相應地更新檔案系統,從而恢復自上一個檢查點以來寫入的大部分資料和元資料。

參考資料

《作業系統導論》[美]Remzi H.Arpaci-Dusseau,Andrea C.Arpaci-Dusseau 著,王海鵬 譯,人民郵電出版社