4. XFS 線上 Fsck 設計

本文件記錄了 XFS 線上檔案系統檢查功能的設計。本文件的目的有三個:

  • 幫助核心發行商準確瞭解 XFS 線上 fsck 功能是什麼,以及他們應該注意的問題。

  • 幫助閱讀程式碼的人員在開始深入研究程式碼之前,熟悉相關的概念和設計要點。

  • 透過捕獲支援更高層次決策的原因,幫助開發人員維護系統。

隨著線上 fsck 程式碼的合併,本文件中主題分支的連結將被替換為程式碼連結。

本文件根據 GNU 通用公共許可證 v2 的條款獲得許可。主要作者是 Darrick J. Wong。

本設計文件分為七個部分。第 1 部分定義了 fsck 工具是什麼,以及編寫新工具的動機。第 2 部分和第 3 部分概述了線上 fsck 程序的工作原理以及如何對其進行測試以確保功能正確。第 4 部分討論了使用者介面以及新程式的預期使用模式。第 5 部分和第 6 部分展示了高階元件以及它們如何組合在一起,然後提出了每個修復功能實際工作方式的案例研究。第 7 部分總結了到目前為止所討論的內容,並推測了可以在線上 fsck 之上構建的其他內容。

4.1. 1. 什麼是檔案系統檢查?

Unix 檔案系統有四個主要職責:

  • 提供一個名稱層次結構,應用程式可以透過該層次結構將任意資料 blob 關聯任意時長,

  • 在這些名稱上虛擬化物理儲存介質,以及

  • 隨時檢索命名的資料 blob。

  • 檢查資源使用情況。

直接支援這些功能的元資料(例如,檔案、目錄、空間對映)有時稱為主要元資料。輔助元資料(例如,反向對映和目錄父指標)支援檔案系統內部的操作,例如內部一致性檢查和重組。顧名思義,摘要元資料壓縮主要元資料中包含的資訊,以提高效能。

檔案系統檢查 (fsck) 工具檢查檔案系統中的所有元資料,以查詢錯誤。除了查詢明顯的元資料損壞之外,fsck 還會將不同型別的元資料記錄相互交叉引用,以查詢不一致之處。人們不喜歡丟失資料,因此大多數 fsck 工具還包含一些糾正發現的任何問題的能力。作為一個警告 - 大多數 Linux fsck 工具的主要目標是將檔案系統元資料恢復到一致狀態,而不是最大化恢復的資料。這個先例不會在這裡受到挑戰。

20 世紀的檔案系統通常在磁碟格式中缺少任何冗餘,這意味著 fsck 只能透過擦除檔案直到不再檢測到錯誤來響應錯誤。最近的檔案系統設計在其元資料中包含足夠的冗餘,現在可以在發生非災難性錯誤時重新生成資料結構;這種能力有助於兩種策略。

注意:

系統管理員透過建立備份來增加獨立儲存系統的數量來避免資料丟失;他們透過建立 RAID 陣列來增加每個儲存系統的冗餘來避免停機。fsck 工具僅解決第一個問題。

4.1.1. TLDR;給我看程式碼!

程式碼釋出到 kernel.org git 樹,如下所示:核心更改使用者空間更改QA 測試更改。每個新增線上修復功能的核心補丁集將在核心、xfsprogs 和 fstests git 儲存庫中使用相同的分支名稱。

4.1.2. 現有工具

此處描述的線上 fsck 工具將是 XFS(在 Linux 上)歷史上第三個檢查和修復檔案系統的工具。有兩個程式在它之前:

第一個程式 xfs_check 作為 XFS 偵錯程式 (xfs_db) 的一部分建立,只能與未掛載的檔案系統一起使用。它遍歷檔案系統中的所有元資料,查詢元資料中的不一致之處,但它沒有任何修復發現的內容的能力。由於其高記憶體要求和無法修復任何東西,此程式現已棄用,將不再進一步討論。

第二個程式 xfs_repair 的建立是為了比第一個程式更快更健壯。像它的前身一樣,它只能與未掛載的檔案系統一起使用。它使用基於 extent 的記憶體資料結構來減少記憶體消耗,並嘗試適當地安排預讀 IO,以減少在掃描整個檔案系統的元資料時的 I/O 等待時間。此工具最重要的功能是它能夠透過根據需要擦除事物來消除問題,從而響應檔案元資料和目錄樹中的不一致之處。空間使用元資料從觀察到的檔案元資料重建。

4.1.3. 問題陳述

當前的 XFS 工具留下了一些未解決的問題:

  1. 當由於元資料中的靜默損壞而發生意外關閉時,**使用者程式** 會突然 **失去對檔案系統的訪問許可權**。這些發生是 **不可預測的**,通常沒有警告。

  2. 在 **意外關閉** 發生後的恢復期間,**使用者** 會遇到 **完全的服務中斷**。

  3. 如果檔案系統離線以 **主動查詢問題**,**使用者** 會遇到 **完全的服務中斷**。

  4. **資料所有者** 無法 **檢查** 其儲存資料的 **完整性**,而無需讀取所有資料。當儲存系統管理員執行的線性媒體掃描可能就足夠時,這可能會使他們承擔大量的計費成本。

  5. 如果 **缺乏** 在檔案系統線上時評估檔案系統健康狀況的 **手段**,**系統管理員** 無法 **計劃** 一個維護視窗來處理損壞。

  6. 當這樣做需要 **手動干預** 和停機時,**艦隊監控工具** 無法 **自動化定期檢查** 檔案系統健康狀況。

  7. 當惡意行為者 **利用 Unicode 的怪癖** 在目錄中放置誤導性名稱時,**使用者** 可能會被欺騙 **做他們不希望做的事情**。

鑑於要解決的問題的定義以及將受益的行為者,建議的解決方案是第三個 fsck 工具,它作用於正在執行的檔案系統。

這個新的第三個程式有三個元件:一個用於檢查元資料的核心設施,一個用於修復元資料的核心設施,以及一個用於驅動活動檔案系統上的 fsck 活動的使用者空間驅動程式。xfs_scrub 是驅動程式程式的名稱。本文件的其餘部分介紹了新 fsck 工具的目標和用例,描述了與其目標相關的其主要設計要點,並討論了與現有工具的異同。

注意:

在本文件中,現有的離線 fsck 工具也可以用其當前名稱“xfs_repair”來稱呼。用於新的線上 fsck 工具的使用者空間驅動程式可以被稱為“xfs_scrub”。線上 fsck 的核心部分,用於驗證元資料,稱為“線上清理”,而核心中用於修復元資料的部分稱為“線上修復”。

命名層次結構被分解為稱為目錄和檔案的物件,物理空間被分成稱為分配組的塊。分片使高度並行系統能夠獲得更好的效能,並有助於控制發生損壞時的損失。將檔案系統劃分為主要物件(分配組和 inode)意味著有很多機會可以對檔案系統的子集執行定向檢查和修復。

當這種情況發生時,其他部分會繼續處理 IO 請求。即使只能透過掃描整個系統來重新生成一條檔案系統元資料,也可以在後臺完成掃描,而其他檔案操作繼續進行。

總之,線上 fsck 利用資源分片和冗餘元資料來啟用在系統執行時對目標進行檢查和修復操作。此功能將與自動系統管理相結合,以便 XFS 的自主自愈能夠最大限度地提高服務可用性。

4.2. 2. 操作理論

由於線上 fsck 需要鎖定和掃描即時元資料物件,因此線上 fsck 由三個單獨的程式碼元件組成。第一個是使用者空間驅動程式程式 xfs_scrub,它負責識別單個元資料項,為其安排工作項,適當地響應結果,並向系統管理員報告結果。第二個和第三個在核心中,它實現了檢查和修復每種型別的線上 fsck 工作項的功能。

注意:

為簡潔起見,本文件將短語“線上 fsck 工作項”縮短為“清理項”。

清理項型別的劃分方式與 Unix 設計理念一致,也就是說,每個項都應該處理元資料結構的一個方面,並且處理得很好。

4.2.1. 範圍

原則上,線上 fsck 應該能夠檢查和修復離線 fsck 程式可以處理的所有內容。但是,線上 fsck 不能 100% 的時間都在執行,這意味著在清理完成後可能會潛入潛在的錯誤。如果這些錯誤導致下次掛載失敗,則離線 fsck 是唯一的解決方案。此限制意味著離線 fsck 工具的維護將繼續進行。線上 fsck 的第二個限制是它必須遵循與常規檔案系統相同的資源共享和鎖定獲取規則。這意味著清理不能採取*任何*捷徑來節省時間,因為這樣做可能會導致併發問題。換句話說,線上 fsck 不是離線 fsck 的完整替代品,並且線上 fsck 的完整執行可能比線上 fsck 需要更長的時間。但是,這兩個限制都是可以接受的權衡,以滿足線上 fsck 的不同動機,即 **最大限度地減少系統停機時間** 和 **提高操作的可預測性**。

4.2.2. 工作階段

使用者空間驅動程式程式 xfs_scrub 將檢查和修復整個檔案系統的工作分為七個階段。每個階段都專注於檢查特定型別的清理項,並取決於所有先前階段的成功。這七個階段如下:

  1. 收集有關已掛載檔案系統和計算機的幾何資訊,發現核心的線上 fsck 功能,並開啟底層儲存裝置。

  2. 檢查分配組元資料、所有即時卷元資料和所有配額檔案。每個元資料結構都計劃為一個單獨的清理項。如果在 inode 標頭或 inode b 樹中發現損壞,並且允許 xfs_scrub 執行修復,則會修復這些清理項以準備第 3 階段。透過使用清理項中的資訊重新提交啟用了修復標誌的核心清理呼叫來實現修復;這將在下一節中討論。最佳化和所有其他修復將推遲到第 4 階段。

  3. 檢查檔案系統中每個檔案的所有元資料。每個元資料結構也計劃為一個單獨的清理項。如果需要修復並且允許 xfs_scrub 執行修復,並且在第 2 階段未檢測到任何問題,則會立即修復這些清理項。最佳化、延遲修復和未成功的修復將推遲到第 4 階段。

  4. 如果在呼叫者允許的情況下,在此階段執行所有剩餘的修復和計劃的最佳化。在開始修復之前,會檢查摘要計數器並執行任何必要的修復,以便後續修復不會因摘要計數器嚴重不正確而導致資源預留步驟失敗。如果在檔案系統乾淨的情況下,會在第 4 階段結束時修剪檔案系統中的可用空間。

  5. 到此階段開始時,所有主要和輔助檔案系統元資料必須正確。檢查並更正摘要計數器,例如可用空間計數和配額資源計數。檢查目錄條目名稱和擴充套件屬性名稱中是否有可疑條目,例如名稱中出現的控制字元或令人困惑的 Unicode 序列。

  6. 如果呼叫者要求進行媒體掃描,則讀取檔案系統中所有已分配和寫入的資料檔案範圍。使用硬體輔助的資料檔案完整性檢查是線上 fsck 的新功能;以前的任何工具都不具備此功能。如果發生媒體錯誤,它們將被對映到所屬檔案並報告。

  7. 重新檢查摘要計數器,並向呼叫者提供空間使用情況和檔案計數的摘要。

此責任分配將在本文件後面 重新審視

4.2.3. 每個清理項的步驟

核心清理程式碼使用三步策略來檢查和修復由清理項表示的元資料物件的一個方面:

  1. 檢查感興趣的清理項是否有損壞、最佳化機會和由系統管理員直接控制但看起來可疑的值。如果該項未損壞或不需要最佳化,則會釋放資源,並將肯定的掃描結果返回到使用者空間。如果該項已損壞或可以最佳化但呼叫者不允許,則會釋放資源,並將否定的掃描結果返回到使用者空間。否則,核心將繼續執行第二步。

  2. 呼叫修復函式以重建資料結構。修復函式通常選擇從其他元資料重建結構,而不是嘗試挽救現有結構。如果修復失敗,則會將第一步的掃描結果返回到使用者空間。否則,核心將繼續執行第三步。

  3. 在第三步中,核心對新的元資料項執行相同的檢查,以評估修復的有效性。重新評估的結果將返回到使用者空間。

4.2.4. 元資料分類

每種型別的元資料物件(以及因此每種型別的清理項)都分類如下:

4.2.4.1. 主要元資料

此類別中的元資料結構對於檔案系統使用者來說應該最熟悉,因為它們是由使用者直接建立的,或者它們索引由使用者建立的物件。大多數檔案系統物件都屬於此類:

  • 可用空間和引用計數資訊

  • Inode 記錄和索引

  • 檔案資料的儲存對映資訊

  • 目錄

  • 擴充套件屬性

  • 符號連結

  • 配額限制

清理在資源和鎖獲取方面遵守與常規檔案系統訪問相同的規則。

主要元資料物件是清理最容易處理的。鎖定擁有要清理的項的主檔案系統物件(分配組或 inode),以防止併發更新。檢查函式檢查與該型別關聯的每個記錄是否存在明顯錯誤,並將健康的記錄與另一個元資料交叉引用,以查詢不一致之處。此類清理項的修復很簡單,因為修復函式從保留在上一步中獲取的所有資源開始。修復函式會根據需要掃描可用的元資料,以記錄完成結構所需的所有觀察結果。接下來,它將觀察結果分段到一個新的磁碟結構中,並以原子方式提交它以完成修復。最後,小心地收割舊資料結構的儲存。

因為 xfs_scrub 在修復期間鎖定一個主要物件,所以這實際上是在檔案系統的子集上執行的離線修復操作。這最大限度地降低了修復程式碼的複雜性,因為它不需要處理來自其他執行緒的併發更新,也不需要訪問檔案系統的任何其他部分。因此,可以非常快速地重建索引結構,並且嘗試訪問損壞結構的程式將被阻止,直到修復完成。修復程式碼唯一需要的基礎設施是觀察結果的分段區域和將新結構寫入磁碟的手段。儘管存在這些限制,但線上修復的優勢是顯而易見的:對檔案系統的各個分片進行有針對性的工作可以避免完全的服務中斷。

此機制在 V. Srinivasan 和 M. J. Carey 的第 2.1 節(“離線演算法”)中描述,“線上索引構建演算法的效能”擴充套件資料庫技術,第 293-309 頁,1992 年。

大多數主要元資料修復函式在格式化新的磁碟結構之前,會在記憶體陣列中分段其中間結果,這與 Srinivasan 的第 2.3 節(“基於列表的演算法”)中討論的基於列表的演算法非常相似。但是,任何在修復期間維護資源鎖定的資料結構構建器*始終*是一種離線演算法。

4.2.4.2. 輔助元資料

此類別中的元資料結構反映了在主要元資料中找到的記錄,但僅用於線上 fsck 或檔案系統的重組。

輔助元資料包括:

  • 反向對映資訊

  • 目錄父指標

此類元資料對於清理來說很難處理,因為清理會附加到輔助物件,但需要檢查主要元資料,這與通常的資源獲取順序相反。通常,這意味著需要進行完整的檔案系統掃描才能重建元資料。檢查函式的範圍可以限制以減少執行時。但是,修復需要對主要元資料進行完整掃描,這可能需要很長時間才能完成。在這些條件下,xfs_scrub 不能在整個修復期間鎖定資源。

相反,修復函式會設定一個記憶體中的分段結構來儲存觀察結果。根據特定修復函式的要求,分段索引將具有與磁碟結構相同的格式或特定於該修復函式的設計。下一步是釋放所有鎖並啟動檔案系統掃描。當修復掃描程式需要記錄觀察結果時,會鎖定分段資料足夠長的時間以應用更新。當檔案系統掃描正在進行時,修復函式會鉤住檔案系統,以便它可以將掛起的檔案系統更新應用於分段資訊。掃描完成後,將重新鎖定所屬物件,使用即時資料寫入新的磁碟結構,並以原子方式提交修復。停用鉤子並釋放分段區域。最後,小心地收割舊資料結構的儲存。

引入併發有助於線上修復避免各種鎖定問題,但代價是程式碼複雜性很高。必須鉤住即時檔案系統程式碼,以便修復函式可以觀察到正在進行的更新。分段區域必須成為一個功能齊全的並行結構,以便可以從鉤子合併更新。最後,鉤子、檔案系統掃描和 inode 鎖定模型必須充分整合,以便鉤子事件可以決定是否應將給定的更新應用於分段結構。

從理論上講,清理實現可以將這些相同的技術應用於主要元資料,但這樣做會使其更加複雜且效能更低。嘗試訪問損壞結構的程式不會被阻止執行,這可能會導致應用程式失敗或計劃外的檔案系統關閉。

輔助元資料修復策略的靈感來自上述 Srinivasan 的第 2.4 節以及 C. Mohan 的第 2 節(“NSF:無邊檔案索引構建”)和第 3.1.1 節(“重複鍵插入問題”),“在不暫停更新的情況下為非常大的表建立索引的演算法”,1992 年。

上面提到的側車索引與 Srinivasan 和 Mohan 中提到的側檔案方法有些相似。他們的方法包括一個索引構建器,它提取相關的記錄資料以儘可能快地構建新結構;以及一個輔助結構,用於捕獲所有更新,這些更新將被其他執行緒提交到索引,如果新索引已線上。在索引構建掃描完成後,側檔案中記錄的更新將應用於新索引。為了避免索引構建器和其他寫入執行緒之間的衝突,構建器維護一個公開可見的遊標,該遊標跟蹤掃描在記錄空間中的進度。為了避免側檔案和索引構建器之間的重複工作,當更新的記錄 ID 大於記錄 ID 空間內的遊標位置時,將省略側檔案更新。

為了最大限度地減少對程式碼庫其餘部分的更改,XFS 線上修復會將替換索引隱藏起來,直到它完全準備好。換句話說,在修復執行時,不會嘗試公開新索引的鍵空間。這種方法的複雜性會非常高,並且可能更適合構建新的索引。

未來工作問題:用於促進修復的完整掃描和即時更新程式碼是否也可以用於實現全面的檢查?

答案:理論上可以。如果每個清理函式都使用這些即時掃描來構建元資料的影子副本,然後將影子記錄與磁碟上的記錄進行比較,則檢查會更強大。但是,這樣做比檢查函式現在所做的工作要多得多。即時掃描和鉤子是後來開發的。反過來,這會增加這些清理函式的執行時。

4.2.4.3. 摘要資訊

最後一個類別中的元資料結構總結了主要元資料記錄的內容。這些通常用於加速資源使用查詢,並且比它們表示的主要元資料小很多倍。

摘要資訊的示例包括

  • 可用空間和 inode 的摘要計數

  • 來自目錄的檔案連結計數

  • 配額資源使用計數

檢查和修復需要完整的系統掃描,但資源和鎖的獲取遵循與常規檔案系統訪問相同的路徑。

由於核心計數器的底層實現,超級塊摘要計數器具有特殊要求,將單獨處理。其他型別的摘要計數器(配額資源計數和檔案連結計數)的檢查和修復採用與上述相同的系統掃描和掛鉤技術,但由於底層資料是整數計數器集,因此暫存資料不必是磁碟結構的完整映象。

配額和檔案連結計數修復策略的靈感來自 G. Graefe 的 2.12 節(“線上索引操作”)到 2.14 節(“增量檢視維護”),“併發查詢和更新在摘要檢視及其索引中”,2011 年。

由於配額是非負整數的資源使用計數,因此線上配額檢查可以使用 2.14 節中描述的增量檢視增量來跟蹤每個事務中對塊和 inode 使用計數的待處理更改,並在事務提交時將這些更改提交到 dquot 側檔案。增量跟蹤對於 dquot 是必要的,因為索引構建器掃描 inode,而正在重建的資料結構是 dquot 的索引。連結計數檢查將檢視增量和提交步驟合併為一個,因為它設定了正在掃描的物件的屬性,而不是將它們寫入單獨的資料結構。每個線上 fsck 函式將在本文件後面的案例研究中進行討論。

4.2.5. 風險管理

在線上 fsck 的開發過程中,識別出幾個風險因素,這些因素可能使該功能不適合某些分銷商和使用者。可以採取措施來減輕或消除這些風險,儘管會犧牲一些功能。

  • 效能下降:向檔案系統新增元資料索引會增加將更改持久儲存到磁碟的時間成本,反向空間對映和目錄父指標也不例外。需要最大效能的系統管理員可以在格式化時停用反向對映功能,儘管這種選擇會大大降低線上 fsck 查詢不一致並修復它們的能力。

  • 不正確的修復:與所有軟體一樣,軟體中可能存在缺陷,導致將不正確的修復寫入檔案系統。作者採用系統模糊測試(在下一節中詳細介紹)來儘早發現錯誤,但它可能無法捕獲所有內容。核心構建系統提供了 Kconfig 選項 (CONFIG_XFS_ONLINE_SCRUBCONFIG_XFS_ONLINE_REPAIR) 以使分銷商可以選擇不接受此風險。xfsprogs 構建系統有一個配置選項 (--enable-scrub=no),它停用 xfs_scrub 二進位制檔案的構建,但如果核心功能保持啟用狀態,則這不是風險緩解。

  • 無法修復:有時,檔案系統損壞太嚴重而無法修復。如果幾個元資料索引的鍵空間以某種方式重疊,但無法從收集的記錄中形成連貫的敘述,則修復失敗。為了減少修復因髒事務而失敗並導致檔案系統無法使用的可能性,線上修復功能已被設計為在提交新結構之前暫存和驗證所有新記錄。

  • 行為不端:線上 fsck 需要許多許可權——對塊裝置的原始 IO、按控制代碼開啟檔案、忽略 Unix 自主訪問控制以及執行管理更改的能力。在後臺自動執行此操作會讓人感到害怕,因此 systemd 後臺服務配置為僅使用所需的許可權執行。顯然,這無法解決某些問題,例如核心崩潰或死鎖,但它應該足以防止清理過程逃逸並重新配置系統。cron 作業沒有這種保護。

  • 模糊小子:現在似乎有很多人認為執行對磁碟上工件的自動模糊測試以發現惡作劇行為並將漏洞利用程式碼噴灑到公共郵件列表中以進行即時零日披露在某種程度上具有社會效益。在作者看來,只有當模糊操作員幫助修復缺陷時,才能實現利益,但這種觀點顯然並未在安全“研究人員”中廣泛分享。XFS 維護者持續管理這些事件的能力對開發過程的穩定性構成了持續的風險。在將該功能視為實驗性時,自動化測試應提前載入一些風險。

許多這些風險是軟體程式設計固有的。儘管如此,希望這個新功能能證明在減少意外停機時間方面有用。

4.3. 3. 測試計劃

如前所述,fsck 工具具有三個主要目標

  1. 檢測元資料中的不一致;

  2. 消除這些不一致;和

  3. 儘量減少進一步的資料丟失。

證明正確操作對於建立使用者對軟體在預期範圍內執行的信心是必要的。不幸的是,直到引入具有高 IOPS 儲存的低成本虛擬機器之前,對 fsck 工具的每個方面執行常規的詳盡測試實際上是不可行的。考慮到充足的硬體可用性,線上 fsck 專案的測試策略涉及針對現有 fsck 工具進行差異分析,以及對每種型別的元資料物件的每個屬性進行系統測試。測試可以分為四個主要類別,如下所述。

4.3.1. 與 fstests 的整合測試

任何免費軟體 QA 工作的首要目標是使測試儘可能便宜和廣泛,以最大限度地提高社群的規模優勢。換句話說,測試應最大限度地擴大檔案系統配置場景和硬體設定的範圍。這透過使線上 fsck 的作者能夠儘早發現和修復錯誤來提高程式碼質量,並幫助新功能的開發人員在開發工作中儘早發現整合問題。

Linux 檔案系統社群共享一個通用的 QA 測試套件,fstests,用於功能和迴歸測試。甚至在開始線上 fsck 的開發工作之前,fstests(在 XFS 上執行時)會在每次測試之間對測試和臨時檔案系統執行 xfs_checkxfs_repair -n 命令。這提供了一定程度的保證,即核心和 fsck 工具在什麼構成一致的元資料方面保持一致。在線上檢查程式碼的開發過程中,fstests 被修改為在每次測試之間執行 xfs_scrub -n,以確保新的檢查程式碼產生與兩個現有 fsck 工具相同的結果。

要開始線上修復的開發,fstests 被修改為執行 xfs_repair 以在測試之間重建檔案系統的元資料索引。這確保了離線修復不會崩潰,在退出後不會留下損壞的檔案系統,也不會觸發來自線上檢查的投訴。這也建立了可以和不能離線修復的基線。要完成線上修復的第一個開發階段,fstests 被修改為能夠以“強制重建”模式執行 xfs_scrub。這使得可以比較線上修復與現有離線修復工具的有效性。

4.3.2. 元資料塊的一般模糊測試

XFS 受益於擁有一個非常強大的除錯工具,xfs_db

甚至在線上 fsck 的開發開始之前,就建立了一組 fstests 來測試整個元資料塊損壞的相當常見的故障。這需要建立 fstests 庫程式碼,該程式碼可以建立包含每種可能的元資料物件型別的檔案系統。接下來,建立了單個測試用例來建立測試檔案系統,識別特定型別的元資料物件的單個塊,使用 xfs_db 中的現有 blocktrash 命令將其刪除,並測試特定元資料驗證策略的反應。

這個早期的測試套件使 XFS 開發人員能夠測試核心驗證功能和離線 fsck 工具檢測和消除不一致元資料的能力。測試套件的這一部分以完全相同的方式擴充套件到涵蓋線上 fsck。

