本文件根據 GNU 通用公共許可證 v2 的條款獲得許可。主要作者是 Darrick J. Wong。
本設計文件分為七個部分。第 1 部分定義了 fsck 工具是什麼,以及編寫新工具的動機。第 2 部分和第 3 部分概述了線上 fsck 程序的工作原理以及如何對其進行測試以確保功能正確。第 4 部分討論了使用者介面以及新程式的預期使用模式。第 5 部分和第 6 部分展示了高階元件以及它們如何組合在一起,然後提出了每個修復功能實際工作方式的案例研究。第 7 部分總結了到目前為止所討論的內容,並推測了可以在線上 fsck 之上構建的其他內容。
Unix 檔案系統有四個主要職責:
直接支援這些功能的元資料(例如,檔案、目錄、空間對映)有時稱為主要元資料。輔助元資料(例如,反向對映和目錄父指標)支援檔案系統內部的操作,例如內部一致性檢查和重組。顧名思義,摘要元資料壓縮主要元資料中包含的資訊,以提高效能。
檔案系統檢查 (fsck) 工具檢查檔案系統中的所有元資料,以查詢錯誤。除了查詢明顯的元資料損壞之外,fsck 還會將不同型別的元資料記錄相互交叉引用,以查詢不一致之處。人們不喜歡丟失資料,因此大多數 fsck 工具還包含一些糾正發現的任何問題的能力。作為一個警告 - 大多數 Linux fsck 工具的主要目標是將檔案系統元資料恢復到一致狀態,而不是最大化恢復的資料。這個先例不會在這裡受到挑戰。
20 世紀的檔案系統通常在磁碟格式中缺少任何冗餘,這意味著 fsck 只能透過擦除檔案直到不再檢測到錯誤來響應錯誤。最近的檔案系統設計在其元資料中包含足夠的冗餘,現在可以在發生非災難性錯誤時重新生成資料結構;這種能力有助於兩種策略。
注意: |
系統管理員透過建立備份來增加獨立儲存系統的數量來避免資料丟失;他們透過建立 RAID 陣列來增加每個儲存系統的冗餘來避免停機。fsck 工具僅解決第一個問題。 |
程式碼釋出到 kernel.org git 樹,如下所示:核心更改、使用者空間更改和 QA 測試更改。每個新增線上修復功能的核心補丁集將在核心、xfsprogs 和 fstests git 儲存庫中使用相同的分支名稱。
當前的 XFS 工具留下了一些未解決的問題:
當由於元資料中的靜默損壞而發生意外關閉時,**使用者程式** 會突然 **失去對檔案系統的訪問許可權**。這些發生是 **不可預測的**,通常沒有警告。
在 **意外關閉** 發生後的恢復期間,**使用者** 會遇到 **完全的服務中斷**。
如果檔案系統離線以 **主動查詢問題**,**使用者** 會遇到 **完全的服務中斷**。
**資料所有者** 無法 **檢查** 其儲存資料的 **完整性**,而無需讀取所有資料。當儲存系統管理員執行的線性媒體掃描可能就足夠時,這可能會使他們承擔大量的計費成本。
如果 **缺乏** 在檔案系統線上時評估檔案系統健康狀況的 **手段**,**系統管理員** 無法 **計劃** 一個維護視窗來處理損壞。
當這樣做需要 **手動干預** 和停機時,**艦隊監控工具** 無法 **自動化定期檢查** 檔案系統健康狀況。
當惡意行為者 **利用 Unicode 的怪癖** 在目錄中放置誤導性名稱時,**使用者** 可能會被欺騙 **做他們不希望做的事情**。
鑑於要解決的問題的定義以及將受益的行為者,建議的解決方案是第三個 fsck 工具,它作用於正在執行的檔案系統。
這個新的第三個程式有三個元件:一個用於檢查元資料的核心設施,一個用於修復元資料的核心設施,以及一個用於驅動活動檔案系統上的 fsck 活動的使用者空間驅動程式。xfs_scrub 是驅動程式程式的名稱。本文件的其餘部分介紹了新 fsck 工具的目標和用例,描述了與其目標相關的其主要設計要點,並討論了與現有工具的異同。
注意: |
在本文件中,現有的離線 fsck 工具也可以用其當前名稱“xfs_repair”來稱呼。用於新的線上 fsck 工具的使用者空間驅動程式可以被稱為“xfs_scrub”。線上 fsck 的核心部分,用於驗證元資料,稱為“線上清理”,而核心中用於修復元資料的部分稱為“線上修復”。 |
命名層次結構被分解為稱為目錄和檔案的物件,物理空間被分成稱為分配組的塊。分片使高度並行系統能夠獲得更好的效能,並有助於控制發生損壞時的損失。將檔案系統劃分為主要物件(分配組和 inode)意味著有很多機會可以對檔案系統的子集執行定向檢查和修復。
當這種情況發生時,其他部分會繼續處理 IO 請求。即使只能透過掃描整個系統來重新生成一條檔案系統元資料,也可以在後臺完成掃描,而其他檔案操作繼續進行。
總之,線上 fsck 利用資源分片和冗餘元資料來啟用在系統執行時對目標進行檢查和修復操作。此功能將與自動系統管理相結合,以便 XFS 的自主自愈能夠最大限度地提高服務可用性。
如前所述,fsck 工具具有三個主要目標
檢測元資料中的不一致;
消除這些不一致;和
儘量減少進一步的資料丟失。
證明正確操作對於建立使用者對軟體在預期範圍內執行的信心是必要的。不幸的是,直到引入具有高 IOPS 儲存的低成本虛擬機器之前,對 fsck 工具的每個方面執行常規的詳盡測試實際上是不可行的。考慮到充足的硬體可用性,線上 fsck 專案的測試策略涉及針對現有 fsck 工具進行差異分析,以及對每種型別的元資料物件的每個屬性進行系統測試。測試可以分為四個主要類別,如下所述。
任何免費軟體 QA 工作的首要目標是使測試儘可能便宜和廣泛,以最大限度地提高社群的規模優勢。換句話說,測試應最大限度地擴大檔案系統配置場景和硬體設定的範圍。這透過使線上 fsck 的作者能夠儘早發現和修復錯誤來提高程式碼質量,並幫助新功能的開發人員在開發工作中儘早發現整合問題。
Linux 檔案系統社群共享一個通用的 QA 測試套件,fstests,用於功能和迴歸測試。甚至在開始線上 fsck 的開發工作之前,fstests(在 XFS 上執行時)會在每次測試之間對測試和臨時檔案系統執行 xfs_check 和 xfs_repair -n 命令。這提供了一定程度的保證,即核心和 fsck 工具在什麼構成一致的元資料方面保持一致。在線上檢查程式碼的開發過程中,fstests 被修改為在每次測試之間執行 xfs_scrub -n,以確保新的檢查程式碼產生與兩個現有 fsck 工具相同的結果。
要開始線上修復的開發,fstests 被修改為執行 xfs_repair 以在測試之間重建檔案系統的元資料索引。這確保了離線修復不會崩潰,在退出後不會留下損壞的檔案系統,也不會觸發來自線上檢查的投訴。這也建立了可以和不能離線修復的基線。要完成線上修復的第一個開發階段,fstests 被修改為能夠以“強制重建”模式執行 xfs_scrub。這使得可以比較線上修復與現有離線修復工具的有效性。
線上 fsck 的一個獨特要求是能夠在與常規工作負載併發的檔案系統上執行。儘管當然不可能在執行系統上以 零 可觀察到的影響執行 xfs_scrub,但線上修復程式碼永遠不應將不一致引入檔案系統元資料,並且常規工作負載永遠不應注意到資源匱乏。為了驗證是否滿足這些條件,fstests 已透過以下方式得到增強
對於每個清理專案型別,建立一個測試以在執行 fsstress 時執行檢查該專案型別。
對於每個清理專案型別,建立一個測試以在執行 fsstress 時執行修復該專案型別。
競爭 fsstress 和 xfs_scrub -n 以確保檢查整個檔案系統不會導致問題。
競爭 fsstress 和 xfs_scrub 以強制重建模式確保強制修復整個檔案系統不會導致問題。
在凍結和解凍檔案系統時,競爭以檢查和強制修復模式的 xfs_scrub 與 fsstress。
在以只讀和讀寫方式重新掛載檔案系統時,競爭以檢查和強制修復模式的 xfs_scrub 與 fsstress。
相同,但執行 fsx 而不是 fsstress。(尚未完成?)
成功定義為能夠在不觀察到由於損壞的元資料、核心掛起檢查警告或任何其他型別的惡作劇而導致的任何意外的檔案系統關閉的情況下執行所有這些測試。
提議的補丁集包括 常規壓力測試 和 現有每個功能的壓力測試的演變。
與離線修復一樣,線上 fsck 的主要使用者是系統管理員。線上 fsck 向管理員提供兩種操作模式:按需線上 fsck 的前臺 CLI 程序,以及執行自主檢查和修復的後臺服務。
對於想要了解檔案系統中元資料的最新資訊的管理員,可以在命令列上作為前臺程序執行 xfs_scrub。該程式檢查檔案系統中的每一段元資料,同時管理員等待報告結果,就像現有的 xfs_repair 工具一樣。兩種工具都共享一個 -n 選項來執行只讀掃描,以及一個 -v 選項來增加報告的資訊的詳細程度。
xfs_scrub 的一個新功能是 -x 選項,它利用硬體的糾錯功能來檢查資料檔案內容。預設情況下不啟用介質掃描,因為它可能會大大增加程式執行時並消耗舊儲存硬體上的大量頻寬。
前臺呼叫的輸出捕獲在系統日誌中。
xfs_scrub_all 程式遍歷已掛載檔案系統的列表,並並行啟動每個檔案系統的 xfs_scrub。它序列化任何解析為同一頂級核心塊裝置的檔案系統的掃描,以防止過度消耗資源。
為了減少系統管理員的工作量,xfs_scrub 軟體包提供了一套 systemd 定時器和服務,預設情況下在週末自動執行線上 fsck。後臺服務配置為以儘可能少的許可權、最低的 CPU 和 IO 優先順序以及受 CPU 限制的單執行緒模式執行清理。系統管理員可以隨時對其進行調整,以適應客戶工作負載的延遲和吞吐量要求。
後臺服務的輸出也捕獲在系統日誌中。如果需要,可以透過在以下服務檔案中設定 EMAIL_ADDR 環境變數來自動透過電子郵件傳送故障報告(由於不一致或僅僅是執行時錯誤)
啟用後臺掃描的決定留給系統管理員。這可以透過啟用以下任何服務來完成
這種自動每週掃描配置為每月對所有檔案資料執行一次額外的介質掃描。這不如儲存檔案資料塊校驗和那麼可靠,但如果應用程式軟體提供自己的完整性檢查,則效能要好得多,冗餘可以在檔案系統上方的其他地方提供,或者儲存裝置的完整性保證被認為是足夠的。
systemd 單元檔案定義已經過安全稽核(截至 systemd 249),以確保 xfs_scrub 程序儘可能少地訪問系統的其餘部分。這是透過 systemd-analyze security 執行的,之後許可權被限制為所需的最小值,沙盒被設定為儘可能的最大程度,並進行沙盒和系統呼叫過濾;並且對檔案系統樹的訪問被限制為啟動程式和訪問正在掃描的檔案系統所需的最小值。服務定義檔案將 CPU 使用率限制為 80% 的一個 CPU 核心,並儘可能對 IO 和 CPU 排程應用不錯的優先順序。採取此措施是為了最大限度地減少檔案系統其餘部分的延遲。沒有為 cron 作業執行此類強化。
提議的補丁集:啟用 xfs_scrub 後臺服務。
XFS 在記憶體中快取每個檔案系統健康狀況的摘要。每當執行 xfs_scrub 時,或者在常規操作期間檢測到檔案系統元資料中的不一致時,都會更新資訊。系統管理員應使用 xfs_spaceman 的 health 命令將此資訊下載到人類可讀的格式中。如果觀察到問題,管理員可以安排一個縮短的服務視窗來執行線上修復工具以糾正問題。否則,管理員可以決定安排一個維護視窗來執行傳統的離線修復工具以糾正問題。
未來工作問題:健康狀況報告是否應與新的 inotify fs 錯誤通知系統整合?對於系統管理員來說,擁有一個守護程式來偵聽損壞通知並啟動修復是否有幫助?
答案:這些問題仍未得到解答,但應成為與 XFS 的早期採用者和潛在的下游使用者對話的一部分。
提議的補丁集包括 將健康狀況報告連線到更正返回 和 在記憶體回收期間保留疾病資訊。
本節討論核心程式碼的關鍵演算法和資料結構,這些程式碼提供在系統執行時檢查和修復元資料的能力。本節的前幾章揭示了為檢查元資料提供基礎的部分。本節的其餘部分介紹了 XFS 自我再生的機制。
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 範圍?還是未寫入的資料範圍?
線上檔案系統檢查透過將其資訊與所有其他空間索引進行比較來判斷每個主要元資料記錄的一致性。反向對映索引在一致性檢查過程中起著關鍵作用,因為它包含所有空間分配資訊的集中式備用副本。程式執行時和資源獲取的容易程度是限制線上檢查可以查閱的唯一真正限制。例如,可以根據以下內容檢查檔案資料範圍對映
關於反向對映索引,有幾點需要注意:
如果對上述任何主要元資料有疑問,反向對映可以提供正確的肯定確認。大多數主要元資料的檢查程式碼都遵循與上述類似的路徑。
證明次要元資料與主要元資料的一致性是困難的,因為這需要全面掃描所有主要空間元資料,這非常耗時。例如,檢查檔案範圍對映 btree 塊的反向對映記錄需要鎖定檔案並搜尋整個 btree 以確認該塊。相反,scrub 依賴於在主要空間對映結構檢查期間進行嚴格的交叉引用。
如果所需的鎖定順序與常規檔案系統操作使用的順序不同,則一致性掃描必須使用非阻塞鎖獲取原語。例如,如果檔案系統通常在獲取 AGF 緩衝區鎖之前獲取檔案 ILOCK,但 scrub 想要在持有 AGF 緩衝區鎖的同時獲取檔案 ILOCK,則 scrub 不能阻塞第二次獲取。這意味著,如果系統負載很重,則無法保證反向對映資料掃描的這一部分能夠向前推進。
總之,反向對映在主要元資料的重建中起著關鍵作用。這些記錄如何暫存、寫入磁碟並提交到檔案系統的詳細資訊將在後續章節中介紹。
檢查元資料結構的第一步是檢查結構中包含的每個記錄及其與系統其餘部分的關係。XFS 包含多個檢查層,以防止不一致的元資料對系統造成嚴重破壞。每一層都提供資訊,幫助核心對元資料結構的健康狀況做出三個決定:
此結構的某一部分是否明顯損壞 (XFS_SCRUB_OFLAG_CORRUPT)?
此結構是否與系統的其餘部分不一致 (XFS_SCRUB_OFLAG_XCORRUPT)?
檔案系統周圍是否存在太多損壞,以至於無法進行交叉引用 (XFS_SCRUB_OFLAG_XFAIL)?
是否可以最佳化結構以提高效能或減小元資料的大小 (XFS_SCRUB_OFLAG_PREEN)?
結構是否包含不不一致但值得系統管理員審查的資料 (XFS_SCRUB_OFLAG_WARNING)?
以下各節介紹元資料清理過程的工作原理。
在緩衝區快取之後,下一級別的元資料保護是內置於檔案系統中的內部記錄驗證程式碼。這些檢查在緩衝區驗證器、緩衝區快取的內建檔案系統使用者和 scrub 程式碼本身之間進行拆分,具體取決於所需的高階上下文的數量。檢查的範圍仍然在塊內部。這些更高級別的檢查函式回答以下問題:
此類別的記錄檢查更加嚴格且耗時更多。例如,檢查塊指標和 inumber,以確保它們指向分配組的動態分配部分和檔案系統內。檢查名稱是否存在無效字元,並檢查標誌是否存在無效組合。還會檢查其他記錄屬性是否存在合理的值。檢查跨越 btree 鍵空間間隔的 Btree 記錄是否具有正確的順序和缺少可合併性(檔案 fork 對映除外)。出於效能原因,除非啟用除錯或即將發生寫入,否則常規程式碼可能會跳過其中一些檢查。當然,Scrub 函式必須檢查所有可能的問題。
各種檔案系統元資料由使用者空間直接控制。由於這種性質,驗證工作不能比檢查值是否在可能的範圍內更精確。這些欄位包括:
擴充套件屬性實現了一個鍵值儲存,使資料片段可以附加到任何檔案。核心和使用者空間都可以訪問鍵和值,但受到名稱空間和許可權限制。最典型的是,這些片段是有關檔案的元資料——來源、安全上下文、使用者提供的標籤、索引資訊等。
名稱最長可達 255 位元組,並且可以存在於多個不同的名稱空間中。值的大小可達 64KB。檔案的擴充套件屬性儲存在 attr fork 對映的塊中。對映指向葉子塊、遠端值塊或 dabtree 塊。屬性 fork 中的塊 0 始終是結構的頂部,但否則可以在 attr fork 中的任何偏移處找到這三種類型的塊中的每一種。葉子塊包含指向名稱和值的屬性鍵記錄。名稱始終儲存在同一葉子塊中的其他位置。小於檔案系統塊大小的 3/4 的值也儲存在同一葉子塊中的其他位置。遠端值塊包含太大而無法放入葉子中的值。如果葉子資訊超過單個檔案系統塊,則會建立一個 dabtree(也以塊 0 為根)以將屬性名稱的雜湊值對映到 attr fork 中的葉子塊。
由於 attr 塊和索引塊之間缺乏分離,因此檢查擴充套件屬性結構並不那麼簡單。Scrub 必須讀取 attr fork 對映的每個塊並忽略非葉子塊。
遍歷 attr fork 中的 dabtree(如果存在),以確保塊或不指向 attr 葉子塊的 dabtree 對映中沒有不規則之處。
遍歷 attr fork 的塊以查詢葉子塊。對於葉子中的每個條目:
驗證名稱是否不包含無效字元。
讀取 attr 值。這將執行 attr 名稱的命名查詢,以確保 dabtree 的正確性。如果該值儲存在遠端塊中,這也會驗證遠端值塊的完整性。
檔案系統目錄樹是一個有向無環圖結構,其中檔案構成節點,目錄條目 (dirent) 構成邊。目錄是一種特殊型別的檔案,包含從 255 位元組序列(名稱)到 inumber 的一組對映。這些稱為目錄條目,或簡稱為 dirent。每個目錄檔案必須只有一個指向該檔案的目錄。根目錄指向自身。目錄條目指向任何型別的檔案。每個非目錄檔案可能具有指向它的多個目錄。
在 XFS 中,目錄實現為一個檔案,包含多達三個 32GB 分割槽。第一個分割槽包含目錄條目資料塊。每個資料塊都包含可變大小的記錄,這些記錄將使用者提供的名稱與 inumber 和(可選)檔案型別相關聯。如果目錄條目資料增長超過一個塊,則第二個分割槽(以 EOF 後範圍的形式存在)將填充一個包含空閒空間資訊和一個索引的塊,該索引將 dirent 名稱的雜湊對映到第一個分割槽中的目錄資料塊。這使得目錄名稱查詢非常快。如果第二個分割槽增長超過一個塊,則第三個分割槽將填充一個空閒空間資訊的線性陣列,以便更快地擴充套件。如果空閒空間已分離並且第二個分割槽再次增長超過一個塊,則使用 dabtree 將 dirent 名稱的雜湊對映到目錄資料塊。
檢查目錄非常簡單:
遍歷第二個分割槽中的 dabtree(如果存在),以確保塊或不指向 dirent 塊的 dabtree 對映中沒有不規則之處。
遍歷第一個分割槽中的塊以查詢目錄條目。按如下方式檢查每個 dirent:
名稱是否不包含無效字元?
inumber 是否對應於實際分配的 inode?
子 inode 是否具有非零連結計數?
如果 dirent 中包含檔案型別,它是否與 inode 的型別匹配?
如果子項是一個子目錄,子項的 dotdot 指標是否指向父項?
如果目錄具有第二個分割槽,請執行 dirent 名稱的命名查詢以確保 dabtree 的正確性。
遍歷第三個分割槽中的空閒空間列表(如果存在),以確保它描述的空閒空間確實未使用。
涉及 父級 和 檔案連結計數 的檢查操作將在後面的章節中進行更詳細的討論。
如前幾節所述,目錄/屬性 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 塊的內容?
XFS 維護三類彙總計數器:可用資源、配額資源使用情況和檔案連結計數。
從理論上講,可以透過遍歷整個檔案系統來找到可用資源量(資料塊、inode、即時範圍)。這將使報告速度非常慢,因此事務性檔案系統可以在超級塊中維護此資訊的摘要。將這些值與檔案系統元資料交叉引用應該是遍歷每個 AG 中的空閒空間和 inode 元資料以及即時點陣圖的簡單問題,但存在複雜性,將在 稍後更詳細地討論。
配額使用情況 和 檔案連結計數 檢查非常複雜,值得單獨章節介紹。
執行修復後,檢查程式碼將再次執行以驗證新結構,並且健康評估的結果將在內部記錄並返回到呼叫程序。此步驟對於使系統管理員能夠監視檔案系統的狀態和任何修復的進度至關重要。對於開發人員,這是一種有用的手段,可以判斷線上和離線檢查工具中的錯誤檢測和糾正的有效性。
複雜操作可以使用事務鏈對多個按 AG 資料結構進行修改。一旦這些鏈提交到日誌,如果在處理鏈時系統崩潰,則會在日誌恢復期間重新啟動這些鏈。由於 AG 標頭緩衝區在鏈中的事務之間未鎖定,因此線上檢查必須與正在進行的鏈式操作協調,以避免由於掛起的鏈而錯誤地檢測到不一致。此外,線上修復不得在操作掛起時執行,因為元資料彼此暫時不一致,並且無法重建。
只有在線 fsck 具有對 AG 元資料的完全一致性的要求,與檔案系統更改操作相比,這應該相對罕見。線上 fsck 與事務鏈的協調方式如下:
這可能會導致線上 fsck 花費很長時間才能完成,但常規檔案系統更新優先於後臺檢查活動。有關發現這種情況的詳細資訊將在下一節中介紹,有關解決方案的詳細資訊將在之後介紹。
在線上清理開發的中途,fsstress 測試發現線上 fsck 和其他編寫器執行緒建立的複合事務鏈之間存在錯誤的互動,從而導致了對元資料不一致性的錯誤報告。這些報告的根本原因是引入反向對映和 reflink 時,延遲工作項和複合事務鏈的擴充套件所引入的最終一致性模型。
最初,事務鏈被新增到 XFS 中,以避免從檔案中取消對映空間時出現死鎖。避免死鎖的規則要求僅以遞增順序鎖定 AG,這使得(例如)不可能使用單個事務釋放 AG 7 中的空間範圍,然後嘗試釋放 AG 3 中現在多餘的塊對映 btree 塊。為了避免這些型別的死鎖,XFS 建立範圍釋放意圖 (EFI) 日誌項以提交以在一個事務中釋放一些空間,同時將實際元資料更新推遲到新事務。事務序列如下所示:
第一個事務包含對檔案塊對映結構的物理更新,以從 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 順序排序。
第二個事務包含對 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 而言將顯得不一致,並且將記錄對損壞的觀察。此觀察結果將不正確。如果在這種狀態下嘗試修復,結果將是災難性的!
在發現此缺陷後,評估了其他幾種解決方案並被拒絕:
向分配組新增更高級別的鎖,並要求編寫器執行緒在進行任何更改之前以 AG 順序獲取更高級別的鎖。這在實踐中很難實現,因為很難確定需要獲取哪些鎖以及以什麼順序獲取,而無需模擬整個操作。執行檔案操作的試執行以發現必要的鎖將使檔案系統變得非常慢。
使延遲工作協調器程式碼知道針對同一 AG 的連續 intent 項,並使其在更新之間的事務滾動中保持 AG 標頭緩衝區的鎖定狀態。這將給協調器帶來很多複雜性,因為它僅與實際的延遲工作項鬆散耦合。它也無法解決該問題,因為延遲工作項可以生成新的延遲子任務,但是必須在新的同級任務開始工作之前完成所有子任務。
指導線上 fsck 遍歷所有等待保護正在清理的資料結構的鎖的事務,以查詢掛起的操作。檢查和修復操作必須將這些掛起的操作考慮在執行的評估中。此解決方案不可行,因為它對主檔案系統的侵入性非常強。
線上 fsck 使用原子意圖項計數器和鎖迴圈來與事務鏈進行協調。drain 機制有兩個關鍵屬性。首先,當一個延遲工作項排隊到一個事務時,計數器會遞增,而在相關的意圖完成日誌項提交到另一個事務後,計數器會遞減。第二個屬性是,可以將延遲工作新增到事務中,而無需持有 AG 頭部鎖,但是,如果沒有鎖定該 AG 頭部緩衝區以記錄物理更新和意圖完成日誌項,則無法將每個 AG 的工作項標記為完成。第一個屬性使 scrub 能夠讓位於正在執行的事務鏈,這是線上 fsck 的顯式降級,以利於檔案操作。drain 的第二個屬性是正確協調 scrub 的關鍵,因為 scrub 將始終能夠確定是否可能發生衝突。
對於常規檔案系統程式碼,drain 的工作方式如下
呼叫適當的子系統函式以將延遲工作項新增到事務中。
該函式呼叫 xfs_defer_drain_bump 以增加計數器。
當延遲項管理器想要完成延遲工作項時,它會呼叫 ->finish_item 以完成它。
->finish_item 實現記錄一些更改,並呼叫 xfs_defer_drain_drop 以減少鬆散計數器,並喚醒任何等待 drain 的執行緒。
子事務提交,這將解鎖與意圖項關聯的資源。
對於 scrub,drain 的工作方式如下
鎖定與要 scrub 的元資料關聯的資源。例如,掃描 refcount btree 將鎖定 AGI 和 AGF 頭部緩衝區。
如果計數器為零(xfs_defer_drain_busy 返回 false),則沒有正在進行的鏈,並且該操作可以繼續。
否則,釋放在步驟 1 中獲取的資源。
等待意圖計數器達到零 (xfs_defer_drain_intents),然後返回到步驟 1,除非已捕獲訊號。
為避免在步驟 4 中進行輪詢,drain 提供了一個等待佇列,以便在意圖計數降至零時喚醒 scrub 執行緒。
提出的補丁集是 scrub intent drain 系列。
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 獲取的所有靜態鍵。
有關更多資訊,請參見 靜態鍵 的核心文件。
一些線上檢查函式透過掃描檔案系統以在記憶體中構建磁碟元資料結構的影子副本並比較兩個副本來進行工作。對於線上修復以重建元資料結構,它必須在將新結構持久儲存到磁碟之前,計算將儲存在新結構中的記錄集。理想情況下,修復應以單個原子提交完成,該提交會引入新的資料結構。為實現這些目標,核心需要在不需要檔案系統正確執行的地方收集大量資訊。
核心記憶體不適合,因為
分配連續的記憶體區域以建立 C 陣列非常困難,尤其是在 32 位系統上。
記錄的連結串列引入了雙指標開銷,這非常高,並且消除了索引查詢的可能性。
核心記憶體被固定,這可能會將系統驅動到 OOM 條件中。
系統可能沒有足夠的記憶體來暫存所有資訊。
在任何給定時間,線上 fsck 都不需要將整個記錄集保留在記憶體中,這意味著必要時可以分頁輸出各個記錄。線上 fsck 的持續開發表明,執行索引資料儲存的能力也將非常有用。幸運的是,Linux 核心已經具有用於位元組可定址和可分頁儲存的工具:tmpfs。核心圖形驅動程式(最著名的是 i915)利用 tmpfs 檔案來儲存不需要始終位於記憶體中的中間資料,因此已經建立了使用先例。因此,xfile 誕生了!
歷史側邊欄: |
線上修復的第一版在找到記錄時將其插入到新的 btree 中,這失敗了,因為檔案系統可能會關閉並構建資料結構,這將會在恢復完成後生效。
第二版透過將所有內容儲存在記憶體中來解決了一半重建的結構問題,但經常導致系統記憶體不足。
第三版透過使用連結串列解決了 OOM 問題,但是列表指標的記憶體開銷非常大。
|
對 xfile 預期用途的調查表明瞭以下用例
固定大小記錄的陣列(空間管理 btree、目錄和擴充套件屬性條目)
固定大小記錄的稀疏陣列(配額和連結計數)
可變大小的大型二進位制物件 (BLOB)(目錄和擴充套件屬性名稱和值)
在記憶體中暫存 btree(反向對映 btree)
任意內容(即時空間管理)
為了支援前四個用例,高階資料結構包裝 xfile 以共享線上 fsck 函式之間的功能。本節的其餘部分討論 xfile 向這五個更高級別的資料結構中的四個呈現的介面。第五個用例將在 即時摘要 案例研究中討論。
XFS 是基於記錄的,這表明載入和儲存完整記錄的能力很重要。為了支援這些情況,提供了一對 xfile_load 和 xfile_store 函式,用於將物件讀取和持久儲存到將任何錯誤視為記憶體不足錯誤的 xfile 中。對於線上修復,以這種方式壓縮錯誤情況是可以接受的行為,因為唯一的反應是中止操作返回到使用者空間。
但是,如果沒有回答“但是 mmap 呢?”這個問題,那麼對檔案訪問習慣用法的討論就不完整了。就像使用者空間程式碼使用常規記憶體一樣,使用指標直接訪問儲存是很方便的。線上 fsck 絕不能將系統驅動到 OOM 條件中,這意味著 xfile 必須對記憶體回收做出響應。如果 folio 既未固定也未鎖定,則 tmpfs 只能將 pagecache folio 推送到交換快取,這意味著 xfile 絕不能固定過多的 folio。
對 xfile 內容的短期直接訪問是透過鎖定 pagecache folio 並將其對映到核心地址空間來完成的。物件載入和儲存使用此機制。Folio 鎖不應持有很長時間,因此對 xfile 內容的長期直接訪問是透過提升 folio 引用計數,將其對映到核心地址空間,然後刪除 folio 鎖來完成的。這些長期使用者必須透過掛接到收縮器基礎結構來了解何時釋放 folio,從而對記憶體回收做出響應。
提供了 xfile_get_folio 和 xfile_put_folio 函式來檢索支援 xfile 部分的(已鎖定)folio 並釋放它。唯一使用這些 folio 租用函式的程式碼是 xfarray 排序 演算法和 記憶體 btree。
出於安全原因,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 函式必須提供一個鎖供所有執行緒共享。
在 XFS 中,每種型別的索引空間元資料(可用空間、inode、引用計數、檔案 fork 空間和反向對映)都由一組使用經典 B+ 樹索引的固定大小記錄組成。目錄有一組指向名稱的固定大小的目錄項記錄,擴充套件屬性有一組指向名稱和值的固定大小的屬性鍵。配額計數器和檔案連結計數器使用數字索引記錄。在修復期間,scrub 需要在收集步驟中暫存新記錄,並在 btree 構建步驟中檢索它們。
儘管可以透過直接呼叫 xfile 的讀取和寫入方法來滿足此要求,但是對於呼叫者來說,透過更高級別的抽象來處理計算陣列偏移量、提供迭代器函式以及處理稀疏記錄和排序會更簡單。xfarray 抽象在位元組可訪問的 xfile 之上呈現固定大小記錄的線性陣列。
線上 fsck 中的陣列訪問模式傾向於分為三類。假定所有情況下都需要記錄迭代,並且將在下一節中介紹。
第一種型別的呼叫者處理按位置索引的記錄。記錄之間可能存在間隙,並且在收集步驟中可能會多次更新記錄。換句話說,這些呼叫者需要稀疏的線性定址表文件。典型的用例是配額記錄或檔案連結計數記錄。對陣列元素的訪問是透過 xfarray_load 和 xfarray_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 中刪除記錄。
提出的補丁集是 大的記憶體陣列。
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 */
}
在線上修復的第四次演示中,一位社群審查員評論說,出於效能原因,線上修復應該將批次的記錄載入到 btree 記錄塊中,而不是一次將記錄插入到新的 btree 中。XFS 中的 btree 插入程式碼負責維護記錄的正確排序,因此,xfarray 自然也必須支援在批次載入之前對記錄集進行排序。
xfarray 中使用的排序演算法實際上是自適應快速排序和堆排序子演算法的組合,其精神與 Sedgewick 和 pdqsort 相同,並針對 Linux 核心進行了自定義。為了在合理的時間內對記錄進行排序,xfarray 利用了快速排序提供的二進位制子分割槽,但它也使用堆排序來對沖效能崩潰(如果選擇的快速排序樞軸較差)。這兩種演算法通常都是 O(n * lg(n)),但是兩種實現之間存在很大的效能差距。
Linux 核心已經包含堆排序的相當快的實現。它僅對常規 C 陣列進行操作,這限制了其有用範圍。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 頁面之後,這再次使整體排序執行時減少了近一半。
擴充套件屬性和目錄為暫存記錄添加了額外的要求:有限長度的任意位元組序列。每個目錄條目記錄都需要儲存條目名稱,並且每個擴充套件屬性都需要儲存屬性名稱和值。名稱、鍵和值可能會佔用大量記憶體,因此建立了 xfblob 抽象以簡化 xfile 之上這些 blob 的管理。
Blob 陣列提供 xfblob_load 和 xfblob_store 函式來檢索和持久儲存物件。store 函式為它持久儲存的每個物件返回一個魔術 cookie。稍後,呼叫者將提供此 cookie 給 xblob_load 以呼叫該物件。xfblob_free 函式釋放特定的 blob,並且 xfblob_truncate 函式釋放所有 blob,因為不需要壓縮。
修復目錄和擴充套件屬性的詳細資訊將在有關原子檔案內容交換的後續章節中討論。但是,應注意的是,這些修復函式僅使用 blob 儲存來快取少量條目,然後將其新增到臨時磁碟檔案中,這就是為什麼不需要壓縮的原因。
提出的補丁集位於 擴充套件屬性修復 系列的開頭。
關於 輔助元資料 的章節提到,輔助元資料的檢查和修復通常需要在檔案系統的即時元資料掃描與更新該元資料的寫入器執行緒之間進行協調。保持掃描資料最新需要能夠將元資料更新從檔案系統傳播到掃描收集的資料中。這可以透過將併發更新附加到單獨的日誌檔案中,並在將新元資料寫入磁碟之前應用它們來完成,但是如果系統的其餘部分非常繁忙,這會導致無限的記憶體消耗。另一個選擇是跳過 side-log 並將來自檔案系統的即時更新直接提交到掃描資料中,這以更多的開銷換取更低的最大記憶體需求。在這兩種情況下,儲存掃描結果的資料結構都必須支援索引訪問才能表現良好。
鑑於這兩種策略都需要掃描資料的索引查詢,因此線上 fsck 採用第二種策略,即將即時更新直接提交到掃描資料中。但是,由於 xfarray 未索引且不強制執行記錄排序,因此它們不適合此任務。但是,方便的是,XFS 有一個庫可以建立和維護排序的反向對映記錄:現有的 rmap btree 程式碼!如果只有一種在記憶體中建立它的方法。
回想一下,xfile 抽象將記憶體頁面表示為常規檔案,這意味著核心可以隨意建立位元組或塊可定址的虛擬地址空間。XFS 緩衝區快取專門用於將 IO 抽象為面向塊的地址空間,這意味著將緩衝區快取適應為與 xfile 介面可以重用整個 btree 庫。構建在 xfile 之上的 btree 統稱為 xfbtree。接下來的幾節描述了它們是如何實際工作的。
提出的補丁集是 記憶體 btree 系列。
需要進行兩項修改才能支援 xfile 作為緩衝區快取目標。首先,要使 struct xfs_buftarg 結構能夠託管 struct xfs_buf rhashtable,因為通常這些結構由每個 AG 結構持有。第二個更改是修改緩衝區 ioapply 函式以從 xfile“讀取”快取的頁面,並將快取的頁面“寫入”回 xfile。對單個緩衝區的多次訪問由 xfs_buf 鎖控制,因為 xfile 本身不提供任何鎖定。透過這種適應,xfile 支援的緩衝區快取的使用者使用的 API 與磁碟支援的緩衝區快取的使用者完全相同。xfile 和緩衝區快取之間的分離意味著更高的記憶體使用率,因為它們不共享頁面,但是此屬性有一天可能會使對記憶體中 btree 的事務性更新成為可能。但是,今天,它只是消除了對新程式碼的需求。
xffile 的空間管理非常簡單——每個 btree 塊都是一個記憶體頁面大小。這些塊使用與磁碟 btree 相同的頭部格式,但是記憶體中的塊驗證器會忽略校驗和,假設 xfile 記憶體與常規 DRAM 一樣不易受到損壞。重用此處現有的程式碼比絕對記憶體效率更重要。
支援 xfbtree 的 xfile 的第一個塊包含一個頭部塊。頭部描述了所有者、高度和根 xfbtree 塊的塊號。
要分配 btree 塊,請使用 xfile_seek_data 在檔案中找到一個間隙。如果沒有間隙,請透過擴充套件 xfile 的長度來建立一個。使用 xfile_prealloc 預分配塊的空間,然後返回該位置。要釋放 xfbtree 塊,請使用 xfile_discard(內部使用 FALLOC_FL_PUNCH_HOLE)從 xfile 中刪除記憶體頁面。
想要建立 xfbtree 的線上 fsck 函式應按如下方式進行
呼叫 xfile_create 以建立一個 xfile。
呼叫 xfs_alloc_memory_buftarg 以建立一個指向 xfile 的緩衝區快取目標結構。
將緩衝區快取目標、緩衝區操作和其他資訊傳遞給 xfbtree_init 以初始化傳入的 struct xfbtree,並將初始根塊寫入 xfile。每種 btree 型別都應該定義一個包裝器,將必要的引數傳遞給建立函式。例如,rmap btree 定義了 xfs_rmapbt_mem_create,以處理呼叫者所需的所有細節。
將 xfbtree 物件傳遞給 btree 遊標建立函式,用於該 btree 型別。按照上面的例子,xfs_rmapbt_mem_cursor 負責為呼叫者處理此操作。
將 btree 遊標傳遞給常規 btree 函式,以便對記憶體中的 btree 進行查詢和更新。例如,rmap xfbtree 的 btree 遊標可以像任何其他 btree 遊標一樣傳遞給 xfs_rmap_* 函式。有關處理記錄到事務的 xfbtree 更新的資訊,請參見 下一節。
完成後,刪除 btree 遊標,銷燬 xfbtree 物件,釋放緩衝區目標,並銷燬 xfile 以釋放所有資源。
儘管重用 rmap btree 程式碼來處理暫存結構是一個巧妙的技巧,但記憶體中 btree 塊儲存的短暫性也帶來了一些挑戰。XFS 事務管理器不得為由 xfile 支援的緩衝區提交緩衝區日誌項,因為日誌格式不理解對資料裝置以外的裝置的更新。臨時 xfbtree 可能在 AIL 將日誌事務檢查點寫回檔案系統時不存在,並且肯定不會在日誌恢復期間存在。因此,任何在事務上下文中更新 xfbtree 的程式碼都必須在提交或取消事務之前,從事務中刪除緩衝區日誌項並將更新寫入支援 xfile。
xfbtree_trans_commit 和 xfbtree_trans_cancel 函式按如下方式實現此功能
查詢每個緩衝區日誌項,其緩衝區以 xfile 為目標。
記錄日誌項的 dirty/ordered 狀態。
從緩衝區分離日誌項。
將緩衝區排隊到特殊的 delwri 列表。
如果唯一的 dirty 日誌項是在步驟 3 中分離的那些,則清除事務 dirty 標誌。
如果正在提交更新,則提交 delwri 列表以將更改提交到 xfile。
以這種方式從事務中刪除 xfile 已記錄緩衝區後,可以提交或取消事務。
如前所述,線上修復的早期迭代透過建立一個新的 btree 並單獨新增觀察結果來構建新的 btree 結構。一次載入一個記錄的 btree 的一個優點是不需要在提交之前對 incore 記錄進行排序,但是速度非常慢,並且如果系統在修復過程中關閉,則會洩漏塊。一次載入一個記錄也意味著修復無法控制新 btree 中塊的載入因子。
幸運的是,歷史悠久的 xfs_repair 工具有一種更有效的方法可以從記錄集合中重建 btree 索引 -- 批次 btree 載入。由於 xfs_repair 對每種 btree 型別都有單獨的複製貼上實現,因此這在程式碼方面實現得相當低效。
為了準備線上 fsck,研究了每個批次載入器,記下了筆記,並將這四個載入器重構為單個通用 btree 批次載入機制。這些筆記也進行了更新,並在下面列出。
批次載入的第零步是組裝將儲存在新 btree 中的整個記錄集,並對記錄進行排序。接下來,呼叫 xfs_btree_bload_compute_geometry 以從記錄集、btree 型別和任何載入因子偏好來計算 btree 的形狀。此資訊是資源預留所必需的。
首先,幾何計算從 btree 塊的大小和塊頭部的大小來計算適合葉子塊的最小和最大記錄。粗略地說,記錄的最大數量是
maxrecs = (block_size - header_size) / record_size
XFS 設計指定應該儘可能合併 btree 塊,這意味著記錄的最小數量是 maxrecs 的一半
要確定的下一個變數是所需的載入因子。它必須至少為 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 中紮根的能力時,這才會變得相關,這是一個未來的補丁集,僅在此處為了完整性而包含。
一旦修復知道新 btree 所需的塊數,它將使用可用空間資訊來分配這些塊。每個預留範圍由 btree 構建器狀態資料單獨跟蹤。為了提高崩潰彈性,預留程式碼還在與每個空間分配相同的事務中記錄一個範圍釋放意圖 (EFI) 項,並將其記憶體中的 struct xfs_extent_free_item 物件附加到空間預留。如果系統關閉,日誌恢復將使用未完成的 EFI 來釋放未使用的空間,從而使檔案系統保持不變。
每次 btree 構建器從預留範圍中宣告一個用於 btree 的塊時,它都會更新記憶體中的預留以反映已宣告的空間。塊預留會嘗試分配儘可能多的連續空間,以減少正在使用的 EFI 數量。
當修復正在寫入這些新的 btree 塊時,為空間預留建立的 EFI 會固定磁碟上日誌的尾部。系統的其他部分可能會保持繁忙併將日誌的頭部推向固定的尾部。為了避免檔案系統發生死鎖,EFI 不得將日誌的尾部固定太長時間。為了緩解這個問題,此處重用了延遲操作機制的動態重新記錄功能,以提交一個位於日誌頭部的事務,該事務包含舊 EFI 的 EFD 和頭部的新 EFI。這使日誌能夠釋放舊 EFI 以保持日誌向前移動。
EFI 在提交和收割階段都發揮著作用;請參見下一節和關於 收割 的部分了解更多詳細資訊。
提議的補丁集是 點陣圖重做 和 為批次載入 btree 做準備。
這部分非常簡單 -- 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 所做的空間預留,並收割舊的元資料塊
提交新 btree 根的位置。
對於每個 incore 預留
為 btree 構建器使用的所有空間記錄範圍釋放完成 (EFD) 項。新的 EFD 必須指向附加到預留的 EFI,以防止日誌恢復釋放新的塊。
對於 incore 預留的未宣告部分,建立一個常規的延遲範圍釋放工作項,以便稍後在事務鏈中釋放未使用的空間。
在步驟 2a 和 2b 中記錄的 EFD 和 EFI 不得超出提交事務的預留。如果 btree 載入程式碼懷疑這可能即將發生,它必須呼叫 xrep_defer_finish 以清除延遲工作並獲得新的事務。
第二次清除延遲工作以完成提交併清理修復事務。
步驟 2c 和 3 中滾動的事務代表了修復演算法中的一個弱點,因為在收割步驟結束之前進行日誌重新整理和崩潰可能會導致空間洩漏。線上修復函式透過使用非常大的事務來最大限度地減少這種情況發生的可能性,每個事務都可以容納數千個塊釋放指令。修復會繼續收割舊塊,這將在以下 部分 中在幾個批次載入的案例研究之後進行介紹。
重建 inode 索引 btree 的高階過程是
遍歷反向對映記錄以從 inode 塊資訊和舊 inode btree 塊的點陣圖生成 struct xfs_inobt_rec 記錄。
按 inode 順序將記錄附加到 xfarray。
使用 xfs_btree_bload_compute_geometry 函式來計算 inode btree 所需的塊數。如果啟用了可用空間 inode btree,則再次呼叫它以估計 finobt 的幾何形狀。
分配上一步中計算的塊數。
使用 xfs_btree_bload 將 xfarray 記錄寫入 btree 塊並生成內部節點塊。如果啟用了可用空間 inode btree,則再次呼叫它以載入 finobt。
將新 btree 根塊的位置提交到 AGI。
使用在步驟 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 修復 系列。
反向對映記錄用於重建引用計數資訊。引用計數對於共享檔案資料的寫時複製的正確操作是必需的。將反向對映條目想象成代表物理塊範圍的矩形,並且可以放置矩形以允許它們相互重疊。從下面的圖中可以明顯看出,引用計數記錄必須在堆疊的高度發生變化的任何位置開始或結束。換句話說,記錄發射刺激是電平觸發的
█ ███
██ █████ ████ ███ ██████
██ ████ ███████████ ████ █████████
████████████████████████████████ ███████████
^ ^ ^^ ^^ ^ ^^ ^^^ ^^^^ ^ ^^ ^ ^ ^
2 1 23 21 3 43 234 2123 1 01 2 3 0
磁碟上的引用計數 btree 不儲存 refcount == 0 的情況,因為可用空間 btree 已經記錄了哪些塊是空閒的。用於暫存寫時複製操作的範圍應該是唯一 refcount == 1 的記錄。單個所有者的檔案塊不會記錄在可用空間或引用計數 btree 中。
重建引用計數 btree 的高階過程是
遍歷反向對映記錄以生成 struct xfs_refcount_irec 記錄,用於任何具有多個反向對映的空間,並將它們新增到 xfarray。所有者為 XFS_RMAP_OWN_COW 的任何記錄也會新增到 xfarray,因為這些範圍是分配用於暫存寫時複製操作的範圍,並在 refcount btree 中進行跟蹤。
使用所有者為 XFS_RMAP_OWN_REFC 的任何記錄來建立舊 refcount btree 塊的點陣圖。
按物理範圍順序對記錄進行排序,將 CoW 暫存範圍放在 xfarray 的末尾。這與 refcount btree 中記錄的排序順序相匹配。
使用 xfs_btree_bload_compute_geometry 函式來計算新樹所需的塊數。
分配上一步中計算的塊數。
使用 xfs_btree_bload 將 xfarray 記錄寫入 btree 塊並生成內部節點塊。
將新 btree 根塊的位置提交到 AGF。
使用在步驟 1 中建立的點陣圖收割舊的 btree 塊。
詳細資訊如下;xfs_repair 使用相同的演算法從反向對映記錄生成 refcount 資訊。
在這種情況下,類似包的結構是 xfarray 訪問模式 部分中討論的型別 2 xfarray。使用 xfarray_store_anywhere 將反向對映新增到包中,並使用 xfarray_unset 刪除。透過 xfarray_iter 迴圈檢查包成員。
提議的補丁集是 AG btree 修復 系列。
重建資料/屬性分叉對映 btree 的高階過程是
遍歷反向對映記錄以從該 inode 和分叉的反向對映記錄生成 struct xfs_bmbt_rec 記錄。將這些記錄附加到 xfarray。從 BMBT_BLOCK 記錄計算舊 bmap btree 塊的點陣圖。
使用 xfs_btree_bload_compute_geometry 函式來計算新樹所需的塊數。
按檔案偏移量順序對記錄進行排序。
如果範圍記錄適合 inode 分叉立即區域,則將記錄提交到該立即區域並跳到步驟 8。
分配上一步中計算的塊數。
使用 xfs_btree_bload 將 xfarray 記錄寫入 btree 塊並生成內部節點塊。
將新的 btree 根塊提交到 inode 分叉立即區域。
使用在步驟 1 中建立的點陣圖收割舊的 btree 塊。
這裡有一些複雜性:首先,如果資料和屬性分叉都不是 BMBT 格式,則可以移動分叉偏移量以調整立即區域的大小。其次,如果分叉對映足夠少,則可以使用 EXTENTS 格式而不是 BMBT,這可能需要轉換。第三,必須小心地重新載入 incore 範圍對映,以避免干擾任何延遲分配範圍。
提議的補丁集是 檔案對映修復 系列。
Inode記錄必須小心處理,因為它們既有磁碟上的記錄(“dinodes”),也有記憶體中的(“快取”)表示形式。如果線上fsck在訪問磁碟上的元資料時,沒有小心謹慎地訪問磁碟上的元資料,因為磁碟上的元資料損壞嚴重,檔案系統無法載入記憶體中的表示形式,那麼存在很高的快取一致性問題風險。當線上fsck想要開啟一個損壞的檔案進行清理時,它必須使用專門的資源獲取函式,該函式返回記憶體中的表示形式或一個鎖,該鎖對於阻止對磁碟上位置的任何更新是必需的。
應該對磁碟上的inode緩衝區進行的唯一修復是為了載入核心結構所必需的任何操作。這意味著修復inode叢集緩衝區和inode fork驗證程式捕獲的任何內容,並重試iget操作。如果第二次iget失敗,則修復失敗。
一旦載入了記憶體中的表示形式,修復程式可以鎖定inode,並可以對其進行全面的檢查、修復和最佳化。大多數inode屬性都很容易檢查和約束,或者是使用者控制的任意位模式;這些都容易修復。處理資料和attr fork範圍計數以及檔案塊計數更為複雜,因為計算正確的值需要遍歷fork,或者如果失敗,則使欄位無效並等待fork fsck函式執行。
建議的補丁集是inode修復系列。
與inode類似,配額記錄(“dquots”)也具有磁碟上的記錄和記憶體中的表示形式,因此也受到相同的快取一致性問題的約束。有些令人困惑的是,在XFS程式碼庫中,兩者都稱為dquots。
應該對磁碟上的配額記錄緩衝區進行的唯一修復是為了載入核心結構所必需的任何操作。一旦載入了記憶體中的表示形式,唯一需要檢查的屬性是明顯錯誤的限制和計時器值。
配額使用計數器的檢查、修復和討論在關於動態quotacheck的章節中單獨進行。
建議的補丁集是配額修復系列。
檔案系統摘要計數器跟蹤檔案系統資源的可用性,例如自由塊、自由inode和已分配inode。此資訊可以透過遍歷自由空間和inode索引來編譯,但這是一個緩慢的過程,因此XFS在磁碟上的超級塊中維護了一個副本,該副本應反映磁碟上的元資料,至少在檔案系統已乾淨解除安裝時是這樣。出於效能原因,XFS還維護這些計數器的核心副本,這些副本是為活動事務啟用資源保留的關鍵。編寫器執行緒從核心計數器保留最壞情況下的資源數量,並在提交時返回他們不使用的任何資源。因此,只有在將超級塊提交到磁碟時,才需要對超級塊進行序列化。
XFS v5中引入的惰性超級塊計數器功能透過訓練日誌恢復以從AG標頭重新計算摘要計數器,從而更進一步,這消除了大多數事務甚至觸控超級塊的需求。XFS提交摘要計數器的唯一時間是在檔案系統解除安裝時。為了進一步減少爭用,核心計數器被實現為percpu計數器,這意味著每個CPU都從全域性核心計數器分配一批塊,並且可以滿足來自本地批的小分配。
摘要計數器的高效能性質使得線上fsck難以檢查它們,因為無法在系統執行時停止percpu計數器。儘管線上fsck可以讀取檔案系統元資料來計算摘要計數器的正確值,但無法保持percpu計數器的值穩定,因此在遍歷完成時,計數器很可能已過期。早期版本的線上清理會將不完整的掃描標誌返回給使用者空間,但這對於系統管理員來說不是一個令人滿意的結果。對於修復,在遍歷檔案系統元資料以獲得準確的讀取並將其安裝在percpu計數器中時,必須穩定記憶體中的計數器。
為了滿足此要求,線上fsck必須阻止系統中其他程式啟動新的檔案系統寫入,它必須停用後臺垃圾收集執行緒,並且它必須等待現有編寫器程式退出核心。一旦建立,清理就可以遍歷AG自由空間索引、inode B樹和即時點陣圖,以計算所有四個摘要計數器的正確值。這與檔案系統凍結非常相似,儘管並非所有部分都是必需的。
有了這段程式碼,現在可以暫停檔案系統,只需足夠長的時間來檢查和更正摘要計數器。
歷史側邊欄: |
最初的實現使用了實際的VFS檔案系統凍結機制來停止檔案系統活動。在檔案系統凍結的情況下,可以精確地解析計數器值,但是直接呼叫VFS方法存在許多問題
其他程式可以在我們不知情的情況下解凍檔案系統。這導致不正確的掃描結果和不正確的修復。
新增額外的鎖以防止其他人解凍檔案系統需要在freeze_fs()周圍新增一個->freeze_super函式。反過來,這導致了其他微妙的問題,因為事實證明VFS freeze_super和thaw_super函式可以刪除對VFS超級塊的最後一個引用,並且任何後續訪問都會成為UAF錯誤!如果底層塊裝置凍結了檔案系統,則可能發生這種情況。這個問題可以透過獲取對超級塊的額外引用來解決,但是鑑於此方法的其他不足之處,感覺並不理想。
無需停止日誌即可檢查摘要計數器,但是VFS凍結無論如何都會啟動一個。這會給動態fscounter fsck操作增加不必要的執行時。
停止日誌意味著XFS會將(可能不正確的)計數器重新整理到磁碟,作為清理日誌的一部分。
VFS中的一個錯誤意味著即使sync_filesystem無法重新整理檔案系統並返回錯誤,凍結也可能完成。此錯誤已在Linux 5.17中修復。
|
建議的補丁集是摘要計數器清理系列。
某些型別的元資料只能透過遍歷整個檔案系統中的每個檔案來記錄觀察結果,並將觀察結果與磁碟上記錄的內容進行比較來檢查。與任何其他型別的線上修復一樣,修復是透過將這些觀察結果寫入替換結構並以原子方式提交來進行的。但是,關閉整個檔案系統以檢查數千億個檔案是不切實際的,因為停機時間會過長。因此,線上fsck必須構建基礎結構來管理對檔案系統中所有檔案的動態掃描。要執行動態遍歷,需要解決兩個問題
清理程式在收集資料時如何管理掃描?
掃描如何跟上其他執行緒對系統進行的更改?
在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中
鎖定包含由已訪問inode游標指向的inode的AGI緩衝區。這保證了在推進游標時,此AG中的inode無法被分配或釋放。
使用per-AG inode B樹查詢剛訪問的inode之後的下一個inumber,因為它可能不是金鑰空間相鄰的。
如果此AG中沒有剩餘的inode
將檢查游標移動到與下一個AG的開頭相對應的inumber金鑰空間點。
調整已訪問inode游標,以指示它已“訪問”當前AG的inode金鑰空間中的最後一個可能的inode。XFS inumber是分段的,因此游標需要標記為已訪問直到剛好在下一個AG的inode金鑰空間開始之前的所有金鑰空間。
解鎖AGI,如果檔案系統中存在未檢查的AG,則返回到步驟1。
如果沒有更多AG要檢查,請將兩個游標都設定為inumber金鑰空間的末尾。掃描現已完成。
否則,此AG中至少還有另一個inode要掃描
將檢查游標向前移動到inode B樹標記為已分配的下一個inode。
調整已訪問inode游標以指向剛好在檢查游標當前位置之前的inode。因為掃描器持有AGI緩衝區鎖,所以無法在已訪問inode游標剛剛推進的inode金鑰空間的一部分中建立任何inode。
獲取檢查游標的inumber的核心inode。透過將AGI緩衝區鎖保持到此時,掃描器知道可以安全地跨整個金鑰空間推進檢查游標,並且它已經穩定了下一個inode,因此直到掃描釋放核心inode,它才可能從檔案系統中消失。
放下AGI鎖,並將核心inode返回給呼叫方。
線上fsck函式按以下方式掃描檔案系統中的所有檔案
透過呼叫xchk_iscan_start啟動掃描。
推進掃描游標(xchk_iscan_iter)以獲取下一個inode。如果提供了一個inode
鎖定inode以防止在掃描期間進行更新。
掃描inode。
在仍持有inode鎖定的情況下,調整已訪問inode游標(xchk_iscan_mark_visited)以指向此inode。
解鎖並釋放inode。
呼叫xchk_iscan_teardown以完成掃描。
inode快取存在一些細微之處,這使得為呼叫方抓取核心inode變得複雜。顯然,inode元資料必須足夠一致才能將其載入到inode快取中。其次,如果核心inode卡在某個中間狀態,則掃描協調器必須釋放AGI並推動主檔案系統以使inode恢復到可載入狀態。
建議的補丁是inode掃描器系列。新功能的第一個使用者是線上quotacheck系列。
在常規檔案系統程式碼中,對已分配XFS核心inode的引用始終在事務上下文之外獲取(xfs_iget),因為為現有檔案建立核心上下文不需要元資料更新。但是,重要的是要注意,作為檔案建立一部分獲取的對核心inode的引用必須在事務上下文中執行,因為檔案系統必須確保磁碟上的inode B樹索引更新和實際磁碟上的inode初始化的原子性。
對核心inode的引用始終在事務上下文之外釋放(xfs_irele),因為有一些活動可能需要磁碟上的更新
這些活動統稱為inode失活。失活有兩個部分——VFS部分,它啟動所有髒檔案頁的回寫,以及XFS部分,它清理XFS特定的資訊並在inode未連結時釋放inode。如果inode未連結(或在檔案控制代碼操作後未連線),則核心會立即將inode放入失活機制中。
在正常操作期間,更新的資源獲取遵循此順序以避免死鎖
Inode引用(iget)。
檔案系統凍結保護,如果在修復(mnt_want_write_file)。
Inode IOLOCK(VFS i_rwsem)鎖來控制檔案IO。
Inode MMAPLOCK(頁面快取invalidate_lock)鎖用於可以更新頁面快取對映的操作。
日誌功能啟用。
事務日誌空間授予。
資料和即時裝置上的事務空間。
如果正在修復檔案,則為核心dquot引用。請注意,它們沒有被鎖定,只是被獲取。
Inode ILOCK用於檔案元資料更新。
AG標頭緩衝區鎖/即時元資料inode ILOCK。
即時元資料緩衝區鎖,如果適用。
範圍對映B樹塊,如果適用。
資源通常以相反的順序釋放,儘管這不是必需的。但是,線上fsck與常規XFS操作不同,因為它可能會檢查通常在鎖定順序的後期階段獲取的物件,然後決定將該物件與在順序中較早獲取的物件進行交叉引用。接下來的幾個部分詳細介紹了線上fsck如何小心避免死鎖的具體方法。
代表清理操作執行的inode掃描在事務上下文中執行,並且可能已經鎖定了資源並將其繫結到該事務上下文中。對於iget來說,這並不是什麼大問題,因為它可以在現有事務的上下文中執行,只要所有繫結資源都在常規檔案系統中的inode引用之前獲取即可。
當VFS iput函式被賦予一個沒有其他引用的連結inode時,它通常會將inode放在LRU列表中,希望如果另一個程序在系統耗盡記憶體並釋放inode之前重新開啟該檔案,則可以節省時間。檔案系統呼叫方可以透過在inode上設定DONTCACHE標誌來使LRU過程短路,以導致核心嘗試立即將inode放入失活機制中。
過去,失活總是從刪除inode的程序完成的,這對於清理來說是一個問題,因為清理可能已經持有一個事務,並且XFS不支援巢狀事務。另一方面,如果沒有清理事務,則最好立即刪除未使用的inode,以避免汙染快取。為了捕捉這些細微之處,線上fsck程式碼有一個單獨的xchk_irele函式來設定或清除DONTCACHE標誌以獲得所需的釋放行為。
建議的補丁集包括修復清理iget用法和dir iget用法。
在常規檔案系統程式碼中,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必須準備好測量鎖定週期之前和之後的清理資源,以檢測更改並做出相應的反應。
考慮目錄父指標修復程式碼作為一個例子。線上fsck必須驗證目錄的dotdot dirent是否指向父目錄,並且父目錄是否包含僅一個指向子目錄的dirent。完全驗證這種關係(並在可能的情況下修復它)需要在持有子級鎖定的情況下遍歷檔案系統上的每個目錄,並且在目錄樹正在更新時進行。協調的inode掃描提供了一種遍歷檔案系統的方法,而不會遺漏inode。子目錄保持鎖定以防止更新dotdot dirent,但是如果掃描器無法鎖定父級,則可以刪除並重新鎖定子級和預期的父級。如果在目錄解鎖時dotdot條目發生更改,則移動或重新命名操作必須已更改子級的父級,並且掃描可以提前退出。
建議的補丁集是目錄修復系列。
線上fsck函式在完整檔案系統掃描期間需要的第二個支援是能夠隨時瞭解檔案系統中其他執行緒所做的更新,因為與過去的比較在動態環境中毫無用處。兩個Linux核心基礎結構使線上fsck能夠監視常規檔案系統操作:檔案系統掛鉤和靜態鍵。
檔案系統掛鉤將有關正在進行的檔案系統操作的資訊傳遞給下游使用者。在這種情況下,下游使用者始終是線上fsck函式。由於多個fsck函式可以並行執行,因此線上fsck使用Linux通知程式呼叫鏈工具將更新分派給任何數量的感興趣的fsck程序。呼叫鏈是一個動態列表,這意味著可以在執行時對其進行配置。由於這些掛鉤是XFS模組私有的,因此傳遞的資訊僅包含檢查函式更新其觀察結果所需的內容。
當前XFS掛鉤的實現使用SRCU通知程式鏈來減少對高度執行緒化工作負載的影響。常規阻塞通知程式鏈使用rwsem,並且對於單執行緒應用程式而言,開銷似乎要低得多。但是,事實證明,阻塞鏈和靜態鍵的組合是更高效的組合;此處需要更多研究。
要掛鉤檔案系統中的某個點,需要以下部分
一個struct xfs_hooks物件必須嵌入在方便的位置,例如眾所周知的核心檔案系統物件中。
每個掛鉤必須定義一個操作程式碼和一個包含有關操作的更多上下文的結構。
掛鉤提供程式應在xfs_hooks和xfs_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未執行時將檔案系統掛鉤的開銷降低到幾乎為零。
線上 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 掃描器 系列的一部分。
比較掛載時間配額檢查程式碼與線上修復配額檢查程式碼非常有用。 掛載時間配額檢查不必與併發操作競爭,因此它執行以下操作
確保 ondisk dquot 的狀態足夠好,所有 incore dquot 都可以實際載入,並將 ondisk 緩衝區中的資源使用計數器歸零。
遍歷檔案系統中的每個 inode。 將每個檔案的資源使用情況新增到 incore dquot。
遍歷每個 incore dquot。 如果 incore dquot 沒有被重新整理,則將支援 incore dquot 的 ondisk 緩衝區新增到延遲寫入 (delwri) 列表。
將緩衝區列表寫入磁碟。
與大多數線上 fsck 函式一樣,線上配額檢查在新的收集的元資料反映所有檔案系統狀態之前,無法寫入常規檔案系統物件。 因此,線上配額檢查將檔案資源使用情況記錄到使用稀疏 xfarray 實現的影子 dquot 索引中,並且僅在掃描完成後才寫入真正的 dquot。 處理事務性更新很棘手,因為配額資源使用情況更新分階段處理,以最大限度地減少對 dquot 的爭用
所涉及的 inode 被連線並鎖定到事務。
對於附加到檔案的每個 dquot
dquot 被鎖定。
配額預留被新增到 dquot 的資源使用情況中。 預留記錄在事務中。
dquot 被解鎖。
實際配額使用情況的變化在事務中被跟蹤。
在事務提交時,再次檢查每個 dquot
dquot 再次被鎖定。
配額使用情況更改被記錄,未使用的預留被返回給 dquot。
dquot 被解鎖。
對於線上配額檢查,hook 被放置在步驟 2 和 4 中。步驟 2 的 hook 建立事務 dquot 上下文 (dqtrx) 的影子版本,其操作方式與常規程式碼類似。 步驟 4 的 hook 將影子 dqtrx 更改提交到影子 dquot。 請注意,這兩個 hook 都使用鎖定的 inode 呼叫,這就是即時更新與 inode 掃描器協調的方式。
配額檢查掃描如下所示
設定一個協調的 inode 掃描。
對於 inode 掃描迭代器返回的每個 inode
獲取並鎖定 inode。
確定 inode 的資源使用情況(資料塊、inode 計數、即時塊),並將其新增到與 inode 關聯的使用者、組和專案 ID 的影子 dquot 中。
解鎖並釋放inode。
對於系統中的每個 dquot
獲取並鎖定 dquot。
根據掃描建立並由即時 hook 更新的影子 dquot 檢查 dquot。
即時更新是能夠遍歷每個配額記錄的關鍵,而無需長時間持有任何鎖。 如果需要修復,則鎖定真實 dquot 和影子 dquot,並且它們的資源計數被設定為影子 dquot 中的值。
提議的補丁集是 線上配額檢查 系列。
檔案連結計數檢查也使用即時更新 hook。 協調的 inode 掃描器用於訪問檔案系統上的所有目錄,並且每個檔案的連結計數記錄儲存在按 inumber 索引的稀疏 xfarray 中。 在掃描階段,目錄中的每個條目都會生成觀察資料,如下所示
如果條目是根目錄的 dotdot ('..') 條目,則目錄的父連結計數會增加,因為根目錄的 dotdot 條目是自引用的。
如果條目是子目錄的 dotdot 條目,則父目錄的反向引用計數會增加。
如果條目既不是 dot 也不是 dotdot 條目,則目標檔案的父計數會增加。
如果目標是子目錄,則父目錄的子連結計數會增加。
要理解連結計數 inode 掃描器如何與即時更新 hook 互動的一個關鍵點是,掃描游標跟蹤哪些父目錄已被掃描。 換句話說,當 A 尚未被掃描時,即使 B 已被掃描,即時更新也會忽略有關 A → B 的任何更新。 此外,具有指向 B 的 dotdot 條目的子目錄 A 被計為 A 的影子資料中的反向引用計數器,因為子 dotdot 條目會影響父目錄的連結計數。 即時更新 hook 被謹慎地放置在檔案系統中建立、更改或刪除目錄條目的所有部分,因為這些操作涉及 bumplink 和 droplink。
對於任何檔案,正確的連結計數是父目錄的數量加上子目錄的數量。 非目錄永遠沒有任何型別的子目錄。 反向引用資訊用於檢測指向子目錄的連結數量和指向後方的 dotdot 條目數量中的不一致。
掃描完成後,可以透過鎖定 inode 和影子資料並比較連結計數來檢查每個檔案的連結計數。 第二個協調的 inode 掃描游標用於比較。 即時更新是能夠在 inode 之間無需持有任何鎖的情況下遍歷每個 inode 的關鍵。 如果需要修復,則 inode 的連結計數被設定為影子資訊中的值。 如果沒有找到父目錄,則必須將該檔案重新指向孤兒院,以防止該檔案永遠丟失。
提議的補丁集是 檔案連結計數修復 系列。
大多數修復函式遵循相同的模式:鎖定檔案系統資源,遍歷現存的 ondisk 元資料以查詢替換元資料記錄,並使用記憶體中的陣列來儲存收集到的觀察結果。 這種方法的主要優點是修復程式碼的簡單性和模組化 - 程式碼和資料完全包含在 scrub 模組中,不需要主檔案系統中的 hook,並且通常在記憶體使用方面最有效。 這種修復方法的第二個優點是原子性 - 一旦核心確定某個結構已損壞,在核心完成修復和重新驗證元資料之前,沒有其他執行緒可以訪問該元資料。
對於在檔案系統的分片中進行的修復,這些優點超過了在修復分片的部分時鎖定分片所固有的延遲。 不幸的是,反向對映 btree 的修復不能使用“標準” btree 修復策略,因為它必須掃描檔案系統中每個檔案的每個分支的每個空間對映,並且檔案系統不能停止。 因此,rmap 修復放棄了 scrub 和修復之間的原子性。 它結合了協調的 inode 掃描器、即時更新 hook和記憶體中的 rmap btree來完成反向對映記錄的掃描。
設定一個 xfbtree 來暫存 rmap 記錄。
在持有 scrub 期間獲取的 AGI 和 AGF 緩衝區上的鎖時,為所有 AG 元資料生成反向對映:inodes、btrees、CoW 暫存 extent 和內部日誌。
設定一個 inode 掃描器。
hook 到正在修復的 AG 的 rmap 更新中,以便即時掃描資料可以在檔案掃描期間接收來自檔案系統其餘部分的反向對映 btree 的更新。
對於在掃描的每個檔案的任一分支中找到的每個空間對映,確定該對映是否與感興趣的 AG 匹配。 如果是,則
為記憶體中的 btree 建立一個 btree 游標。
使用 rmap 程式碼將記錄新增到記憶體中的 btree。
使用特殊提交函式將 xfbtree 更改寫入 xfile。
對於透過 hook 接收到的每個即時更新,確定所有者是否已經被掃描。 如果是,則將即時更新應用於掃描資料中
為記憶體中的 btree 建立一個 btree 游標。
將操作重放到記憶體中的 btree 中。
使用特殊提交函式將 xfbtree 更改寫入 xfile。 這是使用空事務執行的,以避免更改呼叫者的狀態。
當 inode 掃描完成時,建立一個新的 scrub 事務並重新鎖定兩個 AG 標頭。
像所有其他 btree 重建函式一樣,使用影子 btree 中的 rmap 記錄數計算新的 btree 幾何。
分配上一步中計算的塊數。
執行通常的 btree 批次載入並提交以安裝新的 rmap btree。
如關於如何在 rmap btree 修復後進行收割的案例研究中所討論的那樣,收割舊的 rmap btree 塊。
釋放 xfbtree,因為它現在不需要了。
提議的補丁集是 rmap 修復 系列。
XFS 在檔案分支中儲存大量的元資料:目錄、擴充套件屬性、符號連結目標、即時卷的可用空間點陣圖和摘要資訊以及配額記錄。 檔案分支將 64 位邏輯檔案分支空間 extent 對映到物理儲存空間 extent,類似於記憶體管理單元將 64 位虛擬地址對映到物理記憶體地址的方式。 因此,基於檔案的樹結構(例如目錄和擴充套件屬性)使用對映在檔案分支偏移地址空間中的塊,這些塊指向對映在同一地址空間內的其他塊,而基於檔案的線性結構(例如點陣圖和配額記錄)則計算檔案分支偏移地址空間中的陣列元素偏移量。
由於檔案分支可以消耗與整個檔案系統一樣多的空間,因此即使有分頁方案可用,也無法在記憶體中暫存修復。 因此,基於檔案的元資料的線上修復會在 XFS 檔案系統中建立一個臨時檔案,將新的結構以正確的偏移量寫入臨時檔案,並以原子方式交換所有檔案分支對映(以及分支內容)以提交修復。 修復完成後,可以根據需要收割舊的分支; 如果系統在收割期間關閉,則 iunlink 程式碼將在日誌恢復期間刪除這些塊。
注意:檔案系統中的所有空間使用情況和 inode 索引必須一致才能安全地使用臨時檔案! 此依賴關係是線上修復只能使用可分頁核心記憶體來暫存 ondisk 空間使用情況資訊的原因。
使用臨時檔案交換元資料檔案對映要求塊標頭的 owner 欄位與正在修復的檔案匹配,而不是與臨時檔案匹配。 目錄、擴充套件屬性和符號連結函式都經過修改,允許呼叫者顯式指定所有者編號。
收割過程存在一個缺點——如果在收割階段系統崩潰並且分支 extent 是交叉連結的,則 iunlink 處理將失敗,因為釋放空間會找到額外的反向對映並中止。
為修復建立的臨時檔案類似於使用者空間建立的 O_TMPFILE 檔案。 它們未連結到目錄中,並且當對該檔案的最後一個引用丟失時,將收割整個檔案。 主要區別在於,這些檔案必須完全沒有核心外部的訪問許可權,它們必須被特別標記以防止被控制代碼開啟,並且它們絕不能連結到目錄樹中。
歷史側邊欄: |
在檔案元資料修復的初始迭代中,將掃描損壞的元資料塊以查詢可挽救的資料; 將收割檔案分支中的 extent; 然後將在其位置構建新的結構。 該策略並未在本文件前面表達的原子修復要求的引入中倖存下來。
第二次迭代探索了從挽救資料在分支的高偏移量處構建第二個結構,收割舊的 extent,並使用 COLLAPSE_RANGE 操作將新的 extent 滑動到位。
這有很多缺點
陣列結構是線性定址的,並且常規檔案系統程式碼庫沒有可以應用於記錄偏移量計算以構建備用副本的線性偏移量的概念。
擴充套件屬性允許使用整個 attr 分支偏移地址空間。
即使修復可以在分支地址空間的不同部分構建資料結構的備用副本,原子修復提交要求也意味著線上修復必須能夠執行日誌輔助 COLLAPSE_RANGE 操作以確保舊結構被完全替換。
在構建輔助樹之後但在範圍摺疊之前崩潰將導致檔案分支中存在無法訪問的塊。 這可能會進一步混淆事情。
修復後收割塊不是一個簡單的操作,並且在日誌恢復期間從重新啟動的範圍摺疊操作啟動收割操作令人望而卻步。
目錄條目塊和配額記錄在每個塊的標頭區域中記錄檔案分支偏移量。 原子範圍摺疊操作必須重寫每個塊標頭的這一部分。 重寫塊標頭中的單個欄位不是一個大問題,但需要注意。
目錄或擴充套件屬性 btree 索引中的每個塊都包含同級塊和子塊指標。 如果原子提交要使用範圍摺疊操作,則必須非常小心地重寫每個塊以保留圖形結構。 作為範圍摺疊的一部分執行此操作意味著重複重寫大量塊,這不利於快速修復。
這導致引入了臨時檔案暫存。
|
線上修復程式碼應使用 xrep_tempfile_create 函式在檔案系統內建立臨時檔案。 這會分配一個 inode,將 incore inode 標記為私有,並將其附加到 scrub 上下文。 這些檔案對使用者空間隱藏,可能不會新增到目錄樹中,並且必須保持私有。
臨時檔案僅使用兩個 inode 鎖:IOLOCK 和 ILOCK。 此處不需要 MMAPLOCK,因為資料分支塊的使用者空間不得存在頁面錯誤。 這兩個鎖的使用模式與任何其他 XFS 檔案相同——對檔案資料的訪問透過 IOLOCK 控制,對檔案元資料的訪問透過 ILOCK 控制。 提供了鎖定幫助程式,以便 scrub 上下文可以清理臨時檔案及其鎖定狀態。 為了符合inode 鎖定部分中規定的巢狀鎖定策略,建議 scrub 函式使用 xrep_tempfile_ilock*_nowait 鎖定幫助程式。
可以透過兩種方式將資料寫入臨時檔案
xrep_tempfile_copyin 可用於從 xfile 設定常規臨時檔案的內容。
常規目錄、符號連結和擴充套件屬性函式可用於寫入臨時檔案。
一旦在臨時檔案中構建了資料檔案的良好副本,就必須將其傳送到正在修復的檔案,這是下一節的主題。
提議的補丁位於 修復臨時檔案 系列中。
一旦修復構建了臨時檔案並將新的資料結構寫入其中,它必須將新的更改提交到現有檔案中。 無法交換兩個檔案的 inumber,因此必須替換新的元資料來代替舊的元資料。 這表明需要能夠交換 extent,但是檔案碎片整理工具 xfs_fsr 使用的現有 extent 交換程式碼不足以進行線上修復,因為
啟用反向對映 btree 後,交換程式碼必須透過每次對映交換使反向對映資訊保持最新。 因此,它只能在每個事務中交換一個對映,並且每個事務都是獨立的。
反向對映對於線上 fsck 的操作至關重要,因此舊的碎片整理程式碼(它在單個操作中交換了整個 extent 分支)在此處沒有用處。
假定碎片整理發生在兩個內容相同的檔案之間。 對於此用例,即使操作中斷,不完整的交換也不會導致使用者可見的檔案內容更改。
線上修復需要交換定義為不相同的兩個檔案的內容。 對於目錄和 xattr 修復,使用者可見的內容可能相同,但是各個塊的內容可能非常不同。
檔案中的舊塊可能與其他結構交叉連結,並且如果系統在修復過程中關閉,則不得重新出現。
這些問題透過建立一個新的延遲操作和一種新的日誌意圖項來解決,該日誌意圖項用於跟蹤交換兩個檔案範圍的操作的進度。 新的交換操作型別將反向對映 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 函式以釋放該功能。 在日誌變得乾淨之前,不會從超級塊中清除該功能位。
日誌輔助擴充套件屬性更新和檔案內容交換都使用日誌不相容功能並提供圍繞該功能的便捷包裝程式。
|
在檔案分支之間交換內容是一項複雜的任務。 目標是在兩個檔案分支偏移範圍之間交換所有檔案分支對映。 每個分支中都可能存在許多 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 欄位用於在操作結束時交換檔案大小。
當啟動交換時,操作順序如下所示
為檔案對映交換建立一個延遲工作項。 首先,它應該包含要交換的整個檔案塊範圍。
呼叫 xfs_defer_finish 以處理交換。 這封裝在用於 scrub 操作的 xrep_tempexch_contents 中。 這會將 extent 交換意圖項記錄到延遲對映交換工作項的事務中。
直到延遲對映交換工作項的 xmi_blockcount 為零,
分別讀取從 xmi_startoff1 和 xmi_startoff2 開始的兩個檔案範圍的塊對映,並計算可以在單個步驟中交換的最長 extent。 這是對映中兩個 br_blockcount s 中的最小值。 繼續在檔案分支中前進,直到至少一個對映包含已寫入的塊。 不會交換相互的空洞、未寫入的 extent 和到同一物理空間的 extent 對映。
對於接下來的幾個步驟,本文件會將來自檔案 1 的對映稱為“map1”,並將來自檔案 2 的對映稱為“map2”。
建立一個延遲塊對映更新以從檔案 1 中取消對映 map1。
建立一個延遲塊對映更新以從檔案 2 中取消對映 map2。
建立一個延遲塊對映更新以將 map1 對映到檔案 2 中。
建立一個延遲塊對映更新以將 map2 對映到檔案 1 中。
記錄兩個檔案的塊、配額和 extent 計數更新。
如有必要,擴充套件任一檔案的 ondisk 大小。
為在步驟 3 開始時讀取的對映交換意圖日誌項記錄對映交換完成日誌項。
計算剛剛覆蓋的檔案範圍量。 此數量為 (map1.br_startoff + map1.br_blockcount - xmi_startoff1),因為步驟 3a 可能跳過了空洞。
將 xmi_startoff1 和 xmi_startoff2 的起始偏移量增加上一步中計算的塊數,並將 xmi_blockcount 減少相同的數量。 這會前進游標。
記錄一個新的對映交換意圖日誌項,該日誌項反映了工作項的推進狀態。
將正確的錯誤程式碼 (EAGAIN) 返回給延遲操作管理器,以告知它還有更多工作要做。 操作管理器在返回到步驟 3 的開頭之前完成步驟 3b-3e 中的延遲工作。
執行任何後處理。 將在後續章節中更詳細地討論這一點。
如果檔案系統在操作過程中關閉,則日誌恢復將找到最新的未完成對映交換日誌意圖項並從那裡重新啟動。 這就是原子檔案對映交換如何保證外部觀察者要麼看到舊的損壞結構,要麼看到新的結構,而永遠不會看到兩者的混合。
在啟動原子檔案對映交換操作之前,需要處理一些事情。 首先,常規檔案需要先將頁面快取重新整理到磁碟,然後才能開始操作,並且直接 I/O 寫入要靜止。 與任何檔案系統操作一樣,檔案對映交換必須確定可以在操作中代表兩個檔案消耗的最大磁碟空間和配額量,並預留該資源量以避免一旦開始弄髒元資料就發生無法恢復的空間不足故障。 準備步驟會掃描兩個檔案的範圍以估計
精確估計的需求增加了交換操作的執行時間,但是維護正確的帳戶非常重要。 檔案系統絕不能完全耗盡可用空間,並且對映交換永遠不能向分支新增比它可以支援的 extent 對映更多。 常規使用者需要遵守配額限制,儘管元資料修復可能會超過配額以解決其他地方的不一致元資料。
擴充套件屬性、符號連結和目錄可以將 fork 格式設定為“local”,並將 fork 視為資料儲存的字面區域。元資料修復必須採取額外的步驟來支援這些情況。
如果兩個 fork 都採用 local 格式,並且 fork 區域足夠大,則交換透過複製記憶體中的 fork 內容、記錄兩個 fork 並提交來執行。由於可以使用單個事務完成此操作,因此不需要原子檔案對映交換機制。
如果兩個 fork 都對映塊,則使用常規的原子檔案對映交換。
否則,只有一個 fork 採用 local 格式。local 格式 fork 的內容將被轉換為塊以執行交換。轉換為塊格式必須在記錄初始對映交換意圖日誌項的同一事務中完成。常規的原子對映交換用於交換元資料檔案對映。在交換操作上設定特殊標誌,以便事務可以再次回滾以將第二個檔案的 fork 轉換回 local 格式,以便第二個檔案在 ILOCK 釋放後即可使用。
擴充套件屬性和目錄將擁有的 inode 蓋印到每個塊中,但緩衝區驗證器實際上並不檢查 inode 編號!雖然沒有驗證,但維護引用完整性仍然很重要,因此在執行對映交換之前,線上修復會使用正在修復的檔案的所有者欄位構建新資料結構中的每個塊。
在成功完成交換操作後,修復操作必須透過處理每個 fork 對映透過標準檔案範圍回收機制來回收舊的 fork 塊,該機制在修復後執行。如果檔案系統在修復的回收部分期間崩潰,則恢復結束時的 iunlink 處理將釋放臨時檔案和未回收的任何塊。但是,此 iunlink 處理省略了線上修復的交叉連結檢測,並且並非完全可靠。
要修復元資料檔案,線上修復按如下步驟進行:
建立一個臨時修復檔案。
使用暫存資料將新內容寫入臨時修復檔案。必須寫入與正在修復的相同的 fork。
提交清理事務,因為交換資源估計步驟必須在進行事務預留之前完成。
呼叫 xrep_tempexch_trans_alloc 以分配具有適當資源預留和鎖的新清理事務,並使用交換操作的詳細資訊填充 struct xfs_exchmaps_req。
呼叫 xrep_tempexch_contents 以交換內容。
提交事務以完成修復。
在 XFS 檔案系統的“即時”部分中,空閒空間透過點陣圖進行跟蹤,類似於 Unix FFS。點陣圖中的每個位代表一個即時範圍,範圍的大小是檔案系統塊大小的倍數,介於 4KiB 和 1GiB 之間。即時摘要檔案將給定大小的空閒範圍的數量索引到即時空閒空間點陣圖中這些空閒範圍開始的塊的偏移量。換句話說,摘要檔案透過長度幫助分配器查詢空閒範圍,類似於空閒空間計數 (cntbt) btree 對資料部分所做的事情。
摘要檔案本身是一個平面檔案(沒有塊標頭或校驗和!),分為 log2(總 rt 範圍) 部分,其中包含足夠的 32 位計數器來匹配 rt 點陣圖中的塊數。每個計數器記錄從該點陣圖塊開始且可以滿足 2 的冪分配請求的空閒範圍的數量。
要針對點陣圖檢查摘要檔案:
獲取即時點陣圖和摘要檔案的 ILOCK。
對於點陣圖中記錄的每個空閒空間範圍:
計算摘要檔案中包含代表此空閒範圍的計數器的位置。
從 xfile 中讀取計數器。
遞增計數器,然後將其寫回 xfile。
將 xfile 的內容與磁碟上的檔案進行比較。
要修復摘要檔案,請將 xfile 內容寫入臨時檔案,並使用原子對映交換來提交新內容。然後回收臨時檔案。
建議的補丁集是 即時摘要修復系列。
在 XFS 中,擴充套件屬性被實現為名稱空間的名稱-值儲存。值的大小限制為 64KiB,但名稱的數量沒有限制。屬性 fork 未分割槽,這意味著屬性結構的根始終位於邏輯塊零中,但屬性葉塊、dabtree 索引塊和遠端值塊是混合的。屬性葉塊包含可變大小的記錄,這些記錄將使用者提供的名稱與使用者提供的值相關聯。大於一個塊的值被分配單獨的範圍並寫入那裡。如果葉資訊擴充套件到單個塊之外,則會建立一個目錄/屬性 btree (dabtree) 以將屬性名稱的雜湊對映到條目以進行快速查詢。
搶救擴充套件屬性按如下步驟進行:
遍歷正在修復的檔案的 attr fork 對映以查詢屬性葉塊。當找到一個時:
遍歷 attr 葉塊以查詢候選鍵。當找到一個時:
檢查名稱是否存在問題,如果存在問題則忽略該名稱。
檢索值。如果成功,則將名稱和值新增到暫存 xfarray 和 xfblob。
如果 xfarray 和 xfblob 的記憶體使用量超過一定量的記憶體,或者沒有更多 attr fork 塊要檢查,則解鎖該檔案並將暫存的擴充套件屬性新增到臨時檔案。
使用原子檔案對映交換來交換新的和舊的擴充套件屬性結構。舊的屬性塊現在已附加到臨時檔案。
回收臨時檔案。
建議的補丁集是 擴充套件屬性修復系列。
使用當前可用的檔案系統功能修復目錄很困難,因為目錄條目不是冗餘的。離線修復工具會掃描所有 inode 以查詢連結計數不為零的檔案,然後掃描所有目錄以建立這些連結檔案的父子關係。損壞的檔案和目錄將被刪除,沒有父檔案的檔案將被移動到 /lost+found 目錄。它不會嘗試搶救任何東西。
線上修復目前能做的最好的事情是讀取目錄資料塊並搶救任何看起來合理的 dirent,糾正連結計數,並將孤兒移回目錄樹。檔案連結計數 fsck程式碼負責修復連結計數並將孤兒移動到 /lost+found 目錄。
與擴充套件屬性不同,目錄塊的大小都相同,因此搶救目錄很簡單:
查詢目錄的父目錄。如果點點條目不可讀,請嘗試確認聲稱的父目錄是否具有指向正在修復的目錄的子條目。否則,遍歷檔案系統以找到它。
遍歷目錄資料 fork 的第一個分割槽以查詢目錄條目資料塊。當找到一個時:
遍歷目錄資料塊以查詢候選條目。當找到一個條目時:
檢查名稱是否存在問題,如果存在問題則忽略該名稱。
檢索 inumber 並獲取 inode。如果成功,則將名稱、inode 編號和檔案型別新增到暫存 xfarray 和 xblob。
如果 xfarray 和 xfblob 的記憶體使用量超過一定量的記憶體,或者沒有更多目錄資料塊要檢查,則解鎖該目錄並將暫存的 dirent 新增到臨時目錄。截斷暫存檔案。
使用原子檔案對映交換來交換新的和舊的目錄結構。舊的目錄塊現在已附加到臨時檔案。
回收臨時檔案。
未來工作問題:在重建目錄時,修復是否應該重新驗證 dentry 快取?
答案:是的,應該。
從理論上講,有必要掃描目錄的所有 dentry 快取條目,以確保以下情況之一適用:
快取的 dentry 反映了新目錄中的磁碟上 dirent。
快取的 dentry 在新目錄中不再具有相應的磁碟上 dirent,並且可以從快取中清除該 dentry。
快取的 dentry 不再具有磁碟上的 dirent,但無法清除該 dentry。這是問題所在。
不幸的是,當前 dentry 快取設計沒有提供遍歷特定目錄的每個子 dentry 的方法,這使得這成為一個難題。目前沒有已知的解決方案。
建議的補丁集是目錄修復系列。
父指標是一段檔案元資料,使使用者無需從根目錄遍歷目錄樹即可找到檔案的父目錄。如果沒有它們,目錄樹的重建將受到阻礙,就像過去缺乏反向空間對映資訊阻礙了檔案系統空間元資料的重建一樣。但是,父指標功能使完全目錄重建成為可能。
XFS 父指標包含識別父目錄中相應目錄條目所需的資訊。換句話說,子檔案使用擴充套件屬性來儲存指向父目錄的指標,格式為 (dirent_name) → (parent_inum, parent_gen)。可以加強目錄檢查過程,以確保每個 dirent 的目標還包含指向 dirent 的父指標。同樣,可以透過確保每個父指標的目標都是目錄並且包含與父指標匹配的 dirent 來檢查每個父指標。線上和離線修復都可以使用此策略。
歷史側邊欄: |
目錄父指標最初由 SGI 在十多年前作為 XFS 功能提出。從父目錄到子檔案的每個連結都使用子目錄中的擴充套件屬性進行映象,該屬性可用於識別父目錄。不幸的是,這種早期實現存在重大缺陷,從未合併到 Linux XFS 中。
2000 年代末的 XFS 程式碼庫沒有強制執行目錄樹中強引用完整性的基礎結構。它不能保證前向連結中的更改始終會透過對反向連結的相應更改來跟進。
引用完整性未整合到離線修復中。在未掛載的檔案系統上執行檢查和修復,而不使用任何核心或 inode 鎖來協調訪問。目前尚不清楚這實際上是如何正常工作的。
擴充套件屬性未記錄父目錄中目錄條目的名稱,因此 SGI 父指標實現不能用於重新連線目錄樹。
擴充套件屬性 fork 僅支援 65,536 個範圍,這意味著父指標屬性的建立很可能在達到最大檔案連結計數之前失敗。
原始父指標設計對於依賴於檔案系統修復之類的事情來說太不穩定了。Allison Henderson、Chandan Babu 和 Catherine Hoang 正在進行第二種實現,該實現解決了第一種實現的所有缺點。在 2022 年期間,Allison 引入了日誌意圖項來跟蹤擴充套件屬性結構的物理操作。這透過使在同一事務中提交 dirent 更新和父指標更新成為可能來解決了引用完整性問題。Chandan 增加了資料和屬性 fork 的最大範圍計數,從而確保擴充套件屬性結構可以增長以處理任何檔案的最大硬連結計數。
對於第二次嘗試,最初提出的磁碟上父指標格式為 (parent_inum, parent_gen, dirent_pos) → (dirent_name)。在開發過程中更改了格式,以消除修復工具需要確保在重建目錄時 dirent_pos 欄位始終匹配的要求。
還有一些其他方法可以解決該問題:
該欄位可以被指定為建議性的,因為其他三個值足以在父目錄中找到該條目。但是,這使得在修復正在進行時無法進行索引鍵查詢。
我們可以允許在指定的偏移量處建立目錄條目,這解決了引用完整性問題,但存在 dirent 建立將由於與目錄中的空閒空間衝突而失敗的風險。
可以透過追加目錄條目並修改 xattr 程式碼以支援更新 xattr 鍵並重新索引 dabtree 來解決這些衝突,儘管這必須在父目錄仍然鎖定的情況下執行。
與上述相同,但原子地刪除舊的父指標條目並新增新的父指標條目。
將磁碟上 xattr 格式更改為 (parent_inum, name) → (parent_gen),這將提供我們所需的 attr 名稱唯一性,而無需強制修復程式碼更新 dirent 位置。不幸的是,這需要更改 xattr 程式碼以支援長達 263 位元組的 attr 名稱。
將磁碟上 xattr 格式更改為 (parent_inum, hash(name)) → (name, parent_gen)。如果雜湊具有足夠的抗衝突性(例如,sha256),那麼這應該提供我們所需的 attr 名稱唯一性。小於 247 位元組的名稱可以直接儲存。
將磁碟上 xattr 格式更改為 (dirent_name) → (parent_ino, parent_gen)。此格式不需要前面建議的任何複雜的巢狀名稱雜湊。但是,發現具有相同檔名的相同 inode 的多個硬連結會導致雜湊 xattr 查找出現效能問題,因此父 inumber 現在已 xor'd 到雜湊索引中。
最後,決定解決方案 #6 是最緊湊和最有效的。為父指標設計了一個新的雜湊函式。
|
目錄重建使用協調的 inode 掃描和目錄條目即時更新掛鉤,如下所示:
設定一個臨時目錄來生成新的目錄結構,一個 xfblob 來儲存條目名稱,以及一個 xfarray 來儲存目錄更新中涉及的固定大小欄位:(子 inumber, 新增 與 刪除, 名稱 cookie, ftype)。
設定一個 inode 掃描器並掛接到目錄條目程式碼中以接收目錄操作的更新。
對於在掃描的每個檔案中找到的每個父指標,確定父指標是否引用感興趣的目錄。如果是:
分別將父指標名稱和此 dirent 的 addname 條目儲存在 xfblob 和 xfarray 中。
完成掃描該檔案或核心記憶體消耗超過閾值時,將儲存的更新重新整理到臨時目錄。
對於透過掛鉤收到的每個即時目錄更新,確定是否已掃描該子目錄。如果是:
稍後將父指標名稱和此 dirent 更新的 addname 或 removename 條目儲存在 xfblob 和 xfarray 中。我們無法直接寫入臨時目錄,因為不允許掛鉤函式修改檔案系統元資料。相反,我們將更新儲存在 xfarray 中,並依靠掃描器執行緒將儲存的更新應用於臨時目錄。
掃描完成後,重播 xfarray 中的任何儲存條目。
掃描完成後,原子地交換臨時目錄和正在修復的目錄的內容。臨時目錄現在包含損壞的目錄結構。
回收臨時目錄。
建議的補丁集是 父指標目錄修復系列。
檔案父指標資訊的線上重建與目錄重建類似:
設定一個臨時檔案來生成新的擴充套件屬性結構,一個 xfblob 來儲存父指標名稱,以及一個 xfarray 來儲存父指標更新中涉及的固定大小欄位:(父 inumber, 父 生成, 新增 與 刪除, 名稱 cookie)。
設定一個 inode 掃描器並掛接到目錄條目程式碼中以接收目錄操作的更新。
對於在掃描的每個目錄中找到的每個目錄條目,確定 dirent 是否引用感興趣的檔案。如果是:
分別將 dirent 名稱和此父指標的 addpptr 條目儲存在 xfblob 和 xfarray 中。
完成掃描目錄或核心記憶體消耗超過閾值時,將儲存的更新重新整理到臨時檔案。
對於透過掛鉤收到的每個即時目錄更新,確定是否已掃描父目錄。如果是:
稍後將 dirent 名稱和此 dirent 更新的 addpptr 或 removepptr 條目儲存在 xfblob 和 xfarray 中。我們無法直接將父指標寫入臨時檔案,因為不允許掛鉤函式修改檔案系統元資料。相反,我們將更新儲存在 xfarray 中,並依靠掃描器執行緒將儲存的父指標更新應用於臨時檔案。
掃描完成後,重播 xfarray 中的任何儲存條目。
將所有非父指標擴充套件屬性複製到臨時檔案。
掃描完成後,原子地交換臨時檔案和正在修復的檔案屬性 fork 的對映。臨時檔案現在包含損壞的擴充套件屬性結構。
回收臨時檔案。
建議的補丁集是 父指標修復系列。
離線修復中檢查父指標的工作方式不同,因為損壞的檔案在執行目錄樹連線檢查之前很久就被擦除了。因此,父指標檢查是新增到現有連線檢查的第二遍。
在建立倖存檔案集(第 6 階段)之後,遍歷檔案系統中每個 AG 的倖存目錄。這已經作為連線檢查的一部分執行。
對於找到的每個目錄條目:
如果該名稱已儲存在 xfblob 中,則使用該 cookie 並跳過下一步。
否則,將該名稱記錄在 xfblob 中,並記住 xfblob cookie。唯一的對映對於以下方面至關重要:
重複資料刪除名稱以減少記憶體使用量,以及
為父指標索引建立一個穩定的排序鍵,以便下面描述的父指標驗證可以工作。
在每個 AG 的記憶體 Slab 中儲存 (child_ag_inum, parent_inum, parent_gen, name_hash, name_len, name_cookie) 元組。本節中引用的 name_hash 是常規目錄條目名稱雜湊,而不是用於父指標 xattr 的專用雜湊。
對於檔案系統中的每個 AG:
按 child_ag_inum、parent_inum、name_hash 和 name_cookie 的順序對每個 AG 的元組集進行排序。為每個 name 提供單個 name_cookie 對於處理包含同一檔案的多個硬連結(其中所有名稱都雜湊到相同值)的不常見情況至關重要。
對於 AG 中的每個 inode:
掃描 inode 中的父指標。對於找到的每個父指標:
驗證磁碟上的父指標。如果驗證失敗,請繼續處理檔案中的下一個父指標。
如果該名稱已儲存在 xfblob 中,則使用該 cookie 並跳過下一步。
在每個檔案的 xfblob 中記錄該名稱,並記住 xfblob cookie。
在每個檔案的 Slab 中儲存 (parent_inum, parent_gen, name_hash, name_len, name_cookie) 元組。
按 parent_inum、name_hash 和 name_cookie 的順序對每個檔案的元組進行排序。
將一個 Slab 游標定位在每個 AG 元組 Slab 中 inode 記錄的開頭。由於每個 AG 元組按子 inumber 排序,因此這應該是微不足道的。
將第二個 Slab 游標定位在每個檔案元組 Slab 的開頭。
以鎖定步驟迭代兩個游標,比較每個游標下的記錄的 parent_ino、name_hash 和 name_cookie 欄位:
如果每個 AG 游標在鍵空間中的位置低於每個檔案游標,則每個 AG 游標指向缺失的父指標。將父指標新增到 inode 並前進每個 AG 游標。
如果每個檔案游標在鍵空間中的位置低於每個 AG 游標,則每個檔案游標指向懸空的父指標。從 inode 中刪除父指標並前進每個檔案游標。
否則,兩個游標都指向相同的父指標。如有必要,更新 parent_gen 元件。前進兩個游標。
繼續檢查連結計數,就像我們今天所做的那樣。
建議的補丁集是 離線父指標修復系列。
從離線修復中的父指標重建目錄將非常具有挑戰性,因為 xfs_repair 當前在第 3 階段和第 4 階段使用檔案系統的兩次單遍掃描來確定哪些檔案損壞到足以被刪除。必須將此掃描轉換為多遍掃描:
掃描的第一遍會像現在一樣刪除損壞的 inode、fork 和屬性。損壞的目錄將被記錄,但不會被刪除。
下一遍記錄指向在第一遍中被記錄為損壞的目錄的父指標。如果第 4 階段也能夠刪除目錄,則此第二遍可能必須在第 4 階段掃描重複塊之後進行。
第三遍將損壞的目錄重置為空的短格式目錄。尚未確保空閒空間元資料,因此修復還不能使用 libxfs 中的目錄構建程式碼。
在第 6 階段開始時,空間元資料已被重建。使用在步驟 2 中記錄的父指標資訊重建 dirent 並將其新增到現在為空的目錄中。
尚未構建此程式碼。
如前所述,檔案系統目錄樹應該是一個有向無環圖結構。但是,此圖中的每個節點都是一個單獨的 xfs_inode 物件,具有自己的鎖,這使得驗證樹的質量很困難。幸運的是,非目錄允許具有多個父目錄,並且不能有子目錄,因此只需要掃描目錄。目錄通常構成檔案系統中文件的 5-10%,這大大減少了工作量。
如果可以凍結目錄樹,則可以透過從根目錄向下執行深度(或廣度)優先搜尋併為找到的每個目錄標記點陣圖來輕鬆發現迴圈和斷開連線的區域。在遍歷中的任何時候,嘗試設定已設定的位意味著存在一個迴圈。掃描完成後,將標記的 inode 點陣圖與 inode 分配點陣圖進行異或運算會顯示斷開連線的 inode。但是,線上修復的設計目標之一是避免鎖定整個檔案系統,除非絕對必要。目錄樹更新可以在即時檔案系統上跨掃描器波前移動子樹,因此無法應用點陣圖演算法。
目錄父指標可以對樹結構進行增量驗證。多個執行緒可以從單個子目錄向上移動到根目錄,而不是使用一個執行緒來掃描整個檔案系統。為此,所有目錄條目和父指標必須在內部一致,每個目錄條目必須具有父指標,並且所有目錄的連結計數必須正確。每個掃描器執行緒必須能夠在保持子目錄的 IOLOCK 的同時獲取聲稱的父目錄的 IOLOCK,以防止任一目錄在樹中移動。這是不可能的,因為 VFS 在移動子目錄時不會獲取子目錄的 IOLOCK,因此掃描器會透過獲取 ILOCK 並安裝 dirent 更新掛鉤來檢測更改來穩定父 -> 子關係。
掃描過程使用 dirent 掛鉤來檢測對掃描資料中提到的目錄的更改。掃描工作原理如下:
對於檔案系統中的每個子目錄:
對於該子目錄的每個父指標:
為該父指標建立一個路徑物件,並在路徑物件的點陣圖中標記子目錄 inode 編號。
在路徑結構中記錄父指標名稱和 inode 編號。
如果聲稱的父目錄是正在清理的子目錄,則該路徑是一個迴圈。標記要刪除的路徑,然後使用下一個子目錄父指標重複步驟 1a。
嘗試在路徑物件中的點陣圖中標記聲稱的父 inode 編號。如果已設定該位,則目錄樹中存在一個迴圈。將該路徑標記為一個迴圈,然後使用下一個子目錄父指標重複步驟 1a。
載入聲稱的父目錄。如果聲稱的父目錄不是連結目錄,則中止掃描,因為父指標資訊不一致。
對於此聲稱的祖先目錄的每個父指標:
如果未為該級別設定任何父目錄,則在路徑物件中記錄父指標名稱和 inode 編號。
如果一個祖先有多個父目錄,則將該路徑標記為損壞。使用下一個子目錄父指標重複步驟 1a。
對在步驟 1a6a 中標識的祖先重複步驟 1a3-1a6。重複此操作,直到到達目錄樹根目錄或未找到任何父目錄。
如果遍歷在根目錄處終止,則將該路徑標記為 ok。
如果遍歷在到達根目錄之前終止,則將該路徑標記為斷開連線。
如果目錄條目更新鉤子觸發,則檢查掃描已找到的所有路徑。如果條目匹配路徑的一部分,則將該路徑和掃描標記為過時。當掃描器執行緒看到掃描已被標記為過時時,它會刪除所有掃描資料並重新開始。
修復目錄樹的工作方式如下
遍歷目標子目錄的每個路徑。
損壞的路徑和迴圈路徑被認為是可疑的。
已標記為刪除的路徑被認為是壞的。
到達根目錄的路徑被認為是好的。
如果子目錄是根目錄或連結計數為零,則刪除直接父目錄中的所有傳入目錄條目。修復完成。
如果子目錄只有一個路徑,則將 dotdot 條目設定為父目錄並退出。
如果子目錄至少有一個好的路徑,則刪除直接父目錄中的所有其他傳入目錄條目。
如果子目錄沒有好的路徑且有多個可疑路徑,則刪除直接父目錄中的所有其他傳入目錄條目。
如果子目錄沒有路徑,則將其附加到 lost and found 目錄。
提議的補丁位於目錄樹修復系列中。
檔案系統將檔案表示為有向的,並希望是無環的圖。換句話說,是一棵樹。檔案系統的根目錄是一個目錄,目錄中的每個條目都向下指向更多的子目錄或非目錄檔案。不幸的是,目錄圖指標的中斷會導致斷開連線的圖,這使得無法透過常規路徑解析訪問檔案。
如果沒有父指標,目錄父指標線上清理程式碼可以檢測到指向沒有連結回到子目錄的父目錄的 dotdot 條目,並且檔案連結計數檢查器可以檢測到檔案系統中沒有任何目錄指向的檔案。如果這樣的檔案具有正連結計數,則該檔案是一個孤兒。
使用父指標,可以透過掃描父指標來重建目錄,也可以透過掃描目錄來重建父指標。這應該減少檔案最終出現在 /lost+found 中的情況。
找到孤兒時,應將其重新連線到目錄樹。離線 fsck 透過建立一個目錄 /lost+found 來作為孤兒院來解決這個問題,並使用 inode 號作為名稱將孤兒檔案連結到孤兒院中。將檔案重新指定給孤兒院不會重置其任何許可權或 ACL。
此過程在核心中比在使用者空間中更復雜。目錄和檔案連結計數修復設定函式必須使用常規 VFS 機制來建立具有所有必要安全屬性和 dentry 快取條目的孤兒院目錄,就像常規目錄樹修改一樣。
孤立檔案按以下方式被孤兒院收養
在清理設定函式的開頭呼叫 xrep_orphanage_try_create,以嘗試確保丟失和找到的目錄實際存在。這還將孤兒院目錄附加到清理上下文中。
如果決定重新連線檔案,則獲取孤兒院和正在重新附加的檔案的 IOLOCK。xrep_orphanage_iolock_two 函式遵循前面討論的 inode 鎖定策略。
使用 xrep_adoption_trans_alloc 來為修復事務保留資源。
呼叫 xrep_orphanage_compute_name 以計算孤兒院中的新名稱。
如果收養即將發生,請呼叫 xrep_adoption_reparent 以將孤立的檔案重新指定到 lost and found 目錄中,並使 dentry 快取無效。
呼叫 xrep_adoption_finish 以提交任何檔案系統更新,釋放孤兒院 ILOCK,並清除清理事務。呼叫 xrep_adoption_commit 以提交更新和清理事務。
如果發生執行時錯誤,請呼叫 xrep_adoption_cancel 以釋放所有資源。
提議的補丁位於 orphanage adoption 系列中。
本節討論使用者空間程式 xfs_scrub 的關鍵演算法和資料結構,該程式提供在核心中驅動元資料檢查和修復、驗證檔案資料以及查詢其他潛在問題的能力。
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 掃描重新平衡系列。
在階段 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 安排的修復工作。修復過程如下
使用工作佇列和足夠的工作人員開始一輪修復,以使 CPU 保持使用者所需的繁忙狀態。
對於階段 2 排隊的每個修復項,
要求核心修復給定檔案系統物件的修復項中列出的所有內容。
記下核心是否在減少此物件所需的修復數量方面取得了任何進展。
如果該物件不再需要修復,請重新驗證與該物件關聯的所有元資料。如果重新驗證成功,請刪除修復項。否則,重新排隊該項以進行更多修復。
如果進行了任何修復,請跳回 1a 以重試所有階段 2 項。
對於階段 3 排隊的每個修復項,
要求核心修復給定檔案系統物件的修復項中列出的所有內容。
記下核心是否在減少此物件所需的修復數量方面取得了任何進展。
如果該物件不再需要修復,請重新驗證與該物件關聯的所有元資料。如果重新驗證成功,請刪除修復項。否則,重新排隊該項以進行更多修復。
如果進行了任何修復,請跳回 1c 以重試所有階段 3 項。
如果步驟 1 取得了任何型別的修復進展,請跳回步驟 1 以開始另一輪修復。
如果還有剩餘的專案需要修復,請再次按順序執行所有專案。如果修復不成功,請抱怨,因為這是修復任何內容的最後機會。
在階段 5 和 7 中遇到的損壞和不一致會立即修復。階段 6 報告的損壞檔案資料塊無法由檔案系統恢復。
提議的補丁集是 修復警告改進、修復資料依賴關係 和 物件跟蹤 的重構,以及 修復排程 改進系列。
如果 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 中報告給系統管理員。
希望本文件的讀者已經遵循了本文件中提出的設計,並且現在對 XFS 如何線上重建其元資料索引以及檔案系統使用者如何與該功能互動有一定的瞭解。儘管這項工作的範圍令人望而卻步,但希望本指南能讓程式碼讀者更容易理解已構建的內容、為誰構建以及為什麼構建。請隨時透過 XFS 郵件列表提出問題。
如前所述,原子檔案對映交換機制的第二個前端是一個新的 ioctl 呼叫,使用者空間程式可以使用該呼叫原子地提交對檔案的更新。這個前端已經發布供審查多年了,儘管對線上修復的必要改進以及缺乏客戶需求意味著該提案沒有被大力推動。
如前所述,XFS 長期以來都能夠交換檔案之間的區段,xfs_fsr 幾乎專門使用它來整理檔案。最早的形式是 fork 交換機制,透過交換每個 inode fork 的直接區域中的原始位元組,可以在兩個檔案之間交換資料 fork 的全部內容。當 XFS v5 附帶自描述元資料時,這種舊機制增加了一些日誌支援,以便在日誌恢復期間繼續重寫 BMBT 塊的所有者欄位。當反向對映 btree 稍後新增到 XFS 時,維護 fork 對映與反向對映索引的一致性的唯一方法是開發一種迭代機制,該機制使用延遲 bmap 和 rmap 操作一次交換一個對映。該機制與上述步驟 2-3 相同,除了新的跟蹤項,因為原子檔案對映交換機制是現有機制的迭代,而不是完全新穎的東西。對於檔案整理的狹窄情況,檔案內容必須相同,因此恢復保證並沒有多大收穫。
原子檔案內容交換比現有的 swapext 實現更靈活,因為它可以保證即使在崩潰後,呼叫者也永遠不會看到新舊內容的混合,並且它可以對兩個任意的檔案 fork 範圍進行操作。額外的靈活性實現了幾個新的用例
事務性檔案更新:與上述機制相同,但只有在原始檔案的內容沒有更改時,呼叫者才希望發生提交。要實現這一點,呼叫程序會在將其資料重新連結到臨時檔案之前,快照原始檔案的檔案修改和更改時間戳。當程式準備好提交更改時,它會將時間戳作為原子檔案對映交換系統呼叫的引數傳遞到核心中。只有在提供的時間戳與原始檔案匹配時,核心才會提交更改。提供了一個新的 ioctl(XFS_IOC_COMMIT_RANGE)來執行此操作。
模擬原子塊裝置寫入:匯出具有與檔案系統塊大小匹配的邏輯扇區大小的塊裝置,以強制所有寫入都與檔案系統塊大小對齊。將所有寫入暫存到臨時檔案,並在完成後,呼叫原子檔案對映交換系統呼叫,並使用一個標誌來指示應忽略臨時檔案中的空洞。這在軟體中模擬了原子裝置寫入,並且可以支援任意分散的寫入。
事實證明,前面提到的重構修復項是啟用向量化清理系統呼叫的催化劑。自 2018 年以來,在某些系統上,呼叫核心的成本顯著增加,以減輕推測執行攻擊的影響。這激勵程式作者儘可能少地進行系統呼叫,以減少執行路徑跨越安全邊界的次數。
使用向量化清理,使用者空間將檔案系統物件的標識、要針對該物件執行的清理型別列表以及所選清理型別之間的資料依賴關係的簡單表示形式推送到核心。核心會執行呼叫者的計劃,直到它遇到由於損壞而無法滿足的依賴關係,並告訴使用者空間完成了多少。希望 io_uring 將拾取足夠多的此功能,以便線上 fsck 可以使用它,而不是向 XFS 新增單獨的向量化清理系統呼叫。
相關的補丁集是 核心向量化清理 和 使用者空間向量化清理 系列。
線上 fsck 程式碼的一個嚴重缺點是它可以在核心中保持資源鎖的時間基本上是無限的。允許使用者空間向該程序傳送一個致命訊號,該訊號將導致 xfs_scrub 在它到達一個良好的停止點時退出,但是使用者空間無法向核心提供時間預算。鑑於清理程式碼庫具有檢測致命訊號的幫助程式,因此允許使用者空間為清理/修復操作指定超時並在超過預算時中止該操作不應該花費太多精力。但是,大多數修復函式都具有一個屬性,即一旦它們開始觸及磁碟上的元資料,該操作就無法乾淨地取消,此後 QoS 超時不再有用。
多年來,許多 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 defrag 和 userspace freespace defrag 系列。
移除檔案系統的末尾應該很簡單,只需疏散檔案系統末尾的資料和元資料,並將釋放的空間交給縮小程式碼。 這需要疏散檔案系統末尾的空間,這是對可用空間碎片整理的使用!