換句話說,對於給定的 fstests 檔案系統配置

  • 對於檔案系統上存在的每個元資料物件

    • 將垃圾寫入其中

    • 測試以下反應

      1. 核心驗證程式停止明顯錯誤的元資料

      2. 離線修復 (xfs_repair) 檢測和修復

      3. 線上修復 (xfs_scrub) 檢測和修復

4.3.3. 元資料記錄的定向模糊測試

線上 fsck 的測試計劃包括擴充套件現有的 fs 測試基礎設施,以提供更強大的功能:對檔案系統中每個元資料物件的每個元資料欄位進行定向模糊測試。xfs_db 可以修改檔案系統中每個塊的每個元資料結構的每個欄位,以模擬記憶體損壞和軟體錯誤的影響。鑑於 fstests 已經包含建立包含檔案系統已知的所有元資料格式的檔案系統的能力,xfs_db 可用於執行詳盡的模糊測試!

對於給定的 fstests 檔案系統配置

  • 對於檔案系統上存在的每個元資料物件...

    • 對於該元資料物件中的每個記錄...

      • 對於該記錄中的每個欄位...

        • 對於可以應用於位欄位的每種可能的轉換型別...

          1. 清除所有位

          2. 設定所有位

          3. 切換最高有效位

          4. 切換中間位

          5. 切換最低有效位

          6. 新增少量

          7. 減去少量

          8. 隨機化內容

          • ...測試以下反應

            1. 核心驗證程式停止明顯錯誤的元資料

            2. 離線檢查 (xfs_repair -n)

            3. 離線修復 (xfs_repair)

            4. 線上檢查 (xfs_scrub -n)

            5. 線上修復 (xfs_scrub)

            6. 兩種修復工具 (xfs_scrub,如果線上修復不成功,則 xfs_repair)

這是一個相當大的組合爆炸!

幸運的是,擁有如此多的測試覆蓋率使 XFS 開發人員可以輕鬆檢查 XFS 的 fsck 工具的響應。自從引入模糊測試框架以來,這些測試已被用於發現 xfs_repair 中整個元資料物件類別的不正確的修復程式碼和缺少的功能。增強的測試用於最終確定 xfs_check 的棄用,方法是確認 xfs_repair 可以檢測到至少與舊工具一樣多的損壞。

這些測試對於 xfs_scrub 來說非常有價值——它們允許線上 fsck 開發人員將線上 fsck 與離線 fsck 進行比較,並且它們使 XFS 開發人員可以發現程式碼庫中的缺陷。

提議的補丁集包括 常規模糊器改進模糊基線模糊測試全面性的改進

4.3.4. 壓力測試

線上 fsck 的一個獨特要求是能夠在與常規工作負載併發的檔案系統上執行。儘管當然不可能在執行系統上以 可觀察到的影響執行 xfs_scrub,但線上修復程式碼永遠不應將不一致引入檔案系統元資料,並且常規工作負載永遠不應注意到資源匱乏。為了驗證是否滿足這些條件,fstests 已透過以下方式得到增強

  • 對於每個清理專案型別,建立一個測試以在執行 fsstress 時執行檢查該專案型別。

  • 對於每個清理專案型別,建立一個測試以在執行 fsstress 時執行修復該專案型別。

  • 競爭 fsstressxfs_scrub -n 以確保檢查整個檔案系統不會導致問題。

  • 競爭 fsstressxfs_scrub 以強制重建模式確保強制修復整個檔案系統不會導致問題。

  • 在凍結和解凍檔案系統時,競爭以檢查和強制修復模式的 xfs_scrubfsstress

  • 在以只讀和讀寫方式重新掛載檔案系統時,競爭以檢查和強制修復模式的 xfs_scrubfsstress

  • 相同,但執行 fsx 而不是 fsstress。(尚未完成?)

成功定義為能夠在不觀察到由於損壞的元資料、核心掛起檢查警告或任何其他型別的惡作劇而導致的任何意外的檔案系統關閉的情況下執行所有這些測試。

提議的補丁集包括 常規壓力測試現有每個功能的壓力測試的演變

4.4. 4. 使用者介面

與離線修復一樣,線上 fsck 的主要使用者是系統管理員。線上 fsck 向管理員提供兩種操作模式:按需線上 fsck 的前臺 CLI 程序,以及執行自主檢查和修復的後臺服務。

4.4.1. 按需檢查

對於想要了解檔案系統中元資料的最新資訊的管理員,可以在命令列上作為前臺程序執行 xfs_scrub。該程式檢查檔案系統中的每一段元資料,同時管理員等待報告結果,就像現有的 xfs_repair 工具一樣。兩種工具都共享一個 -n 選項來執行只讀掃描,以及一個 -v 選項來增加報告的資訊的詳細程度。

xfs_scrub 的一個新功能是 -x 選項,它利用硬體的糾錯功能來檢查資料檔案內容。預設情況下不啟用介質掃描,因為它可能會大大增加程式執行時並消耗舊儲存硬體上的大量頻寬。

前臺呼叫的輸出捕獲在系統日誌中。

xfs_scrub_all 程式遍歷已掛載檔案系統的列表,並並行啟動每個檔案系統的 xfs_scrub。它序列化任何解析為同一頂級核心塊裝置的檔案系統的掃描,以防止過度消耗資源。

4.4.2. 後臺服務

為了減少系統管理員的工作量,xfs_scrub 軟體包提供了一套 systemd 定時器和服務,預設情況下在週末自動執行線上 fsck。後臺服務配置為以儘可能少的許可權、最低的 CPU 和 IO 優先順序以及受 CPU 限制的單執行緒模式執行清理。系統管理員可以隨時對其進行調整,以適應客戶工作負載的延遲和吞吐量要求。

後臺服務的輸出也捕獲在系統日誌中。如果需要,可以透過在以下服務檔案中設定 EMAIL_ADDR 環境變數來自動透過電子郵件傳送故障報告(由於不一致或僅僅是執行時錯誤)

  • xfs_scrub_fail@.service

  • xfs_scrub_media_fail@.service

  • xfs_scrub_all_fail.service

啟用後臺掃描的決定留給系統管理員。這可以透過啟用以下任何服務來完成

  • xfs_scrub_all.timer 在 systemd 系統上

  • xfs_scrub_all.cron 在非 systemd 系統上

這種自動每週掃描配置為每月對所有檔案資料執行一次額外的介質掃描。這不如儲存檔案資料塊校驗和那麼可靠,但如果應用程式軟體提供自己的完整性檢查,則效能要好得多,冗餘可以在檔案系統上方的其他地方提供,或者儲存裝置的完整性保證被認為是足夠的。

systemd 單元檔案定義已經過安全稽核(截至 systemd 249),以確保 xfs_scrub 程序儘可能少地訪問系統的其餘部分。這是透過 systemd-analyze security 執行的,之後許可權被限制為所需的最小值,沙盒被設定為儘可能的最大程度,並進行沙盒和系統呼叫過濾;並且對檔案系統樹的訪問被限制為啟動程式和訪問正在掃描的檔案系統所需的最小值。服務定義檔案將 CPU 使用率限制為 80% 的一個 CPU 核心,並儘可能對 IO 和 CPU 排程應用不錯的優先順序。採取此措施是為了最大限度地減少檔案系統其餘部分的延遲。沒有為 cron 作業執行此類強化。

提議的補丁集:啟用 xfs_scrub 後臺服務

4.4.3. 健康狀況報告

XFS 在記憶體中快取每個檔案系統健康狀況的摘要。每當執行 xfs_scrub 時,或者在常規操作期間檢測到檔案系統元資料中的不一致時,都會更新資訊。系統管理員應使用 xfs_spacemanhealth 命令將此資訊下載到人類可讀的格式中。如果觀察到問題,管理員可以安排一個縮短的服務視窗來執行線上修復工具以糾正問題。否則,管理員可以決定安排一個維護視窗來執行傳統的離線修復工具以糾正問題。

未來工作問題:健康狀況報告是否應與新的 inotify fs 錯誤通知系統整合?對於系統管理員來說,擁有一個守護程式來偵聽損壞通知並啟動修復是否有幫助?

答案:這些問題仍未得到解答,但應成為與 XFS 的早期採用者和潛在的下游使用者對話的一部分。

提議的補丁集包括 將健康狀況報告連線到更正返回在記憶體回收期間保留疾病資訊

4.5. 5. 核心演算法和資料結構

本節討論核心程式碼的關鍵演算法和資料結構,這些程式碼提供在系統執行時檢查和修復元資料的能力。本節的前幾章揭示了為檢查元資料提供基礎的部分。本節的其餘部分介紹了 XFS 自我再生的機制。

4.5.1. 自描述元資料

從 2012 年的 XFS 版本 5 開始,XFS 更新了幾乎每個磁碟上塊頭的格式,以記錄幻數、校驗和、通用“唯一”識別符號 (UUID)、所有者程式碼、塊的磁碟上地址以及日誌序列號。從磁碟載入塊緩衝區時,幻數、UUID、所有者和磁碟上地址確認檢索到的塊與當前檔案系統的特定所有者匹配,並且塊中包含的資訊應該位於磁碟上地址。前三個元件使檢查工具能夠忽略不屬於檔案系統的所謂元資料,第四個元件使檔案系統能夠檢測丟失的寫入。

每當檔案系統操作修改一個塊時,該更改將作為事務的一部分提交到日誌。然後,日誌處理這些事務,在它們安全地持久儲存到儲存後,將它們標記為完成。日誌記錄程式碼維護最後一個事務更新的校驗和和日誌序列號。校驗和對於檢測撕裂的寫入以及計算機及其儲存裝置之間可能引入的其他差異非常有用。序列號跟蹤使日誌恢復能夠避免將過時的日誌更新應用於檔案系統。

這兩個功能透過提供一種在從磁碟讀取元資料塊時檢測明顯損壞的方法來提高整體執行時彈性,但是這些緩衝區驗證程式無法提供元資料結構之間的任何一致性檢查。

有關更多資訊,請參閱 XFS 自描述元資料 的文件

4.5.2. 反向對映

XFS 的原始設計(大約 1993 年)是對 1980 年代 Unix 檔案系統設計的改進。在那些日子裡,儲存密度昂貴,CPU 時間稀缺,過度的尋道時間可能會扼殺效能。出於效能原因,檔案系統作者不願向檔案系統新增冗餘,即使以犧牲資料完整性為代價。21 世紀初的檔案系統設計者選擇不同的策略來增加內部冗餘——儲存幾乎相同的元資料副本,或更節省空間的編碼技術。

對於 XFS,選擇了一種不同的冗餘策略來使設計現代化:一種輔助空間使用索引,將分配的磁碟範圍映射回其所有者。透過新增一個新的索引,檔案系統保留了其大部分良好擴充套件到涉及大型資料集的重執行緒工作負載的能力,因為主要檔案元資料(目錄樹、檔案塊對映和分配組)保持不變。與任何提高冗餘的系統一樣,反向對映功能會增加空間對映活動的開銷成本。但是,它有兩個關鍵優勢:首先,反向索引是啟用線上 fsck 和其他請求的功能(例如空閒空間碎片整理、更好的介質故障報告和檔案系統收縮)的關鍵。其次,反向對映 btree 的不同磁碟上儲存格式會阻止裝置級重複資料刪除,因為檔案系統需要真正的冗餘。

側邊欄:

對新增輔助索引的批評是它對提高使用者資料儲存本身的健壯性沒有任何作用。這是一個有效的觀點,但是新增用於檔案資料塊校驗和的新索引會透過將資料覆蓋轉換為複製寫入來增加寫入放大,這會使檔案系統過早老化。與三十年的先例保持一致,想要檔案資料完整性的使用者可以提供他們所需的強大解決方案。至於元資料,新增空間使用的新輔助索引的複雜性遠小於將卷管理和儲存裝置映象新增到 XFS 本身。RAID 和卷管理的完善最好留給核心中的現有層。

反向空間對映記錄中捕獲的資訊如下

struct xfs_rmap_irec {
    xfs_agblock_t    rm_startblock;   /* extent start block */
    xfs_extlen_t     rm_blockcount;   /* extent length */
    uint64_t         rm_owner;        /* extent owner */
    uint64_t         rm_offset;       /* offset within the owner */
    unsigned int     rm_flags;        /* state flags */
};

前兩個欄位以檔案系統塊為單位捕獲物理空間的位置和大小。所有者欄位告訴清理程式哪個元資料結構或檔案 inode 已被分配了該空間。對於分配給檔案的空間,偏移量欄位告訴清理程式空間在檔案 fork 中對映的位置。最後,標誌欄位提供有關空間使用的額外資訊——這是屬性 fork 範圍嗎?檔案對映 btree 範圍?還是未寫入的資料範圍?

線上檔案系統檢查透過將其資訊與所有其他空間索引進行比較來判斷每個主要元資料記錄的一致性。反向對映索引在一致性檢查過程中起著關鍵作用,因為它包含所有空間分配資訊的集中式備用副本。程式執行時和資源獲取的容易程度是限制線上檢查可以查閱的唯一真正限制。例如,可以根據以下內容檢查檔案資料範圍對映

  • 空閒空間資訊中缺少條目。

  • inode 索引中缺少條目。

  • 如果檔案未標記為具有共享範圍,則引用計數資料中缺少條目。

  • 反向對映資訊中條目的對應關係。

關於反向對映索引,有幾點需要注意:

  1. 如果對上述任何主要元資料有疑問,反向對映可以提供正確的肯定確認。大多數主要元資料的檢查程式碼都遵循與上述類似的路徑。

  2. 證明次要元資料與主要元資料的一致性是困難的,因為這需要全面掃描所有主要空間元資料,這非常耗時。例如,檢查檔案範圍對映 btree 塊的反向對映記錄需要鎖定檔案並搜尋整個 btree 以確認該塊。相反,scrub 依賴於在主要空間對映結構檢查期間進行嚴格的交叉引用。

  3. 如果所需的鎖定順序與常規檔案系統操作使用的順序不同,則一致性掃描必須使用非阻塞鎖獲取原語。例如,如果檔案系統通常在獲取 AGF 緩衝區鎖之前獲取檔案 ILOCK,但 scrub 想要在持有 AGF 緩衝區鎖的同時獲取檔案 ILOCK,則 scrub 不能阻塞第二次獲取。這意味著,如果系統負載很重,則無法保證反向對映資料掃描的這一部分能夠向前推進。

總之,反向對映在主要元資料的重建中起著關鍵作用。這些記錄如何暫存、寫入磁碟並提交到檔案系統的詳細資訊將在後續章節中介紹。

4.5.3. 檢查和交叉引用

檢查元資料結構的第一步是檢查結構中包含的每個記錄及其與系統其餘部分的關係。XFS 包含多個檢查層,以防止不一致的元資料對系統造成嚴重破壞。每一層都提供資訊,幫助核心對元資料結構的健康狀況做出三個決定:

  • 此結構的某一部分是否明顯損壞 (XFS_SCRUB_OFLAG_CORRUPT)?

  • 此結構是否與系統的其餘部分不一致 (XFS_SCRUB_OFLAG_XCORRUPT)?

  • 檔案系統周圍是否存在太多損壞,以至於無法進行交叉引用 (XFS_SCRUB_OFLAG_XFAIL)?

  • 是否可以最佳化結構以提高效能或減小元資料的大小 (XFS_SCRUB_OFLAG_PREEN)?

  • 結構是否包含不不一致但值得系統管理員審查的資料 (XFS_SCRUB_OFLAG_WARNING)?

以下各節介紹元資料清理過程的工作原理。

4.5.3.1. 元資料緩衝區驗證

XFS 中最低級別的元資料保護是內置於緩衝區快取中的元資料驗證器。這些函式對塊本身執行廉價的內部一致性檢查,並回答以下問題:

  • 該塊是否屬於此檔案系統?

  • 該塊是否屬於請求讀取的結構? 這假設元資料塊只有一個所有者,這在 XFS 中始終為真。

  • 塊中儲存的資料型別是否在 scrub 期望的合理範圍內?

  • 塊的物理位置是否與讀取它的位置匹配?

  • 塊校驗和是否與資料匹配?

此處的保護範圍非常有限——驗證器只能確定檔案系統程式碼是否合理地沒有嚴重的損壞錯誤,並且儲存系統在檢索方面具有合理的效能。在執行時觀察到的損壞問題會導致生成健康報告、失敗的系統呼叫,在極端情況下,如果損壞的元資料強制取消髒事務,則會導致檔案系統關閉。

預計每個線上 fsck 清理函式都會在檢查結構的過程中讀取結構的每個磁碟元資料塊。在檢查期間觀察到的損壞問題會立即作為損壞報告給使用者空間;在交叉引用期間,一旦完成全面檢查,它們將作為交叉引用失敗報告。

4.5.3.2. 內部一致性檢查

在緩衝區快取之後,下一級別的元資料保護是內置於檔案系統中的內部記錄驗證程式碼。這些檢查在緩衝區驗證器、緩衝區快取的內建檔案系統使用者和 scrub 程式碼本身之間進行拆分,具體取決於所需的高階上下文的數量。檢查的範圍仍然在塊內部。這些更高級別的檢查函式回答以下問題:

  • 塊中儲存的資料型別是否與 scrub 期望的匹配?

  • 該塊是否屬於請求讀取的所屬結構?

  • 如果塊包含記錄,記錄是否適合塊?

  • 如果塊跟蹤內部可用空間資訊,它是否與記錄區域一致?

  • 塊內包含的記錄是否沒有明顯的損壞?

此類別的記錄檢查更加嚴格且耗時更多。例如,檢查塊指標和 inumber,以確保它們指向分配組的動態分配部分和檔案系統內。檢查名稱是否存在無效字元,並檢查標誌是否存在無效組合。還會檢查其他記錄屬性是否存在合理的值。檢查跨越 btree 鍵空間間隔的 Btree 記錄是否具有正確的順序和缺少可合併性(檔案 fork 對映除外)。出於效能原因,除非啟用除錯或即將發生寫入,否則常規程式碼可能會跳過其中一些檢查。當然,Scrub 函式必須檢查所有可能的問題。

4.5.3.3. 驗證使用者空間控制的記錄屬性

各種檔案系統元資料由使用者空間直接控制。由於這種性質,驗證工作不能比檢查值是否在可能的範圍內更精確。這些欄位包括:

  • 由掛載選項控制的超級塊欄位

  • 檔案系統標籤

  • 檔案時間戳

  • 檔案許可權

  • 檔案大小

  • 檔案標誌

  • 目錄條目、擴充套件屬性鍵和檔案系統標籤中存在的名稱

  • 擴充套件屬性鍵名稱空間

  • 擴充套件屬性值

  • 檔案資料塊內容

  • 配額限制

  • 配額計時器到期(如果資源使用量超過軟限制)

4.5.3.4. 交叉引用空間元資料

在內部塊檢查之後,下一個更高級別的檢查是交叉引用元資料結構之間的記錄。對於常規執行時程式碼,這些檢查的成本被認為是高得令人望而卻步,但是由於 scrub 致力於消除不一致,因此它必須尋求所有查詢途徑。交叉引用的確切集合高度依賴於正在檢查的資料結構的上下文。

XFS btree 程式碼具有線上 fsck 用於將一個結構與另一個結構進行交叉引用的鍵空間掃描函式。具體而言,scrub 可以掃描索引的鍵空間,以確定該鍵空間是否完全、稀疏或根本沒有對映到記錄。對於反向對映 btree,可以遮蔽部分鍵以進行鍵空間掃描,以便 scrub 可以確定 rmap btree 是否包含對映某個物理空間範圍的記錄,而不會受到其餘 rmap 鍵空間稀疏性的影響。

Btree 塊在交叉引用之前會進行以下檢查:

  • 塊中儲存的資料型別是否與 scrub 期望的匹配?

  • 該塊是否屬於請求讀取的所屬結構?

  • 記錄是否適合該塊?

  • 塊內包含的記錄是否沒有明顯的損壞?

  • 名稱雜湊是否按正確的順序排列?

  • btree 中的節點指標是否指向 btree 型別的有效塊地址?

  • 子指標是否指向葉子?

  • 兄弟指標是否在同一級別上指向?

  • 對於每個節點塊記錄,記錄鍵是否準確地反映了子塊的內容?

空間分配記錄按如下方式交叉引用:

  1. 任何元資料結構提及的任何空間都按如下方式交叉引用:

    • 反向對映索引是否僅將適當的所有者列為每個塊的所有者?

    • 是否沒有塊被宣告為空閒空間?

    • 如果這些不是檔案資料塊,是否沒有塊被宣告為由不同所有者共享的空間?

  2. Btree 塊按如下方式交叉引用:

    • 上面第 1 類中的所有內容。

    • 如果存在父節點塊,則為此塊列出的鍵是否與此塊的鍵空間匹配?

    • 兄弟指標是否指向有效塊?是否處於同一級別?

    • 子指標是否指向有效塊?是否處於下一級別?

  3. 空閒空間 btree 記錄按如下方式交叉引用:

    • 上面第 1 類和第 2 類中的所有內容。

    • 反向對映索引是否列出此空間的所有者?

    • inode 索引是否未宣告此空間用於 inode?

    • 引用計數索引中是否未提及它?

    • 在另一個空閒空間 btree 中是否存在匹配的記錄?

  4. Inode btree 記錄按如下方式交叉引用:

    • 上面第 1 類和第 2 類中的所有內容。

    • 空閒 inode btree 中是否存在匹配的記錄?

    • holemask 中清除的位是否與 inode 叢集對應?

    • freemask 中的設定位是否與連結計數為零的 inode 記錄對應?

  5. Inode 記錄按如下方式交叉引用:

    • 第 1 類中的所有內容。

    • 總結有關檔案 fork 的資訊的欄位是否實際上與這些 fork 匹配?

    • 每個連結計數為零的 inode 是否對應於空閒 inode btree 中的記錄?

  6. 檔案 fork 空間對映記錄按如下方式交叉引用:

    • 上面第 1 類和第 2 類中的所有內容。

    • inode btree 是否未提及此空間?

    • 如果這是一個 CoW fork 對映,它是否對應於引用計數 btree 中的 CoW 條目?

  7. 引用計數記錄按如下方式交叉引用:

    • 上面第 1 類和第 2 類中的所有內容。

    • 在 rmap btree 的空間子鍵空間內(也就是說,對映到特定空間範圍並忽略所有者資訊的所有記錄),每個塊的反向對映記錄的數量是否與引用計數記錄宣告的數量相同?

擬議的補丁集是為了找到 refcount btreeinode btreermap btree 記錄中的缺口;查詢 可合併記錄;並在開始修復之前改進與 rmap 的交叉引用

4.5.3.5. 檢查擴充套件屬性

擴充套件屬性實現了一個鍵值儲存,使資料片段可以附加到任何檔案。核心和使用者空間都可以訪問鍵和值,但受到名稱空間和許可權限制。最典型的是,這些片段是有關檔案的元資料——來源、安全上下文、使用者提供的標籤、索引資訊等。

名稱最長可達 255 位元組,並且可以存在於多個不同的名稱空間中。值的大小可達 64KB。檔案的擴充套件屬性儲存在 attr fork 對映的塊中。對映指向葉子塊、遠端值塊或 dabtree 塊。屬性 fork 中的塊 0 始終是結構的頂部,但否則可以在 attr fork 中的任何偏移處找到這三種類型的塊中的每一種。葉子塊包含指向名稱和值的屬性鍵記錄。名稱始終儲存在同一葉子塊中的其他位置。小於檔案系統塊大小的 3/4 的值也儲存在同一葉子塊中的其他位置。遠端值塊包含太大而無法放入葉子中的值。如果葉子資訊超過單個檔案系統塊,則會建立一個 dabtree(也以塊 0 為根)以將屬性名稱的雜湊值對映到 attr fork 中的葉子塊。

由於 attr 塊和索引塊之間缺乏分離,因此檢查擴充套件屬性結構並不那麼簡單。Scrub 必須讀取 attr fork 對映的每個塊並忽略非葉子塊。

  1. 遍歷 attr fork 中的 dabtree(如果存在),以確保塊或不指向 attr 葉子塊的 dabtree 對映中沒有不規則之處。

  2. 遍歷 attr fork 的塊以查詢葉子塊。對於葉子中的每個條目:

    1. 驗證名稱是否不包含無效字元。

    2. 讀取 attr 值。這將執行 attr 名稱的命名查詢,以確保 dabtree 的正確性。如果該值儲存在遠端塊中,這也會驗證遠端值塊的完整性。

4.5.3.6. 檢查和交叉引用目錄

檔案系統目錄樹是一個有向無環圖結構,其中檔案構成節點,目錄條目 (dirent) 構成邊。目錄是一種特殊型別的檔案,包含從 255 位元組序列(名稱)到 inumber 的一組對映。這些稱為目錄條目,或簡稱為 dirent。每個目錄檔案必須只有一個指向該檔案的目錄。根目錄指向自身。目錄條目指向任何型別的檔案。每個非目錄檔案可能具有指向它的多個目錄。

在 XFS 中,目錄實現為一個檔案,包含多達三個 32GB 分割槽。第一個分割槽包含目錄條目資料塊。每個資料塊都包含可變大小的記錄,這些記錄將使用者提供的名稱與 inumber 和(可選)檔案型別相關聯。如果目錄條目資料增長超過一個塊,則第二個分割槽(以 EOF 後範圍的形式存在)將填充一個包含空閒空間資訊和一個索引的塊,該索引將 dirent 名稱的雜湊對映到第一個分割槽中的目錄資料塊。這使得目錄名稱查詢非常快。如果第二個分割槽增長超過一個塊,則第三個分割槽將填充一個空閒空間資訊的線性陣列,以便更快地擴充套件。如果空閒空間已分離並且第二個分割槽再次增長超過一個塊,則使用 dabtree 將 dirent 名稱的雜湊對映到目錄資料塊。

檢查目錄非常簡單:

  1. 遍歷第二個分割槽中的 dabtree(如果存在),以確保塊或不指向 dirent 塊的 dabtree 對映中沒有不規則之處。

  2. 遍歷第一個分割槽中的塊以查詢目錄條目。按如下方式檢查每個 dirent:

    1. 名稱是否不包含無效字元?

    2. inumber 是否對應於實際分配的 inode?

    3. 子 inode 是否具有非零連結計數?

    4. 如果 dirent 中包含檔案型別,它是否與 inode 的型別匹配?

    5. 如果子項是一個子目錄,子項的 dotdot 指標是否指向父項?

    6. 如果目錄具有第二個分割槽,請執行 dirent 名稱的命名查詢以確保 dabtree 的正確性。

  3. 遍歷第三個分割槽中的空閒空間列表(如果存在),以確保它描述的空閒空間確實未使用。

涉及 父級檔案連結計數 的檢查操作將在後面的章節中進行更詳細的討論。

4.5.3.6.1. 檢查目錄/屬性 Btree

如前幾節所述,目錄/屬性 btree (dabtree) 索引對映使用者提供的名稱,以透過避免線性掃描來提高查詢時間。在內部,它將名稱的 32 位雜湊值對映到相應檔案 fork 中的塊偏移量。

dabtree 的內部結構與記錄固定大小元資料記錄的 btree 非常相似——每個 dabtree 塊都包含一個幻數、一個校驗和、兄弟指標、一個 UUID、一個樹級別和一個日誌序列號。葉子和節點記錄的格式相同——每個條目都指向層次結構中的下一級別,其中 dabtree 節點記錄指向 dabtree 葉子塊,而 dabtree 葉子記錄指向 fork 中其他位置的非 dabtree 塊。

檢查和交叉引用 dabtree 與對空間 btree 執行的操作非常相似:

  • 塊中儲存的資料型別是否與 scrub 期望的匹配?

  • 該塊是否屬於請求讀取的所屬結構?

  • 記錄是否適合該塊?

  • 塊內包含的記錄是否沒有明顯的損壞?

  • 名稱雜湊是否按正確的順序排列?

  • dabtree 中的節點指標是否指向 dabtree 塊的有效 fork 偏移量?

  • dabtree 中的葉子指標是否指向目錄或 attr 葉子塊的有效 fork 偏移量?

  • 子指標是否指向葉子?

  • 兄弟指標是否在同一級別上指向?

  • 對於每個 dabtree 節點記錄,記錄鍵是否準確地反映了子 dabtree 塊的內容?

  • 對於每個 dabtree 葉子記錄,記錄鍵是否準確地反映了目錄或 attr 塊的內容?

4.5.3.7. 交叉引用匯總計數器

XFS 維護三類彙總計數器:可用資源、配額資源使用情況和檔案連結計數。

從理論上講,可以透過遍歷整個檔案系統來找到可用資源量(資料塊、inode、即時範圍)。這將使報告速度非常慢,因此事務性檔案系統可以在超級塊中維護此資訊的摘要。將這些值與檔案系統元資料交叉引用應該是遍歷每個 AG 中的空閒空間和 inode 元資料以及即時點陣圖的簡單問題,但存在複雜性,將在 稍後更詳細地討論

配額使用情況檔案連結計數 檢查非常複雜,值得單獨章節介紹。

4.5.3.8. 修復後重新驗證

執行修復後,檢查程式碼將再次執行以驗證新結構,並且健康評估的結果將在內部記錄並返回到呼叫程序。此步驟對於使系統管理員能夠監視檔案系統的狀態和任何修復的進度至關重要。對於開發人員,這是一種有用的手段,可以判斷線上和離線檢查工具中的錯誤檢測和糾正的有效性。

4.5.4. 最終一致性與線上 Fsck

複雜操作可以使用事務鏈對多個按 AG 資料結構進行修改。一旦這些鏈提交到日誌,如果在處理鏈時系統崩潰,則會在日誌恢復期間重新啟動這些鏈。由於 AG 標頭緩衝區在鏈中的事務之間未鎖定,因此線上檢查必須與正在進行的鏈式操作協調,以避免由於掛起的鏈而錯誤地檢測到不一致。此外,線上修復不得在操作掛起時執行,因為元資料彼此暫時不一致,並且無法重建。

只有在線 fsck 具有對 AG 元資料的完全一致性的要求,與檔案系統更改操作相比,這應該相對罕見。線上 fsck 與事務鏈的協調方式如下:

  • 對於每個 AG,維護針對該 AG 的 intent 項的計數。每當向鏈中新增新項時,都應增加該計數。當檔案系統鎖定 AG 標頭緩衝區並完成工作時,應刪除該計數。

  • 當線上 fsck 想要檢查 AG 時,它應該鎖定 AG 標頭緩衝區以使所有想要修改該 AG 的事務鏈靜止。如果計數為零,則繼續進行檢查操作。如果計數為非零,則迴圈緩衝區鎖以允許鏈向前推進。

這可能會導致線上 fsck 花費很長時間才能完成,但常規檔案系統更新優先於後臺檢查活動。有關發現這種情況的詳細資訊將在下一節中介紹,有關解決方案的詳細資訊將在之後介紹。

4.5.4.1. 問題的發現

在線上清理開發的中途,fsstress 測試發現線上 fsck 和其他編寫器執行緒建立的複合事務鏈之間存在錯誤的互動,從而導致了對元資料不一致性的錯誤報告。這些報告的根本原因是引入反向對映和 reflink 時,延遲工作項和複合事務鏈的擴充套件所引入的最終一致性模型。

最初,事務鏈被新增到 XFS 中,以避免從檔案中取消對映空間時出現死鎖。避免死鎖的規則要求僅以遞增順序鎖定 AG,這使得(例如)不可能使用單個事務釋放 AG 7 中的空間範圍,然後嘗試釋放 AG 3 中現在多餘的塊對映 btree 塊。為了避免這些型別的死鎖,XFS 建立範圍釋放意圖 (EFI) 日誌項以提交以在一個事務中釋放一些空間,同時將實際元資料更新推遲到新事務。事務序列如下所示:

  1. 第一個事務包含對檔案塊對映結構的物理更新,以從 btree 塊中刪除對映。然後,它將一個操作項附加到記憶體中事務,以安排延遲釋放空間。具體來說,每個事務都維護一個 struct xfs_defer_pending 物件的列表,每個物件都維護一個 struct xfs_extent_free_item 物件的列表。回到上面的示例,操作項跟蹤 AG 7 中未對映空間的釋放和 AG 3 中塊對映 btree (BMBT) 塊的釋放。以這種方式記錄的延遲釋放透過從 struct xfs_extent_free_item 物件建立 EFI 日誌項並將日誌項附加到事務來提交到日誌中。當日志持久儲存到磁碟時,EFI 項將寫入磁碟事務記錄中。EFI 最多可以列出 16 個要釋放的範圍,所有範圍都按 AG 順序排序。

  2. 第二個事務包含對 AG 3 的空閒空間 btree 的物理更新,以釋放之前的 BMBT 塊,並對 AG 7 的空閒空間 btree 進行第二次物理更新,以釋放未對映的檔案空間。請注意,物理更新在可能的情況下會以正確的順序重新排序。附加到事務的是一個範圍釋放完成 (EFD) 日誌項。EFD 包含一個指向在事務 #1 中記錄的 EFI 的指標,以便日誌恢復可以判斷是否需要重播 EFI。

如果系統在事務 #1 寫回檔案系統之後但在提交 #2 之前出現故障,則掃描檔案系統元資料將顯示不一致的檔案系統元資料,因為未對映空間似乎沒有任何所有者。幸運的是,日誌恢復為我們糾正了這種不一致——當恢復找到 intent 日誌項但未找到相應的 intent 完成項時,它將重建 intent 項的核心狀態並完成它。在上面的示例中,日誌必須重播在恢復的 EFI 中描述的所有釋放才能完成恢復階段。

XFS 的事務連結策略有一些微妙之處需要考慮:

  • 必須以正確的順序將日誌項新增到事務中,以防止與事務未持有的主要物件發生衝突。換句話說,必須在最後一次更新以釋放範圍之前完成對未對映塊的所有按 AG 元資料更新,並且在最後一次更新提交到日誌之前不應重新分配範圍。

  • AG 標頭緩衝區在鏈中的每個事務之間釋放。這意味著其他執行緒可以在中間狀態觀察 AG,但只要處理了第一個微妙之處,這就不應影響檔案系統操作的正確性。

  • 解除安裝檔案系統會將所有掛起的工作重新整理到磁碟,這意味著離線 fsck 永遠不會看到由延遲工作項處理引起的臨時不一致。

以這種方式,XFS 採用一種形式的最終一致性來避免死鎖並提高並行性。

在反向對映和 reflink 功能的設計階段,決定將單個檔案系統更改的所有反向對映更新塞入單個事務是不切實際的,因為單個檔案對映操作可能會爆炸成許多小更新:

  • 塊對映更新本身

  • 塊對映更新的反向對映更新

  • 修復空閒列表

  • 空閒列表修復的反向對映更新

  • 塊對映 btree 的形狀更改

  • btree 更新的反向對映更新

  • 修復空閒列表(再次)

  • 空閒列表修復的反向對映更新

  • 引用計數資訊的更新

  • refcount 更新的反向對映更新

  • 修復空閒列表(第三次)

  • 空閒列表修復的反向對映更新

  • 釋放未對映且不屬於任何其他檔案的任何空間

  • 修復空閒列表(第四次)

  • 空閒列表修復的反向對映更新

  • 釋放塊對映 btree 使用的空間

  • 修復空閒列表(第五次)

  • 空閒列表修復的反向對映更新

對於每個事務鏈,每個 AG 通常不需要修復空閒列表超過一次,但如果空間非常緊張,理論上是可能的。對於寫時複製更新,這甚至更糟,因為必須執行一次才能從暫存區域中刪除空間,然後再次將其對映到檔案中!

為了以冷靜的方式處理這種爆炸式增長,XFS 擴充套件了其延遲工作項的使用範圍,以覆蓋大多數反向對映更新和所有 refcount 更新。這透過將工作分解為一長串小更新來減少事務預留的最壞情況大小,從而增加了系統中最終一致性的程度。同樣,這通常不是問題,因為 XFS 會仔細排序其延遲工作項,以避免不知情的執行緒之間的資源重用衝突。

但是,線上 fsck 更改了規則——請記住,雖然對按 AG 結構的物理更新透過鎖定 AG 標頭的緩衝區來協調,但緩衝區鎖會在事務之間刪除。一旦 scrub 獲取資源並獲取資料結構的鎖,它必須在不釋放鎖的情況下完成所有驗證工作。如果空間 btree 的主鎖是 AG 標頭緩衝區鎖,則 scrub 可能中斷了另一個執行緒,該執行緒正處於完成鏈的中途。例如,如果執行寫時複製的執行緒已完成反向對映更新但未完成相應的 refcount 更新,則兩個 AG btree 對於 scrub 而言將顯得不一致,並且將記錄對損壞的觀察。此觀察結果將不正確。如果在這種狀態下嘗試修復,結果將是災難性的!

在發現此缺陷後,評估了其他幾種解決方案並被拒絕:

  1. 向分配組新增更高級別的鎖,並要求編寫器執行緒在進行任何更改之前以 AG 順序獲取更高級別的鎖。這在實踐中很難實現,因為很難確定需要獲取哪些鎖以及以什麼順序獲取,而無需模擬整個操作。執行檔案操作的試執行以發現必要的鎖將使檔案系統變得非常慢。

  2. 使延遲工作協調器程式碼知道針對同一 AG 的連續 intent 項,並使其在更新之間的事務滾動中保持 AG 標頭緩衝區的鎖定狀態。這將給協調器帶來很多複雜性,因為它僅與實際的延遲工作項鬆散耦合。它也無法解決該問題,因為延遲工作項可以生成新的延遲子任務,但是必須在新的同級任務開始工作之前完成所有子任務。

  3. 指導線上 fsck 遍歷所有等待保護正在清理的資料結構的鎖的事務,以查詢掛起的操作。檢查和修復操作必須將這些掛起的操作考慮在執行的評估中。此解決方案不可行,因為它對主檔案系統的侵入性非常強。

4.5.4.2. 意圖耗盡

線上 fsck 使用原子意圖項計數器和鎖迴圈來與事務鏈進行協調。drain 機制有兩個關鍵屬性。首先,當一個延遲工作項排隊到一個事務時,計數器會遞增,而在相關的意圖完成日誌項提交到另一個事務後,計數器會遞減。第二個屬性是,可以將延遲工作新增到事務中,而無需持有 AG 頭部鎖,但是,如果沒有鎖定該 AG 頭部緩衝區以記錄物理更新和意圖完成日誌項,則無法將每個 AG 的工作項標記為完成。第一個屬性使 scrub 能夠讓位於正在執行的事務鏈,這是線上 fsck 的顯式降級,以利於檔案操作。drain 的第二個屬性是正確協調 scrub 的關鍵,因為 scrub 將始終能夠確定是否可能發生衝突。

對於常規檔案系統程式碼,drain 的工作方式如下

  1. 呼叫適當的子系統函式以將延遲工作項新增到事務中。

  2. 該函式呼叫 xfs_defer_drain_bump 以增加計數器。

  3. 當延遲項管理器想要完成延遲工作項時,它會呼叫 ->finish_item 以完成它。

  4. ->finish_item 實現記錄一些更改,並呼叫 xfs_defer_drain_drop 以減少鬆散計數器,並喚醒任何等待 drain 的執行緒。

  5. 子事務提交,這將解鎖與意圖項關聯的資源。

對於 scrub,drain 的工作方式如下

  1. 鎖定與要 scrub 的元資料關聯的資源。例如,掃描 refcount btree 將鎖定 AGI 和 AGF 頭部緩衝區。

  2. 如果計數器為零(xfs_defer_drain_busy 返回 false),則沒有正在進行的鏈,並且該操作可以繼續。

  3. 否則,釋放在步驟 1 中獲取的資源。

  4. 等待意圖計數器達到零 (xfs_defer_drain_intents),然後返回到步驟 1,除非已捕獲訊號。

為避免在步驟 4 中進行輪詢,drain 提供了一個等待佇列,以便在意圖計數降至零時喚醒 scrub 執行緒。

提出的補丁集是 scrub intent drain 系列

4.5.4.3. 靜態鍵(又名跳轉標籤修補)

XFS 的線上 fsck 儘可能地將常規檔案系統與檢查和修復程式碼分開。但是,線上 fsck 的一些部分(例如意圖 drain,以及稍後的即時更新鉤子)中,線上 fsck 程式碼瞭解檔案系統其餘部分中發生的事情很有用。由於預計線上 fsck 不會一直在後臺執行,因此在將線上 fsck 編譯到核心中但沒有代表使用者空間主動執行時,最大限度地減少這些鉤子帶來的執行時開銷非常重要。在寫入器執行緒的熱路徑中獲取鎖以訪問資料結構,結果卻發現不需要採取進一步的措施是很昂貴的——在作者的計算機上,這會產生每次訪問 40-50 納秒的開銷。幸運的是,核心支援動態程式碼修補,這使 XFS 能夠在線上 fsck 未執行時,用 nop sleds 替換靜態分支到鉤子程式碼。此 sled 的開銷取決於指令解碼器跳過該 sled 所需的時間,這似乎在小於 1 納秒的量級上,並且不訪問指令獲取之外的記憶體。

當線上 fsck 啟用靜態鍵時,sled 將被替換為對鉤子程式碼的無條件分支。切換非常昂貴(約 22000 納秒),但完全由呼叫線上 fsck 的程式支付,並且如果多個執行緒同時進入線上 fsck,或者如果同時檢查多個檔案系統,則可以進行攤銷。更改分支方向需要獲取 CPU 熱插拔鎖,並且由於 CPU 初始化需要記憶體分配,因此線上 fsck 必須小心,不要在持有任何可以在記憶體回收路徑中訪問的鎖或資源時更改靜態鍵。為了最大限度地減少 CPU 熱插拔鎖的爭用,應注意不要不必要地啟用或停用靜態鍵。

由於靜態鍵旨在最大限度地減少 xfs_scrub 未執行時常規檔案系統操作的鉤子開銷,因此預期的使用模式如下

  • XFS 的鉤子部分應宣告一個靜態範圍的靜態鍵,預設為 false。DEFINE_STATIC_KEY_FALSE 宏處理此問題。靜態鍵本身應宣告為 static 變數。

  • 在決定呼叫僅由 scrub 使用的程式碼時,如果未啟用靜態鍵,則常規檔案系統應呼叫 static_branch_unlikely 謂詞以避免僅限 scrub 的鉤子程式碼。

  • 常規檔案系統應匯出呼叫 static_branch_inc 以啟用和 static_branch_dec 以停用靜態鍵的輔助函式。如果核心分發者在構建時關閉線上 fsck,則包裝函式可以輕鬆地編譯出相關程式碼。

  • 想要啟用僅限 scrub 的 XFS 功能的 Scrub 函式應從設定函式呼叫 xchk_fsgates_enable 以啟用特定鉤子。必須在獲取記憶體回收使用的任何資源之前完成此操作。呼叫者最好確定他們是否真的需要靜態鍵限制的功能;TRY_HARDER 標誌在這裡很有用。

線上 scrub 具有資源獲取輔助函式(例如 xchk_perag_lock)來處理所有 scrubber 函式的鎖定 AGI 和 AGF 緩衝區。如果它檢測到 scrub 和正在執行的事務之間存在衝突,它將嘗試等待意圖完成。如果輔助函式的呼叫者未啟用靜態鍵,則輔助函式將返回 -EDEADLOCK,這應導致 scrub 使用 TRY_HARDER 標誌重新啟動。scrub 設定函式應檢測到該標誌,啟用靜態鍵,然後再次嘗試 scrub。Scrub 拆卸會停用 xchk_fsgates_enable 獲取的所有靜態鍵。

有關更多資訊,請參見 靜態鍵 的核心文件。

4.5.5. 可分頁核心記憶體

一些線上檢查函式透過掃描檔案系統以在記憶體中構建磁碟元資料結構的影子副本並比較兩個副本來進行工作。對於線上修復以重建元資料結構,它必須在將新結構持久儲存到磁碟之前,計算將儲存在新結構中的記錄集。理想情況下,修復應以單個原子提交完成,該提交會引入新的資料結構。為實現這些目標,核心需要在不需要檔案系統正確執行的地方收集大量資訊。

核心記憶體不適合,因為

  • 分配連續的記憶體區域以建立 C 陣列非常困難,尤其是在 32 位系統上。

  • 記錄的連結串列引入了雙指標開銷,這非常高,並且消除了索引查詢的可能性。

  • 核心記憶體被固定,這可能會將系統驅動到 OOM 條件中。

  • 系統可能沒有足夠的記憶體來暫存所有資訊。

在任何給定時間,線上 fsck 都不需要將整個記錄集保留在記憶體中,這意味著必要時可以分頁輸出各個記錄。線上 fsck 的持續開發表明,執行索引資料儲存的能力也將非常有用。幸運的是,Linux 核心已經具有用於位元組可定址和可分頁儲存的工具:tmpfs。核心圖形驅動程式(最著名的是 i915)利用 tmpfs 檔案來儲存不需要始終位於記憶體中的中間資料,因此已經建立了使用先例。因此,xfile 誕生了!

歷史側邊欄:

線上修復的第一版在找到記錄時將其插入到新的 btree 中,這失敗了,因為檔案系統可能會關閉並構建資料結構,這將會在恢復完成後生效。

第二版透過將所有內容儲存在記憶體中來解決了一半重建的結構問題,但經常導致系統記憶體不足。

第三版透過使用連結串列解決了 OOM 問題,但是列表指標的記憶體開銷非常大。

4.5.5.1. xfile 訪問模型

對 xfile 預期用途的調查表明瞭以下用例

  1. 固定大小記錄的陣列(空間管理 btree、目錄和擴充套件屬性條目)

  2. 固定大小記錄的稀疏陣列(配額和連結計數)

  3. 可變大小的大型二進位制物件 (BLOB)(目錄和擴充套件屬性名稱和值)

  4. 在記憶體中暫存 btree(反向對映 btree)

  5. 任意內容(即時空間管理)

為了支援前四個用例,高階資料結構包裝 xfile 以共享線上 fsck 函式之間的功能。本節的其餘部分討論 xfile 向這五個更高級別的資料結構中的四個呈現的介面。第五個用例將在 即時摘要 案例研究中討論。

XFS 是基於記錄的,這表明載入和儲存完整記錄的能力很重要。為了支援這些情況,提供了一對 xfile_loadxfile_store 函式,用於將物件讀取和持久儲存到將任何錯誤視為記憶體不足錯誤的 xfile 中。對於線上修復,以這種方式壓縮錯誤情況是可以接受的行為,因為唯一的反應是中止操作返回到使用者空間。

但是,如果沒有回答“但是 mmap 呢?”這個問題,那麼對檔案訪問習慣用法的討論就不完整了。就像使用者空間程式碼使用常規記憶體一樣,使用指標直接訪問儲存是很方便的。線上 fsck 絕不能將系統驅動到 OOM 條件中,這意味著 xfile 必須對記憶體回收做出響應。如果 folio 既未固定也未鎖定,則 tmpfs 只能將 pagecache folio 推送到交換快取,這意味著 xfile 絕不能固定過多的 folio。

對 xfile 內容的短期直接訪問是透過鎖定 pagecache folio 並將其對映到核心地址空間來完成的。物件載入和儲存使用此機制。Folio 鎖不應持有很長時間,因此對 xfile 內容的長期直接訪問是透過提升 folio 引用計數,將其對映到核心地址空間,然後刪除 folio 鎖來完成的。這些長期使用者必須透過掛接到收縮器基礎結構來了解何時釋放 folio,從而對記憶體回收做出響應。

提供了 xfile_get_folioxfile_put_folio 函式來檢索支援 xfile 部分的(已鎖定)folio 並釋放它。唯一使用這些 folio 租用函式的程式碼是 xfarray 排序 演算法和 記憶體 btree

4.5.5.2. xfile 訪問協調

出於安全原因,xfile 必須由核心私有擁有。它們被標記為 S_PRIVATE 以防止安全系統的干擾,絕不能對映到程序檔案描述符表中,並且它們的頁面絕不能對映到使用者空間程序中。

為避免 VFS 出現鎖定遞迴問題,對 shmfs 檔案的所有訪問都是透過直接操作頁面快取來執行的。xfile 寫入器呼叫 xfile 地址空間的 ->write_begin->write_end 函式來獲取可寫頁面,將呼叫者的緩衝區複製到頁面中,然後釋放頁面。xfile 讀取器呼叫 shmem_read_mapping_page_gfp 以在將內容複製到呼叫者的緩衝區之前直接獲取頁面。換句話說,xfile 忽略 VFS 讀取和寫入程式碼路徑,以避免建立虛擬 struct kiocb 以及避免獲取 inode 和凍結鎖。tmpfs 無法凍結,並且 xfile 不得暴露給使用者空間。

如果 xfile 線上程之間共享以暫存修復,則呼叫者必須提供自己的鎖來協調訪問。例如,如果 scrub 函式將掃描結果儲存在 xfile 中,並且需要其他執行緒提供對掃描資料的更新,則 scrub 函式必須提供一個鎖供所有執行緒共享。

4.5.5.3. 固定大小記錄的陣列

在 XFS 中,每種型別的索引空間元資料(可用空間、inode、引用計數、檔案 fork 空間和反向對映)都由一組使用經典 B+ 樹索引的固定大小記錄組成。目錄有一組指向名稱的固定大小的目錄項記錄,擴充套件屬性有一組指向名稱和值的固定大小的屬性鍵。配額計數器和檔案連結計數器使用數字索引記錄。在修復期間,scrub 需要在收集步驟中暫存新記錄,並在 btree 構建步驟中檢索它們。

儘管可以透過直接呼叫 xfile 的讀取和寫入方法來滿足此要求,但是對於呼叫者來說,透過更高級別的抽象來處理計算陣列偏移量、提供迭代器函式以及處理稀疏記錄和排序會更簡單。xfarray 抽象在位元組可訪問的 xfile 之上呈現固定大小記錄的線性陣列。

4.5.5.3.1. 陣列訪問模式

線上 fsck 中的陣列訪問模式傾向於分為三類。假定所有情況下都需要記錄迭代,並且將在下一節中介紹。

第一種型別的呼叫者處理按位置索引的記錄。記錄之間可能存在間隙,並且在收集步驟中可能會多次更新記錄。換句話說,這些呼叫者需要稀疏的線性定址表文件。典型的用例是配額記錄或檔案連結計數記錄。對陣列元素的訪問是透過 xfarray_loadxfarray_store 函式以程式設計方式執行的,這些函式包裝了類似命名的 xfile 函式,以提供在任意陣列索引處載入和儲存陣列元素。間隙被定義為空記錄,空記錄被定義為全零位元組的序列。透過呼叫 xfarray_element_is_null 來檢測空記錄。可以透過呼叫 xfarray_unset 以使現有記錄為空,或者透過從不向陣列索引儲存任何內容來建立空記錄。

第二種型別的呼叫者處理不由位置索引的記錄,並且不需要對記錄進行多次更新。此處的典型用例是重建空間 btree 和鍵/值 btree。這些呼叫者可以透過 xfarray_append 函式將記錄新增到陣列中,而無需關心陣列索引,該函式將記錄儲存在陣列的末尾。對於需要以特定順序呈現記錄的呼叫者(例如,重建 btree 資料),xfarray_sort 函式可以排列排序的記錄;該函式將在稍後介紹。

第三種類型的呼叫者是 bag,這對於計算記錄很有用。此處的典型用例是根據反向對映資訊構造空間範圍引用計數。可以將記錄以任何順序放入 bag 中,可以隨時從 bag 中刪除它們,並且記錄的唯一性留給呼叫者處理。xfarray_store_anywhere 函式用於在 bag 中的任何空記錄槽中插入記錄;xfarray_unset 函式從 bag 中刪除記錄。

提出的補丁集是 大的記憶體陣列

4.5.5.3.2. 迭代陣列元素

xfarray 的大多數使用者都需要能夠迭代儲存在陣列中的記錄。呼叫者可以使用以下內容探測每個可能的陣列索引

xfarray_idx_t i;
foreach_xfarray_idx(array, i) {
    xfarray_load(array, i, &rec);

    /* do something with rec */
}

此習慣用法的所有使用者都必須準備好處理空記錄,或者必須已經知道沒有任何空記錄。

對於想要迭代稀疏陣列的 xfarray 使用者,xfarray_iter 函式會忽略 xfarray 中從未透過呼叫 xfile_seek_data(內部使用 SEEK_DATA)寫入過的索引,以跳過陣列中未填充記憶體頁面的區域。找到頁面後,它將跳過頁面的歸零區域。

xfarray_idx_t i = XFARRAY_CURSOR_INIT;
while ((ret = xfarray_iter(array, &i, &rec)) == 1) {
    /* do something with rec */
}
4.5.5.3.3. 排序陣列元素

在線上修復的第四次演示中,一位社群審查員評論說,出於效能原因,線上修復應該將批次的記錄載入到 btree 記錄塊中,而不是一次將記錄插入到新的 btree 中。XFS 中的 btree 插入程式碼負責維護記錄的正確排序,因此,xfarray 自然也必須支援在批次載入之前對記錄集進行排序。

4.5.5.3.3.1. 案例研究:排序 xfarray

xfarray 中使用的排序演算法實際上是自適應快速排序和堆排序子演算法的組合,其精神與 Sedgewickpdqsort 相同,並針對 Linux 核心進行了自定義。為了在合理的時間內對記錄進行排序,xfarray 利用了快速排序提供的二進位制子分割槽,但它也使用堆排序來對沖效能崩潰(如果選擇的快速排序樞軸較差)。這兩種演算法通常都是 O(n * lg(n)),但是兩種實現之間存在很大的效能差距。

Linux 核心已經包含堆排序的相當快的實現。它僅對常規 C 陣列進行操作,這限制了其有用範圍。xfarray 使用它的兩個關鍵位置是

  • 排序由單個 xfile 頁面支援的任何記錄子集。

  • 將少量的 xfarray 記錄從 xfarray 的可能不同的部分載入到記憶體緩衝區中,並對緩衝區進行排序。

換句話說,xfarray 使用堆排序來約束快速排序的巢狀遞迴,從而減輕快速排序的最壞執行時行為。

選擇快速排序樞軸是一項棘手的事情。好的樞軸將要排序的集合分成兩半,從而導致對於 O(n * lg(n)) 效能至關重要的分而治之行為。不良的樞軸幾乎不會拆分子集,從而導致 O(n2) 執行時。xfarray 排序例程嘗試透過將九個記錄取樣到記憶體緩衝區中,並使用核心堆排序來標識這九個記錄的中值,從而避免選擇不良的樞軸。

大多數現代快速排序實現都使用 Tukey 的“ninther”從經典 C 陣列中選擇樞軸。典型的 ninther 實現選擇三個唯一的記錄三元組,對每個三元組進行排序,然後對每個三元組的中間值進行排序以確定 ninther 值。但是,如前所述,xfile 訪問並非完全便宜。事實證明,將九個元素讀入記憶體緩衝區,在緩衝區上執行核心的記憶體中堆排序,然後選擇該緩衝區的第 4 個元素作為樞軸,效能更高。Tukey 的 ninthers 在 J. W. Tukey 的 The ninther, a technique for low-effort robust (resistant) location in large samples 中進行了描述,該文章位於 Contributions to Survey Sampling and Applied Statistics 中,由 H. David 編輯,(Academic Press, 1978),第 251–257 頁。

快速排序的分割槽非常教科書式——圍繞樞軸重新排列記錄子集,然後設定當前堆疊幀和下一個堆疊幀以分別與樞軸的較大一半和較小一半進行排序。這使堆疊空間要求保持在 log2(記錄計數)。

作為最後的效能最佳化,快速排序的 hi 和 lo 掃描階段使檢查的 xfile 頁面儘可能長時間地對映在核心中,以減少對映/取消對映週期。令人驚訝的是,在考慮將堆排序直接應用於 xfile 頁面之後,這再次使整體排序執行時減少了近一半。

4.5.5.4. Blob 儲存

擴充套件屬性和目錄為暫存記錄添加了額外的要求:有限長度的任意位元組序列。每個目錄條目記錄都需要儲存條目名稱,並且每個擴充套件屬性都需要儲存屬性名稱和值。名稱、鍵和值可能會佔用大量記憶體,因此建立了 xfblob 抽象以簡化 xfile 之上這些 blob 的管理。

Blob 陣列提供 xfblob_loadxfblob_store 函式來檢索和持久儲存物件。store 函式為它持久儲存的每個物件返回一個魔術 cookie。稍後,呼叫者將提供此 cookie 給 xblob_load 以呼叫該物件。xfblob_free 函式釋放特定的 blob,並且 xfblob_truncate 函式釋放所有 blob,因為不需要壓縮。

修復目錄和擴充套件屬性的詳細資訊將在有關原子檔案內容交換的後續章節中討論。但是,應注意的是,這些修復函式僅使用 blob 儲存來快取少量條目,然後將其新增到臨時磁碟檔案中,這就是為什麼不需要壓縮的原因。

提出的補丁集位於 擴充套件屬性修復 系列的開頭。

4.5.5.5. 記憶體 B+ 樹

關於 輔助元資料 的章節提到,輔助元資料的檢查和修復通常需要在檔案系統的即時元資料掃描與更新該元資料的寫入器執行緒之間進行協調。保持掃描資料最新需要能夠將元資料更新從檔案系統傳播到掃描收集的資料中。這可以透過將併發更新附加到單獨的日誌檔案中,並在將新元資料寫入磁碟之前應用它們來完成,但是如果系統的其餘部分非常繁忙,這會導致無限的記憶體消耗。另一個選擇是跳過 side-log 並將來自檔案系統的即時更新直接提交到掃描資料中,這以更多的開銷換取更低的最大記憶體需求。在這兩種情況下,儲存掃描結果的資料結構都必須支援索引訪問才能表現良好。

鑑於這兩種策略都需要掃描資料的索引查詢,因此線上 fsck 採用第二種策略,即將即時更新直接提交到掃描資料中。但是,由於 xfarray 未索引且不強制執行記錄排序,因此它們不適合此任務。但是,方便的是,XFS 有一個庫可以建立和維護排序的反向對映記錄:現有的 rmap btree 程式碼!如果只有一種在記憶體中建立它的方法。

回想一下,xfile 抽象將記憶體頁面表示為常規檔案,這意味著核心可以隨意建立位元組或塊可定址的虛擬地址空間。XFS 緩衝區快取專門用於將 IO 抽象為面向塊的地址空間,這意味著將緩衝區快取適應為與 xfile 介面可以重用整個 btree 庫。構建在 xfile 之上的 btree 統稱為 xfbtree。接下來的幾節描述了它們是如何實際工作的。

提出的補丁集是 記憶體 btree 系列。

4.5.5.5.1. 使用 xfile 作為緩衝區快取目標

需要進行兩項修改才能支援 xfile 作為緩衝區快取目標。首先,要使 struct xfs_buftarg 結構能夠託管 struct xfs_buf rhashtable,因為通常這些結構由每個 AG 結構持有。第二個更改是修改緩衝區 ioapply 函式以從 xfile“讀取”快取的頁面,並將快取的頁面“寫入”回 xfile。對單個緩衝區的多次訪問由 xfs_buf 鎖控制,因為 xfile 本身不提供任何鎖定。透過這種適應,xfile 支援的緩衝區快取的使用者使用的 API 與磁碟支援的緩衝區快取的使用者完全相同。xfile 和緩衝區快取之間的分離意味著更高的記憶體使用率,因為它們不共享頁面,但是此屬性有一天可能會使對記憶體中 btree 的事務性更新成為可能。但是,今天,它只是消除了對新程式碼的需求。

4.5.5.5.2. 使用 xfbtree 進行空間管理

xffile 的空間管理非常簡單——每個 btree 塊都是一個記憶體頁面大小。這些塊使用與磁碟 btree 相同的頭部格式,但是記憶體中的塊驗證器會忽略校驗和,假設 xfile 記憶體與常規 DRAM 一樣不易受到損壞。重用此處現有的程式碼比絕對記憶體效率更重要。

支援 xfbtree 的 xfile 的第一個塊包含一個頭部塊。頭部描述了所有者、高度和根 xfbtree 塊的塊號。

要分配 btree 塊,請使用 xfile_seek_data 在檔案中找到一個間隙。如果沒有間隙,請透過擴充套件 xfile 的長度來建立一個。使用 xfile_prealloc 預分配塊的空間,然後返回該位置。要釋放 xfbtree 塊,請使用 xfile_discard(內部使用 FALLOC_FL_PUNCH_HOLE)從 xfile 中刪除記憶體頁面。

4.5.5.5.3. 填充 xfbtree

想要建立 xfbtree 的線上 fsck 函式應按如下方式進行

  1. 呼叫 xfile_create 以建立一個 xfile。

  2. 呼叫 xfs_alloc_memory_buftarg 以建立一個指向 xfile 的緩衝區快取目標結構。

  3. 將緩衝區快取目標、緩衝區操作和其他資訊傳遞給 xfbtree_init 以初始化傳入的 struct xfbtree,並將初始根塊寫入 xfile。每種 btree 型別都應該定義一個包裝器,將必要的引數傳遞給建立函式。例如,rmap btree 定義了 xfs_rmapbt_mem_create,以處理呼叫者所需的所有細節。

  4. 將 xfbtree 物件傳遞給 btree 遊標建立函式,用於該 btree 型別。按照上面的例子,xfs_rmapbt_mem_cursor 負責為呼叫者處理此操作。

  5. 將 btree 遊標傳遞給常規 btree 函式,以便對記憶體中的 btree 進行查詢和更新。例如,rmap xfbtree 的 btree 遊標可以像任何其他 btree 遊標一樣傳遞給 xfs_rmap_* 函式。有關處理記錄到事務的 xfbtree 更新的資訊,請參見 下一節

  6. 完成後,刪除 btree 遊標,銷燬 xfbtree 物件,釋放緩衝區目標,並銷燬 xfile 以釋放所有資源。

4.5.5.5.4. 提交已記錄的 xfbtree 緩衝區

儘管重用 rmap btree 程式碼來處理暫存結構是一個巧妙的技巧,但記憶體中 btree 塊儲存的短暫性也帶來了一些挑戰。XFS 事務管理器不得為由 xfile 支援的緩衝區提交緩衝區日誌項,因為日誌格式不理解對資料裝置以外的裝置的更新。臨時 xfbtree 可能在 AIL 將日誌事務檢查點寫回檔案系統時不存在,並且肯定不會在日誌恢復期間存在。因此,任何在事務上下文中更新 xfbtree 的程式碼都必須在提交或取消事務之前,從事務中刪除緩衝區日誌項並將更新寫入支援 xfile。

xfbtree_trans_commitxfbtree_trans_cancel 函式按如下方式實現此功能

  1. 查詢每個緩衝區日誌項,其緩衝區以 xfile 為目標。

  2. 記錄日誌項的 dirty/ordered 狀態。

  3. 從緩衝區分離日誌項。

  4. 將緩衝區排隊到特殊的 delwri 列表。

  5. 如果唯一的 dirty 日誌項是在步驟 3 中分離的那些,則清除事務 dirty 標誌。

  6. 如果正在提交更新,則提交 delwri 列表以將更改提交到 xfile。

以這種方式從事務中刪除 xfile 已記錄緩衝區後,可以提交或取消事務。

4.5.6. 批次載入磁碟上的 B+樹

如前所述,線上修復的早期迭代透過建立一個新的 btree 並單獨新增觀察結果來構建新的 btree 結構。一次載入一個記錄的 btree 的一個優點是不需要在提交之前對 incore 記錄進行排序,但是速度非常慢,並且如果系統在修復過程中關閉,則會洩漏塊。一次載入一個記錄也意味著修復無法控制新 btree 中塊的載入因子。

幸運的是,歷史悠久的 xfs_repair 工具有一種更有效的方法可以從記錄集合中重建 btree 索引 -- 批次 btree 載入。由於 xfs_repair 對每種 btree 型別都有單獨的複製貼上實現,因此這在程式碼方面實現得相當低效。

為了準備線上 fsck,研究了每個批次載入器,記下了筆記,並將這四個載入器重構為單個通用 btree 批次載入機制。這些筆記也進行了更新,並在下面列出。

4.5.6.1. 幾何計算

批次載入的第零步是組裝將儲存在新 btree 中的整個記錄集,並對記錄進行排序。接下來,呼叫 xfs_btree_bload_compute_geometry 以從記錄集、btree 型別和任何載入因子偏好來計算 btree 的形狀。此資訊是資源預留所必需的。

首先,幾何計算從 btree 塊的大小和塊頭部的大小來計算適合葉子塊的最小和最大記錄。粗略地說,記錄的最大數量是

maxrecs = (block_size - header_size) / record_size

XFS 設計指定應該儘可能合併 btree 塊,這意味著記錄的最小數量是 maxrecs 的一半

minrecs = maxrecs / 2

要確定的下一個變數是所需的載入因子。它必須至少為 minrecs,並且不超過 maxrecs。選擇 minrecs 是不可取的,因為它浪費了塊的一半。選擇 maxrecs 也是不可取的,因為向每個新重建的葉子塊新增單個記錄將導致樹分裂,這會導致效能立即下降。預設載入因子選擇為 maxrecs 的 75%,這提供了一個合理緊湊的結構,沒有任何立即的分裂懲罰

default_load_factor = (maxrecs + minrecs) / 2

如果空間緊張,載入因子將設定為 maxrecs,以儘量避免耗盡空間

leaf_load_factor = enough space ? default_load_factor : maxrecs

使用 btree 鍵和指標的組合大小作為記錄大小來計算 btree 節點塊的載入因子

maxrecs = (block_size - header_size) / (key_size + ptr_size)
minrecs = maxrecs / 2
node_load_factor = enough space ? default_load_factor : maxrecs

完成後,可以將儲存記錄集所需的葉子塊數量計算為

leaf_blocks = ceil(record_count / leaf_load_factor)

指向樹中下一級的所需節點塊數量計算為

n_blocks = (n == 0 ? leaf_blocks : node_blocks[n])
node_blocks[n + 1] = ceil(n_blocks / node_load_factor)

整個計算遞迴執行,直到當前級別只需要一個塊。生成的幾何形狀如下

  • 對於以 AG 為根的 btree,此級別是根級別,因此新樹的高度為 level + 1,所需的空間是每個級別的塊數的總和。

  • 對於根在 inode 中的 btree,如果頂層的記錄不適合 inode 分叉區域,則高度為 level + 2,所需的空間是每個級別的塊數的總和,並且 inode 分叉指向根塊。

  • 對於根在 inode 中的 btree,如果頂層的記錄可以儲存在 inode 分叉區域中,則根塊可以儲存在 inode 中,高度為 level + 1,所需的空間比每個級別的塊數的總和少一個。只有當非 bmap btree 獲得在 inode 中紮根的能力時,這才會變得相關,這是一個未來的補丁集,僅在此處為了完整性而包含。

4.5.6.2. 預留新的 B+樹塊

一旦修復知道新 btree 所需的塊數,它將使用可用空間資訊來分配這些塊。每個預留範圍由 btree 構建器狀態資料單獨跟蹤。為了提高崩潰彈性,預留程式碼還在與每個空間分配相同的事務中記錄一個範圍釋放意圖 (EFI) 項,並將其記憶體中的 struct xfs_extent_free_item 物件附加到空間預留。如果系統關閉,日誌恢復將使用未完成的 EFI 來釋放未使用的空間,從而使檔案系統保持不變。

每次 btree 構建器從預留範圍中宣告一個用於 btree 的塊時,它都會更新記憶體中的預留以反映已宣告的空間。塊預留會嘗試分配儘可能多的連續空間,以減少正在使用的 EFI 數量。

當修復正在寫入這些新的 btree 塊時,為空間預留建立的 EFI 會固定磁碟上日誌的尾部。系統的其他部分可能會保持繁忙併將日誌的頭部推向固定的尾部。為了避免檔案系統發生死鎖,EFI 不得將日誌的尾部固定太長時間。為了緩解這個問題,此處重用了延遲操作機制的動態重新記錄功能,以提交一個位於日誌頭部的事務,該事務包含舊 EFI 的 EFD 和頭部的新 EFI。這使日誌能夠釋放舊 EFI 以保持日誌向前移動。

EFI 在提交和收割階段都發揮著作用;請參見下一節和關於 收割 的部分了解更多詳細資訊。

提議的補丁集是 點陣圖重做為批次載入 btree 做準備

4.5.6.3. 寫入新樹

這部分非常簡單 -- btree 構建器 (xfs_btree_bulkload) 從預留列表中宣告一個塊,寫入新的 btree 塊頭部,用記錄填充塊的其餘部分,並將新的葉子塊新增到已寫入塊的列表

┌────┐
│leaf│
│RRR │
└────┘

每次向級別新增新塊時,都會設定兄弟指標

┌────┐ ┌────┐ ┌────┐ ┌────┐
│leaf│→│leaf│→│leaf│→│leaf│
│RRR │←│RRR │←│RRR │←│RRR │
└────┘ └────┘ └────┘ └────┘

當它完成寫入記錄葉子塊時,它會移動到節點塊。為了填充節點塊,它會遍歷樹中下一級的每個塊,以計算相關的鍵並將它們寫入父節點

    ┌────┐       ┌────┐
    │node│──────→│node│
    │PP  │←──────│PP  │
    └────┘       └────┘
    ↙   ↘         ↙   ↘
┌────┐ ┌────┐ ┌────┐ ┌────┐
│leaf│→│leaf│→│leaf│→│leaf│
│RRR │←│RRR │←│RRR │←│RRR │
└────┘ └────┘ └────┘ └────┘

當它到達根級別時,它已準備好提交新的 btree!

        ┌─────────┐
        │  root   │
        │   PP    │
        └─────────┘
        ↙         ↘
    ┌────┐       ┌────┐
    │node│──────→│node│
    │PP  │←──────│PP  │
    └────┘       └────┘
    ↙   ↘         ↙   ↘
┌────┐ ┌────┐ ┌────┐ ┌────┐
│leaf│→│leaf│→│leaf│→│leaf│
│RRR │←│RRR │←│RRR │←│RRR │
└────┘ └────┘ └────┘ └────┘

提交新 btree 的第一步是將 btree 塊同步持久化到磁碟。這有點複雜,因為新的 btree 塊可能在最近的過去被釋放,因此構建器必須使用 xfs_buf_delwri_queue_here 從 AIL 列表中刪除(過時的)緩衝區,然後才能將新塊寫入磁碟。使用 delwri 列表將塊排隊以進行 IO,並使用 xfs_buf_delwri_submit 在一個大批處理中寫入塊。

一旦新的塊被持久化到磁碟,控制權將返回到呼叫批次載入器的單個修復函式。修復函式必須在事務中記錄新根的位置,清理為新 btree 所做的空間預留,並收割舊的元資料塊

  1. 提交新 btree 根的位置。

  2. 對於每個 incore 預留

    1. 為 btree 構建器使用的所有空間記錄範圍釋放完成 (EFD) 項。新的 EFD 必須指向附加到預留的 EFI,以防止日誌恢復釋放新的塊。

    2. 對於 incore 預留的未宣告部分,建立一個常規的延遲範圍釋放工作項,以便稍後在事務鏈中釋放未使用的空間。

    3. 在步驟 2a 和 2b 中記錄的 EFD 和 EFI 不得超出提交事務的預留。如果 btree 載入程式碼懷疑這可能即將發生,它必須呼叫 xrep_defer_finish 以清除延遲工作並獲得新的事務。

  3. 第二次清除延遲工作以完成提交併清理修復事務。

步驟 2c 和 3 中滾動的事務代表了修復演算法中的一個弱點,因為在收割步驟結束之前進行日誌重新整理和崩潰可能會導致空間洩漏。線上修復函式透過使用非常大的事務來最大限度地減少這種情況發生的可能性,每個事務都可以容納數千個塊釋放指令。修復會繼續收割舊塊,這將在以下 部分 中在幾個批次載入的案例研究之後進行介紹。

4.5.6.3.1. 案例研究:重建 Inode 索引

重建 inode 索引 btree 的高階過程是

  1. 遍歷反向對映記錄以從 inode 塊資訊和舊 inode btree 塊的點陣圖生成 struct xfs_inobt_rec 記錄。

  2. 按 inode 順序將記錄附加到 xfarray。

  3. 使用 xfs_btree_bload_compute_geometry 函式來計算 inode btree 所需的塊數。如果啟用了可用空間 inode btree,則再次呼叫它以估計 finobt 的幾何形狀。

  4. 分配上一步中計算的塊數。

  5. 使用 xfs_btree_bload 將 xfarray 記錄寫入 btree 塊並生成內部節點塊。如果啟用了可用空間 inode btree,則再次呼叫它以載入 finobt。

  6. 將新 btree 根塊的位置提交到 AGI。

  7. 使用在步驟 1 中建立的點陣圖收割舊的 btree 塊。

詳細資訊如下。

inode btree 將 inumber 對映到關聯的 inode 記錄在磁碟上的位置,這意味著 inode btree 可以從反向對映資訊重建。所有者為 XFS_RMAP_OWN_INOBT 的反向對映記錄標記了舊 inode btree 塊的位置。所有者為 XFS_RMAP_OWN_INODES 的每個反向對映記錄標記了至少一個 inode 叢集緩衝區的位置。叢集是在單個事務中可以分配或釋放的最小數量的磁碟上 inode;它永遠不小於 1 個 fs 塊或 4 個 inode。

對於每個 inode 叢集代表的空間,請確保在可用空間 btree 中沒有任何記錄,並且在引用計數 btree 中也沒有任何記錄。如果有,則空間元資料不一致足以中止操作。否則,讀取每個叢集緩衝區以檢查其內容是否顯示為磁碟上 inode,並確定該檔案是否已分配 (xfs_dinode.i_mode != 0) 或已釋放 (xfs_dinode.i_mode == 0)。累積連續 inode 叢集緩衝區讀取的結果,直到有足夠的資訊來填充單個 inode 塊記錄,即 inumber 鍵空間中的 64 個連續數字。如果塊是稀疏的,則塊記錄可能包含空洞。

一旦修復函式累積了一個塊的資料值,它將呼叫 xfarray_append 以將 inode btree 記錄新增到 xfarray。在 btree 建立步驟中,此 xfarray 被遍歷兩次 -- 第一次使用所有 inode 塊記錄填充 inode btree,第二次使用具有空閒非稀疏 inode 的塊的記錄填充空閒 inode btree。inode btree 的記錄數是 xfarray 記錄數,但必須在 xfarray 中儲存 inode 塊記錄時計算空閒 inode btree 的記錄計數。

提議的補丁集是 AG btree 修復 系列。

4.5.6.3.2. 案例研究:重建空間引用計數

反向對映記錄用於重建引用計數資訊。引用計數對於共享檔案資料的寫時複製的正確操作是必需的。將反向對映條目想象成代表物理塊範圍的矩形,並且可以放置矩形以允許它們相互重疊。從下面的圖中可以明顯看出,引用計數記錄必須在堆疊的高度發生變化的任何位置開始或結束。換句話說,記錄發射刺激是電平觸發的

                █    ███
      ██      █████ ████   ███        ██████
██   ████     ███████████ ████     █████████
████████████████████████████████ ███████████
^ ^  ^^ ^^    ^ ^^ ^^^  ^^^^  ^ ^^ ^  ^     ^
2 1  23 21    3 43 234  2123  1 01 2  3     0

磁碟上的引用計數 btree 不儲存 refcount == 0 的情況,因為可用空間 btree 已經記錄了哪些塊是空閒的。用於暫存寫時複製操作的範圍應該是唯一 refcount == 1 的記錄。單個所有者的檔案塊不會記錄在可用空間或引用計數 btree 中。

重建引用計數 btree 的高階過程是

  1. 遍歷反向對映記錄以生成 struct xfs_refcount_irec 記錄,用於任何具有多個反向對映的空間,並將它們新增到 xfarray。所有者為 XFS_RMAP_OWN_COW 的任何記錄也會新增到 xfarray,因為這些範圍是分配用於暫存寫時複製操作的範圍,並在 refcount btree 中進行跟蹤。

    使用所有者為 XFS_RMAP_OWN_REFC 的任何記錄來建立舊 refcount btree 塊的點陣圖。

  2. 按物理範圍順序對記錄進行排序,將 CoW 暫存範圍放在 xfarray 的末尾。這與 refcount btree 中記錄的排序順序相匹配。

  3. 使用 xfs_btree_bload_compute_geometry 函式來計算新樹所需的塊數。

  4. 分配上一步中計算的塊數。

  5. 使用 xfs_btree_bload 將 xfarray 記錄寫入 btree 塊並生成內部節點塊。

  6. 將新 btree 根塊的位置提交到 AGF。

  7. 使用在步驟 1 中建立的點陣圖收割舊的 btree 塊。

詳細資訊如下;xfs_repair 使用相同的演算法從反向對映記錄生成 refcount 資訊。

  • 直到反向對映 btree 用完記錄

    • 從 btree 中檢索下一個記錄並將其放入包中。

    • 從 btree 中收集所有具有相同起始塊的記錄,並將它們放入包中。

    • 只要包不為空

      • 在包中的對映中,計算引用計數發生變化的最低塊號。此位置將是下一個未處理的反向對映的起始塊號,或包中最短對映之後的下一個塊。

      • 從包中刪除所有在此位置結束的對映。

      • 從 btree 中收集所有在此位置開始的反向對映,並將它們放入包中。

      • 如果包的大小發生了變化並且大於 1,則建立一個新的 refcount 記錄,將我們剛剛遍歷到的塊號範圍與包的大小相關聯。

在這種情況下,類似包的結構是 xfarray 訪問模式 部分中討論的型別 2 xfarray。使用 xfarray_store_anywhere 將反向對映新增到包中,並使用 xfarray_unset 刪除。透過 xfarray_iter 迴圈檢查包成員。

提議的補丁集是 AG btree 修復 系列。

4.5.6.3.3. 案例研究:重建檔案分叉對映索引

重建資料/屬性分叉對映 btree 的高階過程是

  1. 遍歷反向對映記錄以從該 inode 和分叉的反向對映記錄生成 struct xfs_bmbt_rec 記錄。將這些記錄附加到 xfarray。從 BMBT_BLOCK 記錄計算舊 bmap btree 塊的點陣圖。

  2. 使用 xfs_btree_bload_compute_geometry 函式來計算新樹所需的塊數。

  3. 按檔案偏移量順序對記錄進行排序。

  4. 如果範圍記錄適合 inode 分叉立即區域,則將記錄提交到該立即區域並跳到步驟 8。

  5. 分配上一步中計算的塊數。

  6. 使用 xfs_btree_bload 將 xfarray 記錄寫入 btree 塊並生成內部節點塊。

  7. 將新的 btree 根塊提交到 inode 分叉立即區域。

  8. 使用在步驟 1 中建立的點陣圖收割舊的 btree 塊。

這裡有一些複雜性:首先,如果資料和屬性分叉都不是 BMBT 格式,則可以移動分叉偏移量以調整立即區域的大小。其次,如果分叉對映足夠少,則可以使用 EXTENTS 格式而不是 BMBT,這可能需要轉換。第三,必須小心地重新載入 incore 範圍對映,以避免干擾任何延遲分配範圍。

提議的補丁集是 檔案對映修復 系列。

4.5.7. 收割舊的元資料塊

每當線上 fsck 構建一個新的資料結構來替換可疑的資料結構時,都會出現如何查詢和處理屬於舊結構的塊的問題。當然,最簡單的方法是不處理它們,但這會慢慢導致服務降級,因為空間會從檔案系統中洩漏出來。希望有人會安排重建可用空間資訊,以堵住所有這些洩漏。離線修復會在記錄它決定不清除的檔案和目錄的使用情況後重建所有空間元資料,因此它可以在發現的可用空間中構建新結構,並避免收割問題。

作為修復的一部分,線上 fsck 嚴重依賴反向對映記錄來查詢由相應的 rmap 所有者擁有但實際上是空閒的空間。將 rmap 記錄與其他 rmap 記錄進行交叉引用是必要的,因為可能還有其他資料結構也認為它們擁有其中的一些塊(例如,交叉連結的樹)。允許塊分配器再次交出它們不會將系統推向一致性。

對於空間元資料,查詢要處理的範圍的過程通常遵循此格式

  1. 建立必須保留的資料結構使用的空間點陣圖。如果要重建的所有物件都使用相同的 rmap 所有者程式碼來表示,則可以使用用於建立新元資料的空間預留。

  2. 調查反向對映資料以建立由要保留的元資料的相同 XFS_RMAP_OWN_* 編號擁有的空間點陣圖。

  3. 使用點陣圖分離運算子從 (2) 中減去 (1)。剩餘的設定位代表可以釋放的候選範圍。該過程繼續執行下面的步驟 4。

基於檔案的元資料(例如擴充套件屬性、目錄、符號連結、配額檔案和即時點陣圖)的修復是透過構建附加到臨時檔案的新結構並交換檔案分叉中的所有對映來執行的。之後,舊檔案分叉中的對映是要處理的候選塊。

處理舊範圍的過程如下

  1. 對於每個候選範圍,統計該範圍中的第一個塊的反向對映記錄數,這些記錄沒有與正在修復的資料結構的 rmap 所有者相同。

    • 如果為零,則該塊具有單個所有者並且可以釋放。

    • 如果不是,則該塊是交叉連結結構的一部分,不得釋放。

  2. 從該範圍中的下一個塊開始,弄清楚有多少個塊與第一個塊具有相同的零/非零其他所有者狀態。

  3. 如果該區域已交叉連結,則刪除要修復的結構的反向對映條目並移動到下一個區域。

  4. 如果要釋放該區域,請將緩衝區快取中任何相應的緩衝區標記為過時,以防止日誌寫回。

  5. 釋放該區域並繼續。

但是,此過程存在一個複雜性。事務的大小是有限的,因此收割過程必須小心地滾動事務以避免溢位。溢位來自兩個來源

  1. 代表不再被佔據的空間記錄的 EFI

  2. 緩衝區失效的日誌項

這也是一個視窗,在此期間收割過程中的崩潰可能會導致塊洩漏。如前所述,線上修復函式使用非常大的事務來最大限度地減少這種情況發生的可能性。

提議的補丁集是 為批次載入 btree 做準備 系列。

4.5.7.1. 案例研究:常規 Btree 修復後收割

舊的引用計數和 inode btree 最容易收割,因為它們具有帶有特殊所有者程式碼的 rmap 記錄:引用計數 btree 的 XFS_RMAP_OWN_REFC,以及 inode 和空閒 inode btree 的 XFS_RMAP_OWN_INOBT。從概念上講,建立要收割舊 btree 塊的範圍列表非常簡單

  1. 鎖定相關的 AGI/AGF 頭部緩衝區以防止分配和釋放。

  2. 對於每個 rmap 所有者與正在重建的元資料結構相對應的反向對映記錄,在點陣圖中設定相應的範圍。

  3. 遍歷具有相同 rmap 所有者的當前資料結構。對於訪問的每個塊,清除上點陣圖中的該範圍。

  4. 點陣圖中的每個設定位都代表一個可能是舊資料結構的塊,因此是收割的候選物件。換句話說,(rmap_records_owned_by & ~blocks_reachable_by_walk) 是可能可以釋放的塊。

如果在整個修復過程中都可以維護 AGF 鎖(這是常見的情況),則可以與建立新 btree 記錄的反向對映記錄遍歷同時執行步驟 2。

4.5.7.2. 案例研究:重建可用空間索引

重建可用空間索引的高階過程是

  1. 遍歷反向對映記錄以從反向對映 btree 中的間隙生成 struct xfs_alloc_rec_incore 記錄。

  2. 將記錄附加到 xfarray。

  3. 使用 xfs_btree_bload_compute_geometry 函式來計算每個新樹所需的塊數。

  4. 從收集的可用空間資訊中分配上一步中計算的塊數。

  5. 使用 xfs_btree_bload 將 xfarray 記錄寫入 btree 塊並生成按長度索引的可用空間的內部節點塊。再次為按塊號索引的可用空間呼叫它。

  6. 將新 btree 根塊的位置提交到 AGF。

  7. 透過查詢反向對映 btree、新可用空間 btree 或 AGFL 未記錄的空間來收割舊的 btree 塊。

與常規 btree 修復相比,修復可用空間 btree 存在三個關鍵複雜性

首先,可用空間不會在反向對映記錄中顯式跟蹤。因此,必須從反向對映 btree 的鍵空間中的物理空間元件中的間隙推斷出新的可用空間記錄。

其次,可用空間修復不能使用通用的 btree 預留程式碼,因為新的塊是從可用空間 btree 中預留的。當修復可用空間 btree 本身時,這是不可能的。但是,修復在可用空間索引重建期間會持有 AGF 緩衝區鎖,因此它可以使用收集的可用空間資訊來為新的可用空間 btree 提供塊。沒有必要使用 EFI 來支援每個預留範圍,因為新的可用空間 btree 是在磁碟上的檔案系統認為是不屬於任何人的空間中構建的。但是,如果從收集的可用空間資訊中預留新 btree 的塊會更改可用空間記錄的數量,則修復必須使用新記錄計數重新估計新的可用空間 btree 幾何形狀,直到預留足夠為止。作為提交新 btree 的一部分,修復必須確保為預留塊建立反向對映,並將未使用的預留塊插入到可用空間 btree 中。延遲的 rmap 和釋放操作用於確保此過渡是原子性的,類似於其他 btree 修復函式。

第三,修復後找到需要回收的塊並非易事。自由空間B樹和反向對映B樹的塊由AGFL提供。放置到AGFL上的塊具有反向對映記錄,所有者為XFS_RMAP_OWN_AG。當塊從AGFL移動到自由空間B樹或反向對映B樹時,此所有權將被保留。當修復程式遍歷反向對映記錄以合成自由空間記錄時,它會建立一個位圖(ag_owner_bitmap),其中包含所有XFS_RMAP_OWN_AG記錄宣告的空間。修復上下文維護第二個點陣圖,對應於rmap B樹塊和AGFL塊(rmap_agfl_bitmap)。遍歷完成後,點陣圖差集運算(ag_owner_bitmap & ~rmap_agfl_bitmap)計算出舊自由空間B樹使用的範圍。然後可以使用上面概述的方法回收這些塊。

提議的補丁集是 AG btree 修復 系列。

4.5.7.3. 案例研究:修復反向對映B樹後進行回收

修復舊的反向對映B樹後,回收起來更容易。如上一節所述,AGFL上的塊、兩個自由空間B樹塊和反向對映B樹塊都具有反向對映記錄,所有者為XFS_RMAP_OWN_AG。收集反向對映記錄並構建新B樹的完整過程在rmap資料動態重建的案例研究中進行了描述,但該討論的一個關鍵點是,新的rmap B樹將不包含舊rmap B樹的任何記錄,並且舊的B樹塊也不會在自由空間B樹中進行跟蹤。候選回收塊的列表透過設定新rmapbt B樹記錄中的間隙對應的位,然後清除自由空間B樹和當前AGFL塊中的範圍對應的位來計算。結果(new_rmapbt_gaps & ~(agfl | bnobt_records)) 使用上面概述的方法進行回收。

重建反向對映B樹的其餘過程在單獨的案例研究中進行了討論。

提議的補丁集是 AG btree 修復 系列。

4.5.7.4. 案例研究:重建AGFL

分配組自由塊列表(AGFL)的修復方式如下:

  1. 為反向對映資料宣告為XFS_RMAP_OWN_AG擁有的所有空間建立一個位圖。

  2. 減去兩個自由空間B樹和rmap B樹使用的空間。

  3. 減去反向對映資料宣告為任何其他所有者擁有的任何空間,以避免將交叉連結的塊重新新增到AGFL。

  4. AGFL填滿後,回收任何剩餘的塊。

  5. 下一個修復空閒列表的操作將調整列表的大小。

有關更多詳細資訊,請參見fs/xfs/scrub/agheader_repair.c

4.5.8. Inode記錄修復

Inode記錄必須小心處理,因為它們既有磁碟上的記錄(“dinodes”),也有記憶體中的(“快取”)表示形式。如果線上fsck在訪問磁碟上的元資料,沒有小心謹慎地訪問磁碟上的元資料,因為磁碟上的元資料損壞嚴重,檔案系統無法載入記憶體中的表示形式,那麼存在很高的快取一致性問題風險。當線上fsck想要開啟一個損壞的檔案進行清理時,它必須使用專門的資源獲取函式,該函式返回記憶體中的表示形式一個鎖,該鎖對於阻止對磁碟上位置的任何更新是必需的。

應該對磁碟上的inode緩衝區進行的唯一修復是為了載入核心結構所必需的任何操作。這意味著修復inode叢集緩衝區和inode fork驗證程式捕獲的任何內容,並重試iget操作。如果第二次iget失敗,則修復失敗。

一旦載入了記憶體中的表示形式,修復程式可以鎖定inode,並可以對其進行全面的檢查、修復和最佳化。大多數inode屬性都很容易檢查和約束,或者是使用者控制的任意位模式;這些都容易修復。處理資料和attr fork範圍計數以及檔案塊計數更為複雜,因為計算正確的值需要遍歷fork,或者如果失敗,則使欄位無效並等待fork fsck函式執行。

建議的補丁集是inode修復系列。

4.5.9. 配額記錄修復

與inode類似,配額記錄(“dquots”)也具有磁碟上的記錄和記憶體中的表示形式,因此也受到相同的快取一致性問題的約束。有些令人困惑的是,在XFS程式碼庫中,兩者都稱為dquots。

應該對磁碟上的配額記錄緩衝區進行的唯一修復是為了載入核心結構所必需的任何操作。一旦載入了記憶體中的表示形式,唯一需要檢查的屬性是明顯錯誤的限制和計時器值。

配額使用計數器的檢查、修復和討論在關於動態quotacheck的章節中單獨進行。

建議的補丁集是配額修復系列。

4.5.10. 凍結以修復摘要計數器

檔案系統摘要計數器跟蹤檔案系統資源的可用性,例如自由塊、自由inode和已分配inode。此資訊可以透過遍歷自由空間和inode索引來編譯,但這是一個緩慢的過程,因此XFS在磁碟上的超級塊中維護了一個副本,該副本應反映磁碟上的元資料,至少在檔案系統已乾淨解除安裝時是這樣。出於效能原因,XFS還維護這些計數器的核心副本,這些副本是為活動事務啟用資源保留的關鍵。編寫器執行緒從核心計數器保留最壞情況下的資源數量,並在提交時返回他們不使用的任何資源。因此,只有在將超級塊提交到磁碟時,才需要對超級塊進行序列化。

XFS v5中引入的惰性超級塊計數器功能透過訓練日誌恢復以從AG標頭重新計算摘要計數器,從而更進一步,這消除了大多數事務甚至觸控超級塊的需求。XFS提交摘要計數器的唯一時間是在檔案系統解除安裝時。為了進一步減少爭用,核心計數器被實現為percpu計數器,這意味著每個CPU都從全域性核心計數器分配一批塊,並且可以滿足來自本地批的小分配。

摘要計數器的高效能性質使得線上fsck難以檢查它們,因為無法在系統執行時停止percpu計數器。儘管線上fsck可以讀取檔案系統元資料來計算摘要計數器的正確值,但無法保持percpu計數器的值穩定,因此在遍歷完成時,計數器很可能已過期。早期版本的線上清理會將不完整的掃描標誌返回給使用者空間,但這對於系統管理員來說不是一個令人滿意的結果。對於修復,在遍歷檔案系統元資料以獲得準確的讀取並將其安裝在percpu計數器中時,必須穩定記憶體中的計數器。

為了滿足此要求,線上fsck必須阻止系統中其他程式啟動新的檔案系統寫入,它必須停用後臺垃圾收集執行緒,並且它必須等待現有編寫器程式退出核心。一旦建立,清理就可以遍歷AG自由空間索引、inode B樹和即時點陣圖,以計算所有四個摘要計數器的正確值。這與檔案系統凍結非常相似,儘管並非所有部分都是必需的。

  • 最終凍結狀態設定為高於SB_FREEZE_COMPLETE,以防止其他執行緒解凍檔案系統,或其他清理執行緒啟動另一個fscounters凍結。

  • 它不會停止日誌。

有了這段程式碼,現在可以暫停檔案系統,只需足夠長的時間來檢查和更正摘要計數器。

歷史側邊欄:

最初的實現使用了實際的VFS檔案系統凍結機制來停止檔案系統活動。在檔案系統凍結的情況下,可以精確地解析計數器值,但是直接呼叫VFS方法存在許多問題

  • 其他程式可以在我們不知情的情況下解凍檔案系統。這導致不正確的掃描結果和不正確的修復。

  • 新增額外的鎖以防止其他人解凍檔案系統需要在freeze_fs()周圍新增一個->freeze_super函式。反過來,這導致了其他微妙的問題,因為事實證明VFS freeze_superthaw_super函式可以刪除對VFS超級塊的最後一個引用,並且任何後續訪問都會成為UAF錯誤!如果底層塊裝置凍結了檔案系統,則可能發生這種情況。這個問題可以透過獲取對超級塊的額外引用來解決,但是鑑於此方法的其他不足之處,感覺並不理想。

  • 無需停止日誌即可檢查摘要計數器,但是VFS凍結無論如何都會啟動一個。這會給動態fscounter fsck操作增加不必要的執行時。

  • 停止日誌意味著XFS會將(可能不正確的)計數器重新整理到磁碟,作為清理日誌的一部分。

  • VFS中的一個錯誤意味著即使sync_filesystem無法重新整理檔案系統並返回錯誤,凍結也可能完成。此錯誤已在Linux 5.17中修復。

建議的補丁集是摘要計數器清理系列。

4.5.11. 完整檔案系統掃描

某些型別的元資料只能透過遍歷整個檔案系統中的每個檔案來記錄觀察結果,並將觀察結果與磁碟上記錄的內容進行比較來檢查。與任何其他型別的線上修復一樣,修復是透過將這些觀察結果寫入替換結構並以原子方式提交來進行的。但是,關閉整個檔案系統以檢查數千億個檔案是不切實際的,因為停機時間會過長。因此,線上fsck必須構建基礎結構來管理對檔案系統中所有檔案的動態掃描。要執行動態遍歷,需要解決兩個問題

  • 清理程式在收集資料時如何管理掃描?

  • 掃描如何跟上其他執行緒對系統進行的更改?

4.5.11.1. 協調的Inode掃描

在1970年代的原始Unix檔案系統中,每個目錄條目都包含一個索引號(inumber),該索引號用作固定大小記錄(inodes)的磁碟上陣列(itable)的索引,這些記錄描述了檔案的屬性及其資料塊對映。J. Lions在Lions’ Commentary on UNIX, 6th Edition,(Dept. of Computer Science, the University of New South Wales, November 1977),pp. 18-2; 中描述了該系統。後來由D. Ritchie和K. Thompson在The UNIX Time-Sharing System,(The Bell System Technical Journal, July 1978),pp. 1913-4. 中的“inode (5659)”“Implementation of the File System”中進行了描述。

XFS保留了此設計的大部分內容,只是現在inumber是資料部分檔案系統中所有空間上的搜尋鍵。它們形成一個連續的金鑰空間,可以用64位整數表示,儘管inode本身在金鑰空間中是稀疏分佈的。掃描以線性方式在inumber金鑰空間中進行,從0x0開始,到0xFFFFFFFFFFFFFFFF結束。自然地,透過金鑰空間的掃描需要一個掃描游標物件來跟蹤掃描進度。由於此金鑰空間是稀疏的,因此該游標包含兩個部分。此掃描游標物件的第一個部分跟蹤下一個將要檢查的inode;稱其為檢查游標。不太明顯的是,掃描游標物件還必須跟蹤金鑰空間的哪些部分已經被訪問過,這對於確定是否需要將併發檔案系統更新合併到掃描資料中至關重要。稱其為已訪問inode游標。

推進掃描游標是一個多步驟過程,封裝在xchk_iscan_iter

  1. 鎖定包含由已訪問inode游標指向的inode的AGI緩衝區。這保證了在推進游標時,此AG中的inode無法被分配或釋放。

  2. 使用per-AG inode B樹查詢剛訪問的inode之後的下一個inumber,因為它可能不是金鑰空間相鄰的。

  3. 如果此AG中沒有剩餘的inode

    1. 將檢查游標移動到與下一個AG的開頭相對應的inumber金鑰空間點。

    2. 調整已訪問inode游標,以指示它已“訪問”當前AG的inode金鑰空間中的最後一個可能的inode。XFS inumber是分段的,因此游標需要標記為已訪問直到剛好在下一個AG的inode金鑰空間開始之前的所有金鑰空間。

    3. 解鎖AGI,如果檔案系統中存在未檢查的AG,則返回到步驟1。

    4. 如果沒有更多AG要檢查,請將兩個游標都設定為inumber金鑰空間的末尾。掃描現已完成。

  4. 否則,此AG中至少還有另一個inode要掃描

    1. 將檢查游標向前移動到inode B樹標記為已分配的下一個inode。

    2. 調整已訪問inode游標以指向剛好在檢查游標當前位置之前的inode。因為掃描器持有AGI緩衝區鎖,所以無法在已訪問inode游標剛剛推進的inode金鑰空間的一部分中建立任何inode。

  5. 獲取檢查游標的inumber的核心inode。透過將AGI緩衝區鎖保持到此時,掃描器知道可以安全地跨整個金鑰空間推進檢查游標,並且它已經穩定了下一個inode,因此直到掃描釋放核心inode,它才可能從檔案系統中消失。

  6. 放下AGI鎖,並將核心inode返回給呼叫方。

線上fsck函式按以下方式掃描檔案系統中的所有檔案

  1. 透過呼叫xchk_iscan_start啟動掃描。

  2. 推進掃描游標(xchk_iscan_iter)以獲取下一個inode。如果提供了一個inode

    1. 鎖定inode以防止在掃描期間進行更新。

    2. 掃描inode。

    3. 在仍持有inode鎖定的情況下,調整已訪問inode游標(xchk_iscan_mark_visited)以指向此inode。

    4. 解鎖並釋放inode。

  1. 呼叫xchk_iscan_teardown以完成掃描。

inode快取存在一些細微之處,這使得為呼叫方抓取核心inode變得複雜。顯然,inode元資料必須足夠一致才能將其載入到inode快取中。其次,如果核心inode卡在某個中間狀態,則掃描協調器必須釋放AGI並推動主檔案系統以使inode恢復到可載入狀態。

建議的補丁是inode掃描器系列。新功能的第一個使用者是線上quotacheck系列。

4.5.11.2. Inode管理

在常規檔案系統程式碼中,對已分配XFS核心inode的引用始終在事務上下文之外獲取(xfs_iget),因為為現有檔案建立核心上下文不需要元資料更新。但是,重要的是要注意,作為檔案建立一部分獲取的對核心inode的引用必須在事務上下文中執行,因為檔案系統必須確保磁碟上的inode B樹索引更新和實際磁碟上的inode初始化的原子性。

對核心inode的引用始終在事務上下文之外釋放(xfs_irele),因為有一些活動可能需要磁碟上的更新

  • VFS可能會決定啟動回寫,作為DONTCACHE inode釋放的一部分。

  • 需要取消保留推測性預分配。

  • 未連結的檔案可能丟失了其最後一個引用,在這種情況下,必須使整個檔案失效,這涉及釋放其在磁碟上的元資料中的所有資源並釋放inode。

這些活動統稱為inode失活。失活有兩個部分——VFS部分,它啟動所有髒檔案頁的回寫,以及XFS部分,它清理XFS特定的資訊並在inode未連結時釋放inode。如果inode未連結(或在檔案控制代碼操作後未連線),則核心會立即將inode放入失活機制中。

在正常操作期間,更新的資源獲取遵循此順序以避免死鎖

  1. Inode引用(iget)。

  2. 檔案系統凍結保護,如果在修復(mnt_want_write_file)。

  3. Inode IOLOCK(VFS i_rwsem)鎖來控制檔案IO。

  4. Inode MMAPLOCK(頁面快取invalidate_lock)鎖用於可以更新頁面快取對映的操作。

  5. 日誌功能啟用。

  6. 事務日誌空間授予。

  7. 資料和即時裝置上的事務空間。

  8. 如果正在修復檔案,則為核心dquot引用。請注意,它們沒有被鎖定,只是被獲取。

  9. Inode ILOCK用於檔案元資料更新。

  10. AG標頭緩衝區鎖/即時元資料inode ILOCK。

  11. 即時元資料緩衝區鎖,如果適用。

  12. 範圍對映B樹塊,如果適用。

資源通常以相反的順序釋放,儘管這不是必需的。但是,線上fsck與常規XFS操作不同,因為它可能會檢查通常在鎖定順序的後期階段獲取的物件,然後決定將該物件與在順序中較早獲取的物件進行交叉引用。接下來的幾個部分詳細介紹了線上fsck如何小心避免死鎖的具體方法。

4.5.11.2.1. 清理期間的iget和irele

代表清理操作執行的inode掃描在事務上下文中執行,並且可能已經鎖定了資源並將其繫結到該事務上下文中。對於iget來說,這並不是什麼大問題,因為它可以在現有事務的上下文中執行,只要所有繫結資源都在常規檔案系統中的inode引用之前獲取即可。

當VFS iput函式被賦予一個沒有其他引用的連結inode時,它通常會將inode放在LRU列表中,希望如果另一個程序在系統耗盡記憶體並釋放inode之前重新開啟該檔案,則可以節省時間。檔案系統呼叫方可以透過在inode上設定DONTCACHE標誌來使LRU過程短路,以導致核心嘗試立即將inode放入失活機制中。

過去,失活總是從刪除inode的程序完成的,這對於清理來說是一個問題,因為清理可能已經持有一個事務,並且XFS不支援巢狀事務。另一方面,如果沒有清理事務,則最好立即刪除未使用的inode,以避免汙染快取。為了捕捉這些細微之處,線上fsck程式碼有一個單獨的xchk_irele函式來設定或清除DONTCACHE標誌以獲得所需的釋放行為。

建議的補丁集包括修復清理iget用法dir iget用法

4.5.11.2.2. 鎖定Inode

在常規檔案系統程式碼中,VFS和XFS將以眾所周知的順序獲取多個IOLOCK鎖:更新目錄樹時為父→子,否則按其struct inode物件的地址的數字順序獲取。對於常規檔案,可以在獲取IOLOCK後獲取MMAPLOCK以停止頁面錯誤。如果必須獲取兩個MMAPLOCK,則按其struct address_space物件的地址的數字順序獲取。由於現有檔案系統程式碼的結構,必須在分配事務之前獲取IOLOCK和MMAPLOCK。如果必須獲取兩個ILOCK,則按inumber順序獲取。

在協調的inode掃描期間,必須小心執行Inode鎖獲取。線上fsck不能遵守這些約定,因為對於目錄樹掃描器,清理程序持有正在掃描的檔案的IOLOCK,並且需要獲取目錄連結另一端檔案的IOLOCK。如果目錄樹已損壞,因為它包含一個迴圈,則xfs_scrub無法使用常規inode鎖定函式並避免陷入ABBA死鎖。

解決這兩個問題很簡單——任何時候線上fsck需要獲取同一類的第二個鎖時,它都會使用trylock來避免ABBA死鎖。如果trylock失敗,則清理會刪除所有inode鎖,並使用trylock迴圈來(重新)獲取所有必需的資源。Trylock迴圈使清理可以檢查掛起的致命訊號,這就是清理避免死鎖檔案系統或成為無響應程序的方式。但是,trylock迴圈意味著線上fsck必須準備好測量鎖定週期之前和之後的清理資源,以檢測更改並做出相應的反應。

4.5.11.2.3. 案例研究:查詢目錄父級

考慮目錄父指標修復程式碼作為一個例子。線上fsck必須驗證目錄的dotdot dirent是否指向父目錄,並且父目錄是否包含僅一個指向子目錄的dirent。完全驗證這種關係(並在可能的情況下修復它)需要在持有子級鎖定的情況下遍歷檔案系統上的每個目錄,並且在目錄樹正在更新時進行。協調的inode掃描提供了一種遍歷檔案系統的方法,而不會遺漏inode。子目錄保持鎖定以防止更新dotdot dirent,但是如果掃描器無法鎖定父級,則可以刪除並重新鎖定子級和預期的父級。如果在目錄解鎖時dotdot條目發生更改,則移動或重新命名操作必須已更改子級的父級,並且掃描可以提前退出。

建議的補丁集是目錄修復系列。

4.5.11.3. 檔案系統掛鉤

線上fsck函式在完整檔案系統掃描期間需要的第二個支援是能夠隨時瞭解檔案系統中其他執行緒所做的更新,因為與過去的比較在動態環境中毫無用處。兩個Linux核心基礎結構使線上fsck能夠監視常規檔案系統操作:檔案系統掛鉤和靜態鍵

檔案系統掛鉤將有關正在進行的檔案系統操作的資訊傳遞給下游使用者。在這種情況下,下游使用者始終是線上fsck函式。由於多個fsck函式可以並行執行,因此線上fsck使用Linux通知程式呼叫鏈工具將更新分派給任何數量的感興趣的fsck程序。呼叫鏈是一個動態列表,這意味著可以在執行時對其進行配置。由於這些掛鉤是XFS模組私有的,因此傳遞的資訊僅包含檢查函式更新其觀察結果所需的內容。

當前XFS掛鉤的實現使用SRCU通知程式鏈來減少對高度執行緒化工作負載的影響。常規阻塞通知程式鏈使用rwsem,並且對於單執行緒應用程式而言,開銷似乎要低得多。但是,事實證明,阻塞鏈和靜態鍵的組合是更高效的組合;此處需要更多研究。

要掛鉤檔案系統中的某個點,需要以下部分

  • 一個struct xfs_hooks物件必須嵌入在方便的位置,例如眾所周知的核心檔案系統物件中。

  • 每個掛鉤必須定義一個操作程式碼和一個包含有關操作的更多上下文的結構。

  • 掛鉤提供程式應在xfs_hooksxfs_hook物件周圍提供適當的包裝器函式和結構,以利用型別檢查來確保正確使用。

  • 必須選擇常規檔案系統程式碼中的呼叫站點來使用操作程式碼和資料結構呼叫xfs_hooks_call。此位置應與檔案系統更新提交到事務的位置相鄰(且不早於該位置)。通常,當檔案系統呼叫掛鉤鏈時,它應該能夠處理休眠並且不應容易受到記憶體回收或鎖定遞迴的影響。但是,確切的要求在很大程度上取決於掛鉤呼叫方和被呼叫方的上下文。

  • 線上fsck函式應定義一個結構來儲存掃描資料,一個鎖來協調對掃描資料的訪問,以及一個struct xfs_hook物件。掃描器函式和常規檔案系統程式碼必須以相同的順序獲取資源;有關詳細資訊,請參見下一節。

  • 線上fsck程式碼必須包含一個C函式來捕獲掛鉤操作程式碼和資料結構。如果正在更新的物件已被掃描訪問過,則必須將掛鉤資訊應用於掃描資料。

  • 在解鎖inode以開始掃描之前,線上fsck必須呼叫xfs_hooks_setup來初始化struct xfs_hook,並呼叫xfs_hooks_add來啟用掛鉤。

  • 線上fsck必須呼叫xfs_hooks_del以在掃描完成後停用掛鉤。

應將掛鉤數量保持在最低限度以降低複雜性。靜態鍵用於在線上fsck未執行時將檔案系統掛鉤的開銷降低到幾乎為零。

4.5.11.4. 掃描期間的即時更新

線上 fsck 掃描程式碼和hook住的檔案系統程式碼的程式碼路徑如下所示

other program
      ↓
inode lock ←────────────────────┐
      ↓                         │
AG header lock                  │
      ↓                         │
filesystem function             │
      ↓                         │
notifier call chain             │    same
      ↓                         ├─── inode
scrub hook function             │    lock
      ↓                         │
scan data mutex ←──┐    same    │
      ↓            ├─── scan    │
update scan data   │    lock    │
      ↑            │            │
scan data mutex ←──┘            │
      ↑                         │
inode lock ←────────────────────┘
      ↑
scrub function
      ↑
inode scanner
      ↑
xfs_scrub

必須遵循以下規則,以確保檢查程式碼和對檔案系統進行更新的程式碼之間的正確互動

  • 在呼叫通知器呼叫鏈之前,被 hook 住的檔案系統函式必須獲取與 scrub 掃描函式掃描 inode 時獲取的相同的鎖。

  • 掃描函式和 scrub hook 函式必須透過獲取掃描資料上的鎖來協調對掃描資料的訪問。

  • Scrub hook 函式不得將即時更新資訊新增到掃描觀察結果中,除非正在更新的 inode 已經過掃描。 掃描協調器為此提供了一個輔助謂詞 (xchk_iscan_want_live_update)。

  • Scrub hook 函式不得更改呼叫者的狀態,包括它正在執行的事務。 它們不得獲取任何可能與被 hook 住的檔案系統函式衝突的資源。

  • hook 函式可以中止 inode 掃描以避免違反其他規則。

inode 掃描 API 非常簡單

  • xchk_iscan_start 啟動掃描

  • xchk_iscan_iter 獲取對掃描中的下一個 inode 的引用,如果沒有剩餘的要掃描的內容,則返回零

  • xchk_iscan_want_live_update 用於確定 inode 是否已在掃描中訪問過。 這對於 hook 函式決定是否需要更新記憶體中的掃描資訊至關重要。

  • xchk_iscan_mark_visited 用於將 inode 標記為已在掃描中訪問過

  • xchk_iscan_teardown 用於完成掃描

此功能也是 inode 掃描器 系列的一部分。

4.5.11.4.1. 案例研究:配額計數器檢查

比較掛載時間配額檢查程式碼與線上修復配額檢查程式碼非常有用。 掛載時間配額檢查不必與併發操作競爭,因此它執行以下操作

  1. 確保 ondisk dquot 的狀態足夠好,所有 incore dquot 都可以實際載入,並將 ondisk 緩衝區中的資源使用計數器歸零。

  2. 遍歷檔案系統中的每個 inode。 將每個檔案的資源使用情況新增到 incore dquot。

  3. 遍歷每個 incore dquot。 如果 incore dquot 沒有被重新整理,則將支援 incore dquot 的 ondisk 緩衝區新增到延遲寫入 (delwri) 列表。

  4. 將緩衝區列表寫入磁碟。

與大多數線上 fsck 函式一樣,線上配額檢查在新的收集的元資料反映所有檔案系統狀態之前,無法寫入常規檔案系統物件。 因此,線上配額檢查將檔案資源使用情況記錄到使用稀疏 xfarray 實現的影子 dquot 索引中,並且僅在掃描完成後才寫入真正的 dquot。 處理事務性更新很棘手,因為配額資源使用情況更新分階段處理,以最大限度地減少對 dquot 的爭用

  1. 所涉及的 inode 被連線並鎖定到事務。

  2. 對於附加到檔案的每個 dquot

    1. dquot 被鎖定。

    2. 配額預留被新增到 dquot 的資源使用情況中。 預留記錄在事務中。

    3. dquot 被解鎖。

  3. 實際配額使用情況的變化在事務中被跟蹤。

  4. 在事務提交時,再次檢查每個 dquot

    1. dquot 再次被鎖定。

    2. 配額使用情況更改被記錄,未使用的預留被返回給 dquot。

    3. dquot 被解鎖。

對於線上配額檢查,hook 被放置在步驟 2 和 4 中。步驟 2 的 hook 建立事務 dquot 上下文 (dqtrx) 的影子版本,其操作方式與常規程式碼類似。 步驟 4 的 hook 將影子 dqtrx 更改提交到影子 dquot。 請注意,這兩個 hook 都使用鎖定的 inode 呼叫,這就是即時更新與 inode 掃描器協調的方式。

配額檢查掃描如下所示

  1. 設定一個協調的 inode 掃描。

  2. 對於 inode 掃描迭代器返回的每個 inode

    1. 獲取並鎖定 inode。

    2. 確定 inode 的資源使用情況(資料塊、inode 計數、即時塊),並將其新增到與 inode 關聯的使用者、組和專案 ID 的影子 dquot 中。

    3. 解鎖並釋放inode。

  3. 對於系統中的每個 dquot

    1. 獲取並鎖定 dquot。

    2. 根據掃描建立並由即時 hook 更新的影子 dquot 檢查 dquot。

即時更新是能夠遍歷每個配額記錄的關鍵,而無需長時間持有任何鎖。 如果需要修復,則鎖定真實 dquot 和影子 dquot,並且它們的資源計數被設定為影子 dquot 中的值。

提議的補丁集是 線上配額檢查 系列。

4.5.11.4.3. 案例研究:重建反向對映記錄

大多數修復函式遵循相同的模式:鎖定檔案系統資源,遍歷現存的 ondisk 元資料以查詢替換元資料記錄,並使用記憶體中的陣列來儲存收集到的觀察結果。 這種方法的主要優點是修復程式碼的簡單性和模組化 - 程式碼和資料完全包含在 scrub 模組中,不需要主檔案系統中的 hook,並且通常在記憶體使用方面最有效。 這種修復方法的第二個優點是原子性 - 一旦核心確定某個結構已損壞,在核心完成修復和重新驗證元資料之前,沒有其他執行緒可以訪問該元資料。

對於在檔案系統的分片中進行的修復,這些優點超過了在修復分片的部分時鎖定分片所固有的延遲。 不幸的是,反向對映 btree 的修復不能使用“標準” btree 修復策略,因為它必須掃描檔案系統中每個檔案的每個分支的每個空間對映,並且檔案系統不能停止。 因此,rmap 修復放棄了 scrub 和修復之間的原子性。 它結合了協調的 inode 掃描器即時更新 hook記憶體中的 rmap btree來完成反向對映記錄的掃描。

  1. 設定一個 xfbtree 來暫存 rmap 記錄。

  2. 在持有 scrub 期間獲取的 AGI 和 AGF 緩衝區上的鎖時,為所有 AG 元資料生成反向對映:inodes、btrees、CoW 暫存 extent 和內部日誌。

  3. 設定一個 inode 掃描器。

  4. hook 到正在修復的 AG 的 rmap 更新中,以便即時掃描資料可以在檔案掃描期間接收來自檔案系統其餘部分的反向對映 btree 的更新。

  5. 對於在掃描的每個檔案的任一分支中找到的每個空間對映,確定該對映是否與感興趣的 AG 匹配。 如果是,則

    1. 為記憶體中的 btree 建立一個 btree 游標。

    2. 使用 rmap 程式碼將記錄新增到記憶體中的 btree。

    3. 使用特殊提交函式將 xfbtree 更改寫入 xfile。

  6. 對於透過 hook 接收到的每個即時更新,確定所有者是否已經被掃描。 如果是,則將即時更新應用於掃描資料中

    1. 為記憶體中的 btree 建立一個 btree 游標。

    2. 將操作重放到記憶體中的 btree 中。

    3. 使用特殊提交函式將 xfbtree 更改寫入 xfile。 這是使用空事務執行的,以避免更改呼叫者的狀態。

  7. 當 inode 掃描完成時,建立一個新的 scrub 事務並重新鎖定兩個 AG 標頭。

  8. 像所有其他 btree 重建函式一樣,使用影子 btree 中的 rmap 記錄數計算新的 btree 幾何。

  9. 分配上一步中計算的塊數。

  10. 執行通常的 btree 批次載入並提交以安裝新的 rmap btree。

  11. 如關於如何在 rmap btree 修復後進行收割的案例研究中所討論的那樣,收割舊的 rmap btree 塊。

  12. 釋放 xfbtree,因為它現在不需要了。

提議的補丁集是 rmap 修復 系列。

4.5.12. 使用磁碟上的臨時檔案暫存修復

XFS 在檔案分支中儲存大量的元資料:目錄、擴充套件屬性、符號連結目標、即時卷的可用空間點陣圖和摘要資訊以及配額記錄。 檔案分支將 64 位邏輯檔案分支空間 extent 對映到物理儲存空間 extent,類似於記憶體管理單元將 64 位虛擬地址對映到物理記憶體地址的方式。 因此,基於檔案的樹結構(例如目錄和擴充套件屬性)使用對映在檔案分支偏移地址空間中的塊,這些塊指向對映在同一地址空間內的其他塊,而基於檔案的線性結構(例如點陣圖和配額記錄)則計算檔案分支偏移地址空間中的陣列元素偏移量。

由於檔案分支可以消耗與整個檔案系統一樣多的空間,因此即使有分頁方案可用,也無法在記憶體中暫存修復。 因此,基於檔案的元資料的線上修復會在 XFS 檔案系統中建立一個臨時檔案,將新的結構以正確的偏移量寫入臨時檔案,並以原子方式交換所有檔案分支對映(以及分支內容)以提交修復。 修復完成後,可以根據需要收割舊的分支; 如果系統在收割期間關閉,則 iunlink 程式碼將在日誌恢復期間刪除這些塊。

注意:檔案系統中的所有空間使用情況和 inode 索引必須一致才能安全地使用臨時檔案! 此依賴關係是線上修復只能使用可分頁核心記憶體來暫存 ondisk 空間使用情況資訊的原因。

使用臨時檔案交換元資料檔案對映要求塊標頭的 owner 欄位與正在修復的檔案匹配,而不是與臨時檔案匹配。 目錄、擴充套件屬性和符號連結函式都經過修改,允許呼叫者顯式指定所有者編號。

收割過程存在一個缺點——如果在收割階段系統崩潰並且分支 extent 是交叉連結的,則 iunlink 處理將失敗,因為釋放空間會找到額外的反向對映並中止。

為修復建立的臨時檔案類似於使用者空間建立的 O_TMPFILE 檔案。 它們未連結到目錄中,並且當對該檔案的最後一個引用丟失時,將收割整個檔案。 主要區別在於,這些檔案必須完全沒有核心外部的訪問許可權,它們必須被特別標記以防止被控制代碼開啟,並且它們絕不能連結到目錄樹中。

歷史側邊欄:

在檔案元資料修復的初始迭代中,將掃描損壞的元資料塊以查詢可挽救的資料; 將收割檔案分支中的 extent; 然後將在其位置構建新的結構。 該策略並未在本文件前面表達的原子修復要求的引入中倖存下來。

第二次迭代探索了從挽救資料在分支的高偏移量處構建第二個結構,收割舊的 extent,並使用 COLLAPSE_RANGE 操作將新的 extent 滑動到位。

這有很多缺點

  • 陣列結構是線性定址的,並且常規檔案系統程式碼庫沒有可以應用於記錄偏移量計算以構建備用副本的線性偏移量的概念。

  • 擴充套件屬性允許使用整個 attr 分支偏移地址空間。

  • 即使修復可以在分支地址空間的不同部分構建資料結構的備用副本,原子修復提交要求也意味著線上修復必須能夠執行日誌輔助 COLLAPSE_RANGE 操作以確保舊結構被完全替換。

  • 在構建輔助樹之後但在範圍摺疊之前崩潰將導致檔案分支中存在無法訪問的塊。 這可能會進一步混淆事情。

  • 修復後收割塊不是一個簡單的操作,並且在日誌恢復期間從重新啟動的範圍摺疊操作啟動收割操作令人望而卻步。

  • 目錄條目塊和配額記錄在每個塊的標頭區域中記錄檔案分支偏移量。 原子範圍摺疊操作必須重寫每個塊標頭的這一部分。 重寫塊標頭中的單個欄位不是一個大問題,但需要注意。

  • 目錄或擴充套件屬性 btree 索引中的每個塊都包含同級塊和子塊指標。 如果原子提交要使用範圍摺疊操作,則必須非常小心地重寫每個塊以保留圖形結構。 作為範圍摺疊的一部分執行此操作意味著重複重寫大量塊,這不利於快速修復。

這導致引入了臨時檔案暫存。

4.5.12.1. 使用臨時檔案

線上修復程式碼應使用 xrep_tempfile_create 函式在檔案系統內建立臨時檔案。 這會分配一個 inode,將 incore inode 標記為私有,並將其附加到 scrub 上下文。 這些檔案對使用者空間隱藏,可能不會新增到目錄樹中,並且必須保持私有。

臨時檔案僅使用兩個 inode 鎖:IOLOCK 和 ILOCK。 此處不需要 MMAPLOCK,因為資料分支塊的使用者空間不得存在頁面錯誤。 這兩個鎖的使用模式與任何其他 XFS 檔案相同——對檔案資料的訪問透過 IOLOCK 控制,對檔案元資料的訪問透過 ILOCK 控制。 提供了鎖定幫助程式,以便 scrub 上下文可以清理臨時檔案及其鎖定狀態。 為了符合inode 鎖定部分中規定的巢狀鎖定策略,建議 scrub 函式使用 xrep_tempfile_ilock*_nowait 鎖定幫助程式。

可以透過兩種方式將資料寫入臨時檔案

  1. xrep_tempfile_copyin 可用於從 xfile 設定常規臨時檔案的內容。

  2. 常規目錄、符號連結和擴充套件屬性函式可用於寫入臨時檔案。

一旦在臨時檔案中構建了資料檔案的良好副本,就必須將其傳送到正在修復的檔案,這是下一節的主題。

提議的補丁位於 修復臨時檔案 系列中。

4.5.13. 記錄的檔案內容交換

一旦修復構建了臨時檔案並將新的資料結構寫入其中,它必須將新的更改提交到現有檔案中。 無法交換兩個檔案的 inumber,因此必須替換新的元資料來代替舊的元資料。 這表明需要能夠交換 extent,但是檔案碎片整理工具 xfs_fsr 使用的現有 extent 交換程式碼不足以進行線上修復,因為

  1. 啟用反向對映 btree 後,交換程式碼必須透過每次對映交換使反向對映資訊保持最新。 因此,它只能在每個事務中交換一個對映,並且每個事務都是獨立的。

  2. 反向對映對於線上 fsck 的操作至關重要,因此舊的碎片整理程式碼(它在單個操作中交換了整個 extent 分支)在此處沒有用處。

  3. 假定碎片整理發生在兩個內容相同的檔案之間。 對於此用例,即使操作中斷,不完整的交換也不會導致使用者可見的檔案內容更改。

  4. 線上修復需要交換定義為相同的兩個檔案的內容。 對於目錄和 xattr 修復,使用者可見的內容可能相同,但是各個塊的內容可能非常不同。

  5. 檔案中的舊塊可能與其他結構交叉連結,並且如果系統在修復過程中關閉,則不得重新出現。

這些問題透過建立一個新的延遲操作和一種新的日誌意圖項來解決,該日誌意圖項用於跟蹤交換兩個檔案範圍的操作的進度。 新的交換操作型別將反向對映 extent 交換程式碼使用的相同事務連結在一起,但是在日誌中記錄了中間進度,以便可以在崩潰後重新啟動操作。 此新功能稱為檔案內容交換 (xfs_exchrange) 程式碼。 底層實現交換檔案分支對映 (xfs_exchmaps)。 新的日誌項記錄了交換的進度,以確保一旦交換開始,它將始終執行到完成,即使有中斷也是如此。 超級塊中的新 XFS_SB_FEAT_INCOMPAT_EXCHRANGE 不相容功能標誌可防止在舊核心上重放這些新的日誌項記錄。

提議的補丁集是 檔案內容交換 系列。

側欄:使用日誌不相容功能標誌

從 XFS v5 開始,超級塊包含一個 sb_features_log_incompat 欄位,用於指示日誌包含某些核心可能無法讀取的記錄,這些核心可以掛載此檔案系統。 簡而言之,日誌不相容功能可以保護日誌內容免受無法理解內容的核心的侵害。 與其他超級塊功能位不同,日誌不相容位是短暫的,因為空(乾淨)日誌不需要保護。 日誌在將其內容提交到檔案系統後自行清理,無論是在解除安裝時還是因為系統處於空閒狀態。 由於上層程式碼可能在清理日誌的同時處理事務,因此上層程式碼需要在使用日誌不相容功能時通知日誌。

日誌透過對每個功能使用一個 struct rw_semaphore 來協調對不相容功能的訪問。 日誌清理程式碼嘗試以獨佔模式獲取此 rwsem 以清除該位; 如果鎖嘗試失敗,則功能位保持設定狀態。 支援日誌不相容功能的程式碼應建立包裝函式以獲取日誌功能並呼叫 xfs_add_incompat_log_feature 以在主超級塊中設定功能位。 超級塊更新是事務性執行的,因此必須在建立使用該功能的事務之前立即呼叫獲取日誌輔助功能的包裝程式。 對於檔案操作,此步驟必須在獲取 IOLOCK 和 MMAPLOCK 之後,但在分配事務之前進行。 事務完成後,呼叫 xlog_drop_incompat_feat 函式以釋放該功能。 在日誌變得乾淨之前,不會從超級塊中清除該功能位。

日誌輔助擴充套件屬性更新和檔案內容交換都使用日誌不相容功能並提供圍繞該功能的便捷包裝程式。

4.5.13.1. 記錄的檔案內容交換的機制

在檔案分支之間交換內容是一項複雜的任務。 目標是在兩個檔案分支偏移範圍之間交換所有檔案分支對映。 每個分支中都可能存在許多 extent 對映,並且對映的邊緣不一定對齊。 此外,在交換之後可能需要進行其他更新,例如交換檔案大小、inode 標誌或將分支資料轉換為本地格式。 這大致是新的延遲交換對映工作項的格式

struct xfs_exchmaps_intent {
    /* Inodes participating in the operation. */
    struct xfs_inode    *xmi_ip1;
    struct xfs_inode    *xmi_ip2;

    /* File offset range information. */
    xfs_fileoff_t       xmi_startoff1;
    xfs_fileoff_t       xmi_startoff2;
    xfs_filblks_t       xmi_blockcount;

    /* Set these file sizes after the operation, unless negative. */
    xfs_fsize_t         xmi_isize1;
    xfs_fsize_t         xmi_isize2;

    /* XFS_EXCHMAPS_* log operation flags */
    uint64_t            xmi_flags;
};

新的日誌意圖項包含足夠的資訊來跟蹤兩個邏輯分支偏移範圍:(inode1, startoff1, blockcount)(inode2, startoff2, blockcount)。 交換操作的每個步驟都會將一個檔案中可能的最大檔案範圍對映交換到另一個檔案。 在交換操作的每個步驟之後,兩個 startoff 欄位都會遞增,並且 blockcount 欄位都會遞減以反映所取得的進度。 flags 欄位捕獲行為引數,例如交換 attr 分支對映而不是資料分支以及交換後要完成的其他工作。 如果檔案資料分支是操作的目標,則兩個 isize 欄位用於在操作結束時交換檔案大小。

當啟動交換時,操作順序如下所示

  1. 為檔案對映交換建立一個延遲工作項。 首先,它應該包含要交換的整個檔案塊範圍。

  2. 呼叫 xfs_defer_finish 以處理交換。 這封裝在用於 scrub 操作的 xrep_tempexch_contents 中。 這會將 extent 交換意圖項記錄到延遲對映交換工作項的事務中。

  3. 直到延遲對映交換工作項的 xmi_blockcount 為零,

    1. 分別讀取從 xmi_startoff1xmi_startoff2 開始的兩個檔案範圍的塊對映,並計算可以在單個步驟中交換的最長 extent。 這是對映中兩個 br_blockcount s 中的最小值。 繼續在檔案分支中前進,直到至少一個對映包含已寫入的塊。 不會交換相互的空洞、未寫入的 extent 和到同一物理空間的 extent 對映。

      對於接下來的幾個步驟,本文件會將來自檔案 1 的對映稱為“map1”,並將來自檔案 2 的對映稱為“map2”。

    2. 建立一個延遲塊對映更新以從檔案 1 中取消對映 map1。

    3. 建立一個延遲塊對映更新以從檔案 2 中取消對映 map2。

    4. 建立一個延遲塊對映更新以將 map1 對映到檔案 2 中。

    5. 建立一個延遲塊對映更新以將 map2 對映到檔案 1 中。

    6. 記錄兩個檔案的塊、配額和 extent 計數更新。

    7. 如有必要,擴充套件任一檔案的 ondisk 大小。

    8. 為在步驟 3 開始時讀取的對映交換意圖日誌項記錄對映交換完成日誌項。

    9. 計算剛剛覆蓋的檔案範圍量。 此數量為 (map1.br_startoff + map1.br_blockcount - xmi_startoff1),因為步驟 3a 可能跳過了空洞。

    10. xmi_startoff1xmi_startoff2 的起始偏移量增加上一步中計算的塊數,並將 xmi_blockcount 減少相同的數量。 這會前進游標。

    11. 記錄一個新的對映交換意圖日誌項,該日誌項反映了工作項的推進狀態。

    12. 將正確的錯誤程式碼 (EAGAIN) 返回給延遲操作管理器,以告知它還有更多工作要做。 操作管理器在返回到步驟 3 的開頭之前完成步驟 3b-3e 中的延遲工作。

  4. 執行任何後處理。 將在後續章節中更詳細地討論這一點。

如果檔案系統在操作過程中關閉,則日誌恢復將找到最新的未完成對映交換日誌意圖項並從那裡重新啟動。 這就是原子檔案對映交換如何保證外部觀察者要麼看到舊的損壞結構,要麼看到新的結構,而永遠不會看到兩者的混合。

4.5.13.2. 檔案內容交換的準備工作

在啟動原子檔案對映交換操作之前,需要處理一些事情。 首先,常規檔案需要先將頁面快取重新整理到磁碟,然後才能開始操作,並且直接 I/O 寫入要靜止。 與任何檔案系統操作一樣,檔案對映交換必須確定可以在操作中代表兩個檔案消耗的最大磁碟空間和配額量,並預留該資源量以避免一旦開始弄髒元資料就發生無法恢復的空間不足故障。 準備步驟會掃描兩個檔案的範圍以估計

  • 處理重複更新到分支對映所需的資料裝置塊。

  • 兩個檔案的資料和即時塊計數的變化。

  • 兩個檔案的配額使用量增加,如果兩個檔案沒有共享同一組配額 ID。

  • 將新增到每個檔案的 extent 對映的數量。

  • 是否存在部分寫入的即時 extent。 使用者程式絕不能訪問對映到即時捲上不同 extent 的即時檔案 extent,如果操作未能執行到完成,則可能會發生這種情況。

精確估計的需求增加了交換操作的執行時間,但是維護正確的帳戶非常重要。 檔案系統絕不能完全耗盡可用空間,並且對映交換永遠不能向分支新增比它可以支援的 extent 對映更多。 常規使用者需要遵守配額限制,儘管元資料修復可能會超過配額以解決其他地方的不一致元資料。

4.5.13.3. 交換元資料檔案內容的特殊功能

擴充套件屬性、符號連結和目錄可以將 fork 格式設定為“local”,並將 fork 視為資料儲存的字面區域。元資料修復必須採取額外的步驟來支援這些情況。

  • 如果兩個 fork 都採用 local 格式,並且 fork 區域足夠大,則交換透過複製記憶體中的 fork 內容、記錄兩個 fork 並提交來執行。由於可以使用單個事務完成此操作,因此不需要原子檔案對映交換機制。

  • 如果兩個 fork 都對映塊,則使用常規的原子檔案對映交換。

  • 否則,只有一個 fork 採用 local 格式。local 格式 fork 的內容將被轉換為塊以執行交換。轉換為塊格式必須在記錄初始對映交換意圖日誌項的同一事務中完成。常規的原子對映交換用於交換元資料檔案對映。在交換操作上設定特殊標誌,以便事務可以再次回滾以將第二個檔案的 fork 轉換回 local 格式,以便第二個檔案在 ILOCK 釋放後即可使用。

擴充套件屬性和目錄將擁有的 inode 蓋印到每個塊中,但緩衝區驗證器實際上並不檢查 inode 編號!雖然沒有驗證,但維護引用完整性仍然很重要,因此在執行對映交換之前,線上修復會使用正在修復的檔案的所有者欄位構建新資料結構中的每個塊。

在成功完成交換操作後,修復操作必須透過處理每個 fork 對映透過標準檔案範圍回收機制來回收舊的 fork 塊,該機制在修復後執行。如果檔案系統在修復的回收部分期間崩潰,則恢復結束時的 iunlink 處理將釋放臨時檔案和未回收的任何塊。但是,此 iunlink 處理省略了線上修復的交叉連結檢測,並且並非完全可靠。

4.5.13.4. 交換臨時檔案內容

要修復元資料檔案,線上修復按如下步驟進行:

  1. 建立一個臨時修復檔案。

  2. 使用暫存資料將新內容寫入臨時修復檔案。必須寫入與正在修復的相同的 fork。

  3. 提交清理事務,因為交換資源估計步驟必須在進行事務預留之前完成。

  4. 呼叫 xrep_tempexch_trans_alloc 以分配具有適當資源預留和鎖的新清理事務,並使用交換操作的詳細資訊填充 struct xfs_exchmaps_req

  5. 呼叫 xrep_tempexch_contents 以交換內容。

  6. 提交事務以完成修復。

4.5.13.4.1. 案例研究:修復即時摘要檔案

在 XFS 檔案系統的“即時”部分中,空閒空間透過點陣圖進行跟蹤,類似於 Unix FFS。點陣圖中的每個位代表一個即時範圍,範圍的大小是檔案系統塊大小的倍數,介於 4KiB 和 1GiB 之間。即時摘要檔案將給定大小的空閒範圍的數量索引到即時空閒空間點陣圖中這些空閒範圍開始的塊的偏移量。換句話說,摘要檔案透過長度幫助分配器查詢空閒範圍,類似於空閒空間計數 (cntbt) btree 對資料部分所做的事情。

摘要檔案本身是一個平面檔案(沒有塊標頭或校驗和!),分為 log2(總 rt 範圍) 部分,其中包含足夠的 32 位計數器來匹配 rt 點陣圖中的塊數。每個計數器記錄從該點陣圖塊開始且可以滿足 2 的冪分配請求的空閒範圍的數量。

要針對點陣圖檢查摘要檔案:

  1. 獲取即時點陣圖和摘要檔案的 ILOCK。

  2. 對於點陣圖中記錄的每個空閒空間範圍:

    1. 計算摘要檔案中包含代表此空閒範圍的計數器的位置。

    2. 從 xfile 中讀取計數器。

    3. 遞增計數器,然後將其寫回 xfile。

  3. 將 xfile 的內容與磁碟上的檔案進行比較。

要修復摘要檔案,請將 xfile 內容寫入臨時檔案,並使用原子對映交換來提交新內容。然後回收臨時檔案。

建議的補丁集是 即時摘要修復系列。

4.5.13.4.2. 案例研究:搶救擴充套件屬性

在 XFS 中,擴充套件屬性被實現為名稱空間的名稱-值儲存。值的大小限制為 64KiB,但名稱的數量沒有限制。屬性 fork 未分割槽,這意味著屬性結構的根始終位於邏輯塊零中,但屬性葉塊、dabtree 索引塊和遠端值塊是混合的。屬性葉塊包含可變大小的記錄,這些記錄將使用者提供的名稱與使用者提供的值相關聯。大於一個塊的值被分配單獨的範圍並寫入那裡。如果葉資訊擴充套件到單個塊之外,則會建立一個目錄/屬性 btree (dabtree) 以將屬性名稱的雜湊對映到條目以進行快速查詢。

搶救擴充套件屬性按如下步驟進行:

  1. 遍歷正在修復的檔案的 attr fork 對映以查詢屬性葉塊。當找到一個時:

    1. 遍歷 attr 葉塊以查詢候選鍵。當找到一個時:

      1. 檢查名稱是否存在問題,如果存在問題則忽略該名稱。

      2. 檢索值。如果成功,則將名稱和值新增到暫存 xfarray 和 xfblob。

  2. 如果 xfarray 和 xfblob 的記憶體使用量超過一定量的記憶體,或者沒有更多 attr fork 塊要檢查,則解鎖該檔案並將暫存的擴充套件屬性新增到臨時檔案。

  3. 使用原子檔案對映交換來交換新的和舊的擴充套件屬性結構。舊的屬性塊現在已附加到臨時檔案。

  4. 回收臨時檔案。

建議的補丁集是 擴充套件屬性修復系列。

4.5.14. 修復目錄

使用當前可用的檔案系統功能修復目錄很困難,因為目錄條目不是冗餘的。離線修復工具會掃描所有 inode 以查詢連結計數不為零的檔案,然後掃描所有目錄以建立這些連結檔案的父子關係。損壞的檔案和目錄將被刪除,沒有父檔案的檔案將被移動到 /lost+found 目錄。它不會嘗試搶救任何東西。

線上修復目前能做的最好的事情是讀取目錄資料塊並搶救任何看起來合理的 dirent,糾正連結計數,並將孤兒移回目錄樹。檔案連結計數 fsck程式碼負責修復連結計數並將孤兒移動到 /lost+found 目錄。

4.5.14.1. 案例研究:搶救目錄

與擴充套件屬性不同,目錄塊的大小都相同,因此搶救目錄很簡單:

  1. 查詢目錄的父目錄。如果點點條目不可讀,請嘗試確認聲稱的父目錄是否具有指向正在修復的目錄的子條目。否則,遍歷檔案系統以找到它。

  2. 遍歷目錄資料 fork 的第一個分割槽以查詢目錄條目資料塊。當找到一個時:

    1. 遍歷目錄資料塊以查詢候選條目。當找到一個條目時:

      1. 檢查名稱是否存在問題,如果存在問題則忽略該名稱。

      2. 檢索 inumber 並獲取 inode。如果成功,則將名稱、inode 編號和檔案型別新增到暫存 xfarray 和 xblob。

  3. 如果 xfarray 和 xfblob 的記憶體使用量超過一定量的記憶體,或者沒有更多目錄資料塊要檢查,則解鎖該目錄並將暫存的 dirent 新增到臨時目錄。截斷暫存檔案。

  4. 使用原子檔案對映交換來交換新的和舊的目錄結構。舊的目錄塊現在已附加到臨時檔案。

  5. 回收臨時檔案。

未來工作問題:在重建目錄時,修復是否應該重新驗證 dentry 快取?

答案:是的,應該。

從理論上講,有必要掃描目錄的所有 dentry 快取條目,以確保以下情況之一適用:

  1. 快取的 dentry 反映了新目錄中的磁碟上 dirent。

  2. 快取的 dentry 在新目錄中不再具有相應的磁碟上 dirent,並且可以從快取中清除該 dentry。

  3. 快取的 dentry 不再具有磁碟上的 dirent,但無法清除該 dentry。這是問題所在。

不幸的是,當前 dentry 快取設計沒有提供遍歷特定目錄的每個子 dentry 的方法,這使得這成為一個難題。目前沒有已知的解決方案。

建議的補丁集是目錄修復系列。

4.5.14.2. 父指標

父指標是一段檔案元資料,使使用者無需從根目錄遍歷目錄樹即可找到檔案的父目錄。如果沒有它們,目錄樹的重建將受到阻礙,就像過去缺乏反向空間對映資訊阻礙了檔案系統空間元資料的重建一樣。但是,父指標功能使完全目錄重建成為可能。

XFS 父指標包含識別父目錄中相應目錄條目所需的資訊。換句話說,子檔案使用擴充套件屬性來儲存指向父目錄的指標,格式為 (dirent_name) (parent_inum, parent_gen)。可以加強目錄檢查過程,以確保每個 dirent 的目標還包含指向 dirent 的父指標。同樣,可以透過確保每個父指標的目標都是目錄並且包含與父指標匹配的 dirent 來檢查每個父指標。線上和離線修復都可以使用此策略。

歷史側邊欄:

目錄父指標最初由 SGI 在十多年前作為 XFS 功能提出。從父目錄到子檔案的每個連結都使用子目錄中的擴充套件屬性進行映象,該屬性可用於識別父目錄。不幸的是,這種早期實現存在重大缺陷,從未合併到 Linux XFS 中。

  1. 2000 年代末的 XFS 程式碼庫沒有強制執行目錄樹中強引用完整性的基礎結構。它不能保證前向連結中的更改始終會透過對反向連結的相應更改來跟進。

  2. 引用完整性未整合到離線修復中。在未掛載的檔案系統上執行檢查和修復,而不使用任何核心或 inode 鎖來協調訪問。目前尚不清楚這實際上是如何正常工作的。

  3. 擴充套件屬性未記錄父目錄中目錄條目的名稱,因此 SGI 父指標實現不能用於重新連線目錄樹。

  4. 擴充套件屬性 fork 僅支援 65,536 個範圍,這意味著父指標屬性的建立很可能在達到最大檔案連結計數之前失敗。

原始父指標設計對於依賴於檔案系統修復之類的事情來說太不穩定了。Allison Henderson、Chandan Babu 和 Catherine Hoang 正在進行第二種實現,該實現解決了第一種實現的所有缺點。在 2022 年期間,Allison 引入了日誌意圖項來跟蹤擴充套件屬性結構的物理操作。這透過使在同一事務中提交 dirent 更新和父指標更新成為可能來解決了引用完整性問題。Chandan 增加了資料和屬性 fork 的最大範圍計數,從而確保擴充套件屬性結構可以增長以處理任何檔案的最大硬連結計數。

對於第二次嘗試,最初提出的磁碟上父指標格式為 (parent_inum, parent_gen, dirent_pos) (dirent_name)。在開發過程中更改了格式,以消除修復工具需要確保在重建目錄時 dirent_pos 欄位始終匹配的要求。

還有一些其他方法可以解決該問題:

  1. 該欄位可以被指定為建議性的,因為其他三個值足以在父目錄中找到該條目。但是,這使得在修復正在進行時無法進行索引鍵查詢。

  2. 我們可以允許在指定的偏移量處建立目錄條目,這解決了引用完整性問題,但存在 dirent 建立將由於與目錄中的空閒空間衝突而失敗的風險。

    可以透過追加目錄條目並修改 xattr 程式碼以支援更新 xattr 鍵並重新索引 dabtree 來解決這些衝突,儘管這必須在父目錄仍然鎖定的情況下執行。

  3. 與上述相同,但原子地刪除舊的父指標條目並新增新的父指標條目。

  4. 將磁碟上 xattr 格式更改為 (parent_inum, name) (parent_gen),這將提供我們所需的 attr 名稱唯一性,而無需強制修復程式碼更新 dirent 位置。不幸的是,這需要更改 xattr 程式碼以支援長達 263 位元組的 attr 名稱。

  5. 將磁碟上 xattr 格式更改為 (parent_inum, hash(name)) (name, parent_gen)。如果雜湊具有足夠的抗衝突性(例如,sha256),那麼這應該提供我們所需的 attr 名稱唯一性。小於 247 位元組的名稱可以直接儲存。

  6. 將磁碟上 xattr 格式更改為 (dirent_name) (parent_ino, parent_gen)。此格式不需要前面建議的任何複雜的巢狀名稱雜湊。但是,發現具有相同檔名的相同 inode 的多個硬連結會導致雜湊 xattr 查找出現效能問題,因此父 inumber 現在已 xor'd 到雜湊索引中。

最後,決定解決方案 #6 是最緊湊和最有效的。為父指標設計了一個新的雜湊函式。

4.5.14.2.1. 案例研究:使用父指標修復目錄

目錄重建使用協調的 inode 掃描目錄條目即時更新掛鉤,如下所示:

  1. 設定一個臨時目錄來生成新的目錄結構,一個 xfblob 來儲存條目名稱,以及一個 xfarray 來儲存目錄更新中涉及的固定大小欄位:(子 inumber, 新增 刪除, 名稱 cookie, ftype)

  2. 設定一個 inode 掃描器並掛接到目錄條目程式碼中以接收目錄操作的更新。

  3. 對於在掃描的每個檔案中找到的每個父指標,確定父指標是否引用感興趣的目錄。如果是:

    1. 分別將父指標名稱和此 dirent 的 addname 條目儲存在 xfblob 和 xfarray 中。

    2. 完成掃描該檔案或核心記憶體消耗超過閾值時,將儲存的更新重新整理到臨時目錄。

  4. 對於透過掛鉤收到的每個即時目錄更新,確定是否已掃描該子目錄。如果是:

    1. 稍後將父指標名稱和此 dirent 更新的 addname 或 removename 條目儲存在 xfblob 和 xfarray 中。我們無法直接寫入臨時目錄,因為不允許掛鉤函式修改檔案系統元資料。相反,我們將更新儲存在 xfarray 中,並依靠掃描器執行緒將儲存的更新應用於臨時目錄。

  5. 掃描完成後,重播 xfarray 中的任何儲存條目。

  6. 掃描完成後,原子地交換臨時目錄和正在修復的目錄的內容。臨時目錄現在包含損壞的目錄結構。

  7. 回收臨時目錄。

建議的補丁集是 父指標目錄修復系列。

4.5.14.2.2. 案例研究:修復父指標

檔案父指標資訊的線上重建與目錄重建類似:

  1. 設定一個臨時檔案來生成新的擴充套件屬性結構,一個 xfblob 來儲存父指標名稱,以及一個 xfarray 來儲存父指標更新中涉及的固定大小欄位:(父 inumber, 生成, 新增 刪除, 名稱 cookie)

  2. 設定一個 inode 掃描器並掛接到目錄條目程式碼中以接收目錄操作的更新。

  3. 對於在掃描的每個目錄中找到的每個目錄條目,確定 dirent 是否引用感興趣的檔案。如果是:

    1. 分別將 dirent 名稱和此父指標的 addpptr 條目儲存在 xfblob 和 xfarray 中。

    2. 完成掃描目錄或核心記憶體消耗超過閾值時,將儲存的更新重新整理到臨時檔案。

  4. 對於透過掛鉤收到的每個即時目錄更新,確定是否已掃描父目錄。如果是:

    1. 稍後將 dirent 名稱和此 dirent 更新的 addpptr 或 removepptr 條目儲存在 xfblob 和 xfarray 中。我們無法直接將父指標寫入臨時檔案,因為不允許掛鉤函式修改檔案系統元資料。相反,我們將更新儲存在 xfarray 中,並依靠掃描器執行緒將儲存的父指標更新應用於臨時檔案。

  5. 掃描完成後,重播 xfarray 中的任何儲存條目。

  6. 將所有非父指標擴充套件屬性複製到臨時檔案。

  7. 掃描完成後,原子地交換臨時檔案和正在修復的檔案屬性 fork 的對映。臨時檔案現在包含損壞的擴充套件屬性結構。

  8. 回收臨時檔案。

建議的補丁集是 父指標修復系列。

4.5.14.2.3. 題外話:父指標的離線檢查

離線修復中檢查父指標的工作方式不同,因為損壞的檔案在執行目錄樹連線檢查之前很久就被擦除了。因此,父指標檢查是新增到現有連線檢查的第二遍。

  1. 在建立倖存檔案集(第 6 階段)之後,遍歷檔案系統中每個 AG 的倖存目錄。這已經作為連線檢查的一部分執行。

  2. 對於找到的每個目錄條目:

    1. 如果該名稱已儲存在 xfblob 中,則使用該 cookie 並跳過下一步。

    2. 否則,將該名稱記錄在 xfblob 中,並記住 xfblob cookie。唯一的對映對於以下方面至關重要:

      1. 重複資料刪除名稱以減少記憶體使用量,以及

      2. 為父指標索引建立一個穩定的排序鍵,以便下面描述的父指標驗證可以工作。

    3. 在每個 AG 的記憶體 Slab 中儲存 (child_ag_inum, parent_inum, parent_gen, name_hash, name_len, name_cookie) 元組。本節中引用的 name_hash 是常規目錄條目名稱雜湊,而不是用於父指標 xattr 的專用雜湊。

  3. 對於檔案系統中的每個 AG:

    1. child_ag_inumparent_inumname_hashname_cookie 的順序對每個 AG 的元組集進行排序。為每個 name 提供單個 name_cookie 對於處理包含同一檔案的多個硬連結(其中所有名稱都雜湊到相同值)的不常見情況至關重要。

    2. 對於 AG 中的每個 inode:

      1. 掃描 inode 中的父指標。對於找到的每個父指標:

        1. 驗證磁碟上的父指標。如果驗證失敗,請繼續處理檔案中的下一個父指標。

        2. 如果該名稱已儲存在 xfblob 中,則使用該 cookie 並跳過下一步。

        3. 在每個檔案的 xfblob 中記錄該名稱,並記住 xfblob cookie。

        4. 在每個檔案的 Slab 中儲存 (parent_inum, parent_gen, name_hash, name_len, name_cookie) 元組。

      2. parent_inumname_hashname_cookie 的順序對每個檔案的元組進行排序。

      3. 將一個 Slab 游標定位在每個 AG 元組 Slab 中 inode 記錄的開頭。由於每個 AG 元組按子 inumber 排序,因此這應該是微不足道的。

      4. 將第二個 Slab 游標定位在每個檔案元組 Slab 的開頭。

      5. 以鎖定步驟迭代兩個游標,比較每個游標下的記錄的 parent_inoname_hashname_cookie 欄位:

        1. 如果每個 AG 游標在鍵空間中的位置低於每個檔案游標,則每個 AG 游標指向缺失的父指標。將父指標新增到 inode 並前進每個 AG 游標。

        2. 如果每個檔案游標在鍵空間中的位置低於每個 AG 游標,則每個檔案游標指向懸空的父指標。從 inode 中刪除父指標並前進每個檔案游標。

        3. 否則,兩個游標都指向相同的父指標。如有必要,更新 parent_gen 元件。前進兩個游標。

  4. 繼續檢查連結計數,就像我們今天所做的那樣。

建議的補丁集是 離線父指標修復系列。

從離線修復中的父指標重建目錄將非常具有挑戰性,因為 xfs_repair 當前在第 3 階段和第 4 階段使用檔案系統的兩次單遍掃描來確定哪些檔案損壞到足以被刪除。必須將此掃描轉換為多遍掃描:

  1. 掃描的第一遍會像現在一樣刪除損壞的 inode、fork 和屬性。損壞的目錄將被記錄,但不會被刪除。

  2. 下一遍記錄指向在第一遍中被記錄為損壞的目錄的父指標。如果第 4 階段也能夠刪除目錄,則此第二遍可能必須在第 4 階段掃描重複塊之後進行。

  3. 第三遍將損壞的目錄重置為空的短格式目錄。尚未確保空閒空間元資料,因此修復還不能使用 libxfs 中的目錄構建程式碼。

  4. 在第 6 階段開始時,空間元資料已被重建。使用在步驟 2 中記錄的父指標資訊重建 dirent 並將其新增到現在為空的目錄中。

尚未構建此程式碼。

4.5.14.2.4. 案例研究:目錄樹結構

如前所述,檔案系統目錄樹應該是一個有向無環圖結構。但是,此圖中的每個節點都是一個單獨的 xfs_inode 物件,具有自己的鎖,這使得驗證樹的質量很困難。幸運的是,非目錄允許具有多個父目錄,並且不能有子目錄,因此只需要掃描目錄。目錄通常構成檔案系統中文件的 5-10%,這大大減少了工作量。

如果可以凍結目錄樹,則可以透過從根目錄向下執行深度(或廣度)優先搜尋併為找到的每個目錄標記點陣圖來輕鬆發現迴圈和斷開連線的區域。在遍歷中的任何時候,嘗試設定已設定的位意味著存在一個迴圈。掃描完成後,將標記的 inode 點陣圖與 inode 分配點陣圖進行異或運算會顯示斷開連線的 inode。但是,線上修復的設計目標之一是避免鎖定整個檔案系統,除非絕對必要。目錄樹更新可以在即時檔案系統上跨掃描器波前移動子樹,因此無法應用點陣圖演算法。

目錄父指標可以對樹結構進行增量驗證。多個執行緒可以從單個子目錄向上移動到根目錄,而不是使用一個執行緒來掃描整個檔案系統。為此,所有目錄條目和父指標必須在內部一致,每個目錄條目必須具有父指標,並且所有目錄的連結計數必須正確。每個掃描器執行緒必須能夠在保持子目錄的 IOLOCK 的同時獲取聲稱的父目錄的 IOLOCK,以防止任一目錄在樹中移動。這是不可能的,因為 VFS 在移動子目錄時不會獲取子目錄的 IOLOCK,因此掃描器會透過獲取 ILOCK 並安裝 dirent 更新掛鉤來檢測更改來穩定父 -> 子關係。

掃描過程使用 dirent 掛鉤來檢測對掃描資料中提到的目錄的更改。掃描工作原理如下:

  1. 對於檔案系統中的每個子目錄:

    1. 對於該子目錄的每個父指標:

      1. 為該父指標建立一個路徑物件,並在路徑物件的點陣圖中標記子目錄 inode 編號。

      2. 在路徑結構中記錄父指標名稱和 inode 編號。

      3. 如果聲稱的父目錄是正在清理的子目錄,則該路徑是一個迴圈。標記要刪除的路徑,然後使用下一個子目錄父指標重複步驟 1a。

      4. 嘗試在路徑物件中的點陣圖中標記聲稱的父 inode 編號。如果已設定該位,則目錄樹中存在一個迴圈。將該路徑標記為一個迴圈,然後使用下一個子目錄父指標重複步驟 1a。

      5. 載入聲稱的父目錄。如果聲稱的父目錄不是連結目錄,則中止掃描,因為父指標資訊不一致。

      6. 對於此聲稱的祖先目錄的每個父指標:

        1. 如果未為該級別設定任何父目錄,則在路徑物件中記錄父指標名稱和 inode 編號。

        2. 如果一個祖先有多個父目錄,則將該路徑標記為損壞。使用下一個子目錄父指標重複步驟 1a。

        3. 對在步驟 1a6a 中標識的祖先重複步驟 1a3-1a6。重複此操作,直到到達目錄樹根目錄或未找到任何父目錄。

      7. 如果遍歷在根目錄處終止,則將該路徑標記為 ok。

      8. 如果遍歷在到達根目錄之前終止,則將該路徑標記為斷開連線。

  2. 如果目錄條目更新鉤子觸發,則檢查掃描已找到的所有路徑。如果條目匹配路徑的一部分,則將該路徑和掃描標記為過時。當掃描器執行緒看到掃描已被標記為過時時,它會刪除所有掃描資料並重新開始。

修復目錄樹的工作方式如下

  1. 遍歷目標子目錄的每個路徑。

    1. 損壞的路徑和迴圈路徑被認為是可疑的。

    2. 已標記為刪除的路徑被認為是壞的。

    3. 到達根目錄的路徑被認為是好的。

  2. 如果子目錄是根目錄或連結計數為零,則刪除直接父目錄中的所有傳入目錄條目。修復完成。

  3. 如果子目錄只有一個路徑,則將 dotdot 條目設定為父目錄並退出。

  4. 如果子目錄至少有一個好的路徑,則刪除直接父目錄中的所有其他傳入目錄條目。

  5. 如果子目錄沒有好的路徑且有多個可疑路徑,則刪除直接父目錄中的所有其他傳入目錄條目。

  6. 如果子目錄沒有路徑,則將其附加到 lost and found 目錄。

提議的補丁位於目錄樹修復系列中。

4.5.15. 孤兒院

檔案系統將檔案表示為有向的,並希望是無環的圖。換句話說,是一棵樹。檔案系統的根目錄是一個目錄,目錄中的每個條目都向下指向更多的子目錄或非目錄檔案。不幸的是,目錄圖指標的中斷會導致斷開連線的圖,這使得無法透過常規路徑解析訪問檔案。

如果沒有父指標,目錄父指標線上清理程式碼可以檢測到指向沒有連結回到子目錄的父目錄的 dotdot 條目,並且檔案連結計數檢查器可以檢測到檔案系統中沒有任何目錄指向的檔案。如果這樣的檔案具有正連結計數,則該檔案是一個孤兒。

使用父指標,可以透過掃描父指標來重建目錄,也可以透過掃描目錄來重建父指標。這應該減少檔案最終出現在 /lost+found 中的情況。

找到孤兒時,應將其重新連線到目錄樹。離線 fsck 透過建立一個目錄 /lost+found 來作為孤兒院來解決這個問題,並使用 inode 號作為名稱將孤兒檔案連結到孤兒院中。將檔案重新指定給孤兒院不會重置其任何許可權或 ACL。

此過程在核心中比在使用者空間中更復雜。目錄和檔案連結計數修復設定函式必須使用常規 VFS 機制來建立具有所有必要安全屬性和 dentry 快取條目的孤兒院目錄,就像常規目錄樹修改一樣。

孤立檔案按以下方式被孤兒院收養

  1. 在清理設定函式的開頭呼叫 xrep_orphanage_try_create,以嘗試確保丟失和找到的目錄實際存在。這還將孤兒院目錄附加到清理上下文中。

  2. 如果決定重新連線檔案,則獲取孤兒院和正在重新附加的檔案的 IOLOCK。xrep_orphanage_iolock_two 函式遵循前面討論的 inode 鎖定策略。

  3. 使用 xrep_adoption_trans_alloc 來為修復事務保留資源。

  4. 呼叫 xrep_orphanage_compute_name 以計算孤兒院中的新名稱。

  5. 如果收養即將發生,請呼叫 xrep_adoption_reparent 以將孤立的檔案重新指定到 lost and found 目錄中,並使 dentry 快取無效。

  6. 呼叫 xrep_adoption_finish 以提交任何檔案系統更新,釋放孤兒院 ILOCK,並清除清理事務。呼叫 xrep_adoption_commit 以提交更新和清理事務。

  7. 如果發生執行時錯誤,請呼叫 xrep_adoption_cancel 以釋放所有資源。

提議的補丁位於 orphanage adoption 系列中。

4.6. 6. 使用者空間演算法和資料結構

本節討論使用者空間程式 xfs_scrub 的關鍵演算法和資料結構,該程式提供在核心中驅動元資料檢查和修復、驗證檔案資料以及查詢其他潛在問題的能力。

4.6.1. 檢查元資料

回顧之前概述的fsck 工作階段。該結構自然地遵循自 1993 年開始設計到檔案系統中的資料依賴關係。在 XFS 中,有幾個元資料依賴項組

  1. 檔案系統摘要計數取決於 inode 索引、分配組空間 btree 和即時卷空間資訊中的一致性。

  2. 配額資源計數取決於配額檔案資料 fork、inode 索引、inode 記錄以及系統上每個檔案的 fork 中的一致性。

  3. 命名層次結構取決於目錄和擴充套件屬性結構中的一致性。這包括檔案連結計數。

  4. 目錄、擴充套件屬性和檔案資料取決於將目錄和擴充套件屬性資料對映到物理儲存介質的檔案 fork 中的一致性。

  5. 檔案 fork 取決於 inode 記錄以及分配組和即時卷的空間元資料索引中的一致性。這包括配額和即時元資料檔案。

  6. Inode 記錄取決於 inode 元資料索引中的一致性。

  7. 即時空間元資料取決於即時元資料 inode 的 inode 記錄和資料 fork。

  8. 分配組元資料索引(可用空間、inode、引用計數和反向對映 btree)取決於 AG 標頭中以及所有 AG 元資料 btree 之間的一致性。

  9. xfs_scrub 取決於檔案系統的掛載和核心對線上 fsck 功能的支援。

因此,元資料依賴圖是安排 xfs_scrub 程式中檢查操作的便捷方法

  • 階段 1 檢查提供的路徑是否對映到 XFS 檔案系統並檢測核心的清理能力,這驗證了組 (i)。

  • 階段 2 使用執行緒化的工作佇列並行清理組 (g) 和 (h)。

  • 階段 3 並行掃描 inode。對於每個 inode,按順序檢查組 (f)、(e) 和 (d)。

  • 階段 4 修復組 (i) 到 (d) 中的所有內容,以便階段 5 和 6 可以可靠地執行。

  • 階段 5 首先並行檢查組 (b) 和 (c),然後再繼續檢查名稱。

  • 階段 6 依賴於組 (i) 到 (b) 來查詢要驗證的檔案資料塊、讀取它們並報告哪些檔案的哪些塊受到影響。

  • 階段 7 檢查組 (a),並已驗證所有其他內容。

請注意,組之間的資料依賴關係由程式流程的結構強制執行。

4.6.2. 並行 Inode 掃描

XFS 檔案系統可以輕鬆地包含數億個 inode。鑑於 XFS 面向具有大型高效能儲存的安裝,因此需要並行清理 inode 以最大程度地縮短執行時間,特別是如果該程式已從命令列手動呼叫。這需要仔細的排程,以使執行緒儘可能均勻地載入。

xfs_scrub inode 掃描程式的早期迭代天真地建立了一個工作佇列,併為每個 AG 安排了一個工作佇列項。每個工作佇列項都遍歷 inode btree(使用 XFS_IOC_INUMBERS)以查詢 inode 塊,然後呼叫 bulkstat(XFS_IOC_BULKSTAT)以收集足夠的資訊來構造檔案控制代碼。然後將檔案控制代碼傳遞給一個函式,以為每個 inode 的每個元資料物件生成清理項。如果檔案系統包含一個具有一些大型稀疏檔案的 AG,而其餘 AG 包含許多較小的檔案,則這種簡單的演算法會導致階段 3 中的執行緒平衡問題。inode 掃描排程函式不夠精細;它應該在單個 inode 級別或 inode btree 記錄級別進行排程,以限制記憶體消耗。

感謝 Dave Chinner,使用者空間中的有界工作佇列使 xfs_scrub 可以透過新增第二個工作佇列輕鬆避免此問題。與以前一樣,第一個工作佇列使用每個 AG 一個工作佇列項進行播種,並使用 INUMBERS 查詢 inode btree 塊。但是,第二個工作佇列配置為可以等待執行的項數的上限。第一個工作佇列的工作人員找到的每個 inode btree 塊都排隊到第二個工作佇列,並且正是第二個工作佇列查詢 BULKSTAT、建立檔案控制代碼並將其傳遞給一個函式,以為每個 inode 的每個元資料物件生成清理項。如果第二個工作佇列太滿,則工作佇列新增函式會阻塞第一個工作佇列的工作人員,直到積壓緩解。這並不能完全解決平衡問題,但足以減少它以進行更緊迫的問題。

提議的補丁集是清理 效能調整inode 掃描重新平衡系列。

4.6.3. 排程修復

在階段 2 中,會立即修復任何 AGI 標頭或 inode btree 中報告的損壞和不一致,因為階段 3 依賴於 inode 索引的正常執行才能找到要掃描的 inode。失敗的修復會重新排程到階段 4。在任何其他空間元資料中報告的問題都將推遲到階段 4。無論其來源如何,最佳化機會始終會推遲到階段 4。

在階段 3 中,如果在階段 2 中驗證了所有空間元資料,則會立即修復檔案的任何元資料部分中報告的損壞和不一致。無法立即修復或無法修復的修復計劃在階段 4 中進行。

xfs_scrub 的原始設計中,人們認為修復非常少見,因此用於與核心通訊的 struct xfs_scrub_metadata 物件也可以用作排程修復的主要物件。隨著給定檔案系統物件的可能最佳化數量最近的增加,使用單個修復項跟蹤給定檔案系統物件的所有符合條件的修復在記憶體方面變得更加有效。每個修復項代表一個可鎖定的物件——AG、元資料檔案、單個 inode 或一類摘要資訊。

階段 4 負責以儘可能快的速度安排大量的修復工作。之前概述的資料依賴關係仍然適用,這意味著 xfs_scrub 必須嘗試完成階段 2 安排的修復工作,然後再嘗試階段 3 安排的修復工作。修復過程如下

  1. 使用工作佇列和足夠的工作人員開始一輪修復,以使 CPU 保持使用者所需的繁忙狀態。

    1. 對於階段 2 排隊的每個修復項,

      1. 要求核心修復給定檔案系統物件的修復項中列出的所有內容。

      2. 記下核心是否在減少此物件所需的修復數量方面取得了任何進展。

      3. 如果該物件不再需要修復,請重新驗證與該物件關聯的所有元資料。如果重新驗證成功,請刪除修復項。否則,重新排隊該項以進行更多修復。

    2. 如果進行了任何修復,請跳回 1a 以重試所有階段 2 項。

    3. 對於階段 3 排隊的每個修復項,

      1. 要求核心修復給定檔案系統物件的修復項中列出的所有內容。

      2. 記下核心是否在減少此物件所需的修復數量方面取得了任何進展。

      3. 如果該物件不再需要修復,請重新驗證與該物件關聯的所有元資料。如果重新驗證成功,請刪除修復項。否則,重新排隊該項以進行更多修復。

    4. 如果進行了任何修復,請跳回 1c 以重試所有階段 3 項。

  2. 如果步驟 1 取得了任何型別的修復進展,請跳回步驟 1 以開始另一輪修復。

  3. 如果還有剩餘的專案需要修復,請再次按順序執行所有專案。如果修復不成功,請抱怨,因為這是修復任何內容的最後機會。

在階段 5 和 7 中遇到的損壞和不一致會立即修復。階段 6 報告的損壞檔案資料塊無法由檔案系統恢復。

提議的補丁集是 修復警告改進修復資料依賴關係物件跟蹤 的重構,以及 修復排程 改進系列。

4.6.4. 檢查名稱中是否存在容易混淆的 Unicode 序列

如果 xfs_scrub 成功在階段 4 結束時驗證檔案系統元資料,則它將繼續進入階段 5,該階段檢查檔案系統中是否存在可疑名稱。這些名稱包括檔案系統標籤、目錄條目中的名稱和擴充套件屬性的名稱。與大多數 Unix 檔案系統一樣,XFS 對名稱的內容施加了最嚴格的約束

  • 目錄條目中不允許使用斜槓和空位元組。

  • 使用者空間可見的擴充套件屬性中不允許使用空位元組。

  • 檔案系統標籤中不允許使用空位元組。

目錄條目和屬性鍵在磁碟上顯式儲存名稱的長度,這意味著空值不是名稱終止符。對於本節,“命名域”是指名稱一起呈現的任何位置——目錄中的所有名稱,或檔案的所有屬性。

儘管 Unix 命名約束非常寬鬆,但大多數現代 Linux 系統的現實是程式使用 Unicode 字元程式碼點來支援國際語言。這些程式通常在使用 C 庫時以 UTF-8 編碼這些程式碼點,因為核心期望以空值終止的名稱。因此,在常見情況下,在 XFS 檔案系統中找到的名稱實際上是 UTF-8 編碼的 Unicode 資料。

為了最大限度地提高其表現力,Unicode 標準為各種字元定義了單獨的控制點,這些字元在世界各地的書寫系統中以相似或相同的方式呈現。例如,字元“西里爾小寫字母 A”U+0430 “а” 通常與“拉丁小寫字母 A”U+0061 “a” 的呈現方式相同。

該標準還允許以多種方式構造字元——透過使用定義的程式碼點,或透過將一個程式碼點與各種組合標記組合在一起。例如,字元“埃符號 U+212B “Å” 也可以表示為“拉丁大寫字母 A”U+0041 “A”,後跟“組合環上”U+030A “◌̊”。這兩個序列的呈現方式相同。

與之前的標準一樣,Unicode 還定義了各種控制字元來改變文字的呈現方式。例如,字元“從右到左覆蓋”U+202E 可以欺騙某些程式將 “moo\xe2\x80\xaegnp.txt” 呈現為 “mootxt.png”。第二類呈現問題涉及空格字元。如果在檔名中遇到字元“零寬度空格”U+200B,則該名稱的呈現方式與沒有零寬度空格的名稱相同。

如果命名域中的兩個名稱具有不同的位元組序列但呈現方式相同,則使用者可能會感到困惑。核心在其對上層編碼方案的漠不關心下,允許這樣做。大多數檔案系統驅動程式會保留 VFS 賦予它們的位元組序列名稱。

檢測容易混淆名稱的技術在 Unicode 安全機制 文件的第 4 節和第 5 節中進行了詳細說明。當 xfs_scrub 檢測到系統上正在使用 UTF-8 編碼時,它會將 Unicode 規範化形式 NFD 與 libicu 的可混淆名稱檢測元件結合使用,以識別目錄中或檔案的擴充套件屬性中可能相互混淆的名稱。還會檢查名稱中是否存在控制字元、非呈現字元和雙向字元的混合。所有這些潛在問題都會在階段 5 中報告給系統管理員。

4.6.5. 檔案資料區段的媒體驗證

系統管理員可以選擇啟動所有檔案資料塊的媒體掃描。在驗證了所有檔案系統元資料(摘要計數器除外)後,此掃描將作為階段 6。掃描首先呼叫 FS_IOC_GETFSMAP 來掃描檔案系統空間對映,以查詢分配給檔案資料 fork 區段的區域。小於 64k 的資料 fork 區段之間的間隙被視為資料 fork 區段,以減少命令設定開銷。當空間對映掃描累積大於 32MB 的區域時,媒體驗證請求將作為原始塊裝置的 directio 讀取傳送到磁碟。

如果驗證讀取失敗,xfs_scrub 會使用單塊讀取重試,以將故障縮小到媒體的特定區域並記錄。當它完成釋出驗證請求時,它再次使用空間對映 ioctl 將記錄的媒體錯誤映射回元資料結構,並報告丟失的內容。對於檔案中擁有的塊中的媒體錯誤,可以使用父指標從 inode 號構造檔案路徑,以便進行使用者友好的報告。

4.7. 7. 結論和未來工作

希望本文件的讀者已經遵循了本文件中提出的設計,並且現在對 XFS 如何線上重建其元資料索引以及檔案系統使用者如何與該功能互動有一定的瞭解。儘管這項工作的範圍令人望而卻步,但希望本指南能讓程式碼讀者更容易理解已構建的內容、為誰構建以及為什麼構建。請隨時透過 XFS 郵件列表提出問題。

4.7.1. XFS_IOC_EXCHANGE_RANGE

如前所述,原子檔案對映交換機制的第二個前端是一個新的 ioctl 呼叫,使用者空間程式可以使用該呼叫原子地提交對檔案的更新。這個前端已經發布供審查多年了,儘管對線上修復的必要改進以及缺乏客戶需求意味著該提案沒有被大力推動。

4.7.1.1. 與常規使用者檔案的檔案內容交換

如前所述,XFS 長期以來都能夠交換檔案之間的區段,xfs_fsr 幾乎專門使用它來整理檔案。最早的形式是 fork 交換機制,透過交換每個 inode fork 的直接區域中的原始位元組,可以在兩個檔案之間交換資料 fork 的全部內容。當 XFS v5 附帶自描述元資料時,這種舊機制增加了一些日誌支援,以便在日誌恢復期間繼續重寫 BMBT 塊的所有者欄位。當反向對映 btree 稍後新增到 XFS 時,維護 fork 對映與反向對映索引的一致性的唯一方法是開發一種迭代機制,該機制使用延遲 bmap 和 rmap 操作一次交換一個對映。該機制與上述步驟 2-3 相同,除了新的跟蹤項,因為原子檔案對映交換機制是現有機制的迭代,而不是完全新穎的東西。對於檔案整理的狹窄情況,檔案內容必須相同,因此恢復保證並沒有多大收穫。

原子檔案內容交換比現有的 swapext 實現更靈活,因為它可以保證即使在崩潰後,呼叫者也永遠不會看到新舊內容的混合,並且它可以對兩個任意的檔案 fork 範圍進行操作。額外的靈活性實現了幾個新的用例

  • 原子提交檔案寫入:使用者空間程序開啟它想要更新的檔案。接下來,它開啟一個臨時檔案並呼叫檔案克隆操作,將第一個檔案的內容重新連結到臨時檔案中。應該將對原始檔案的寫入寫入到臨時檔案中。最後,該程序呼叫原子檔案對映交換系統呼叫(XFS_IOC_EXCHANGE_RANGE)來交換檔案內容,從而將所有更新提交到原始檔案,或者不提交任何更新。

  • 事務性檔案更新:與上述機制相同,但只有在原始檔案的內容沒有更改時,呼叫者才希望發生提交。要實現這一點,呼叫程序會在將其資料重新連結到臨時檔案之前,快照原始檔案的檔案修改和更改時間戳。當程式準備好提交更改時,它會將時間戳作為原子檔案對映交換系統呼叫的引數傳遞到核心中。只有在提供的時間戳與原始檔案匹配時,核心才會提交更改。提供了一個新的 ioctl(XFS_IOC_COMMIT_RANGE)來執行此操作。

  • 模擬原子塊裝置寫入:匯出具有與檔案系統塊大小匹配的邏輯扇區大小的塊裝置,以強制所有寫入都與檔案系統塊大小對齊。將所有寫入暫存到臨時檔案,並在完成後,呼叫原子檔案對映交換系統呼叫,並使用一個標誌來指示應忽略臨時檔案中的空洞。這在軟體中模擬了原子裝置寫入,並且可以支援任意分散的寫入。

4.7.2. 向量化清理

事實證明,前面提到的重構修復項是啟用向量化清理系統呼叫的催化劑。自 2018 年以來,在某些系統上,呼叫核心的成本顯著增加,以減輕推測執行攻擊的影響。這激勵程式作者儘可能少地進行系統呼叫,以減少執行路徑跨越安全邊界的次數。

使用向量化清理,使用者空間將檔案系統物件的標識、要針對該物件執行的清理型別列表以及所選清理型別之間的資料依賴關係的簡單表示形式推送到核心。核心會執行呼叫者的計劃,直到它遇到由於損壞而無法滿足的依賴關係,並告訴使用者空間完成了多少。希望 io_uring 將拾取足夠多的此功能,以便線上 fsck 可以使用它,而不是向 XFS 新增單獨的向量化清理系統呼叫。

相關的補丁集是 核心向量化清理使用者空間向量化清理 系列。

4.7.3. 清理的服務質量目標

線上 fsck 程式碼的一個嚴重缺點是它可以在核心中保持資源鎖的時間基本上是無限的。允許使用者空間向該程序傳送一個致命訊號,該訊號將導致 xfs_scrub 在它到達一個良好的停止點時退出,但是使用者空間無法向核心提供時間預算。鑑於清理程式碼庫具有檢測致命訊號的幫助程式,因此允許使用者空間為清理/修復操作指定超時並在超過預算時中止該操作不應該花費太多精力。但是,大多數修復函式都具有一個屬性,即一旦它們開始觸及磁碟上的元資料,該操作就無法乾淨地取消,此後 QoS 超時不再有用。

4.7.4. 整理可用空間

多年來,許多 XFS 使用者都要求建立一個程式來清除檔案系統下的物理儲存的一部分,以便它成為一個連續的可用空間塊。為簡單起見,將此可用空間整理程式稱為 clearspace

clearspace 程式需要的第一個部分是從使用者空間讀取反向對映索引的能力。這已經以 FS_IOC_GETFSMAP ioctl 的形式存在。它需要的第二個部分是一種新的 fallocate 模式(FALLOC_FL_MAP_FREE_SPACE),用於分配區域中的可用空間並將其對映到檔案。將此檔案稱為“空間收集器”檔案。第三個部分是強制線上修復的能力。

要清除物理儲存的一部分中的所有元資料,clearspace 使用新的 fallocate map-freespace 呼叫將該區域中的任何可用空間對映到空間收集器檔案。接下來,clearspace 透過 GETFSMAP 查詢該區域中的所有元資料塊,並對資料結構發出強制修復請求。這通常會導致元資料在未被清除的其他地方重建。每次重定位後,clearspace 都會再次呼叫 “map free space” 函式,以收集區域中任何新釋放的空間。

要清除物理儲存的一部分中的所有檔案資料,clearspace 使用 FSMAP 資訊來查詢相關的檔案資料塊。確定了一個好的目標後,它會對檔案的那部分使用 FICLONERANGE 呼叫,以嘗試與虛擬檔案共享物理空間。克隆區段意味著原始所有者無法覆蓋內容;任何更改都將透過寫時複製寫入其他位置。Clearspace 在未被清除的區域中建立自己的凍結區段副本,並使用 FIEDEUPRANGE(或 原子檔案內容交換 功能)來更改目標檔案的資料區段對映,使其遠離被清除的區域。當所有其他對映都已移動後,clearspace 會將空間重新連結到空間收集器檔案中,以便它變得不可用。

還有一些可以應用於上述演算法的進一步最佳化。要清除具有高共享因子的物理儲存片段,強烈希望保留此共享因子。實際上,應首先移動這些區段,以在操作完成後最大限度地提高共享因子。為了使此操作順利進行,clearspace 需要一個新的 ioctl(FS_IOC_GETREFCOUNTS)以將引用計數資訊報告給使用者空間。透過公開的 refcount 資訊,clearspace 可以快速找到檔案系統中最長、共享最多的資料區段,並首先定位它們。

未來工作問題:檔案系統如何移動 inode 塊?

答案:要移動 inode 塊,Dave Chinner 構造了一個原型程式,該程式建立一個具有舊內容的新檔案,然後以無鎖方式繞檔案系統執行以更新目錄條目。如果檔案系統崩潰,則操作無法完成。這個問題並非完全無法克服:建立一個隱藏在跳轉標籤後面的 inode 重新對映表,以及一個跟蹤核心遍歷檔案系統以更新目錄條目的日誌項。問題是,核心無法對開啟的檔案做任何事情,因為它無法撤銷它們。

未來工作問題: 是否可以使用靜態金鑰來最小化在XFS檔案上支援 revoke() 的成本?

答案: 是的。在第一次撤銷之前,退出程式碼根本不需要在呼叫路徑中。

相關的補丁集是 kernel freespace defraguserspace freespace defrag 系列。

4.7.5. 縮小檔案系統

移除檔案系統的末尾應該很簡單,只需疏散檔案系統末尾的資料和元資料,並將釋放的空間交給縮小程式碼。 這需要疏散檔案系統末尾的空間,這是對可用空間碎片整理的使用!