1. 庫設計

1.1. 簡介

iomap 是一個用於處理常見檔案操作的檔案系統庫。該庫分為兩層:

  1. 較低層提供檔案偏移量範圍的迭代器。此層嘗試從檔案系統獲取每個檔案範圍到儲存的對映,但儲存資訊並非必需。

  2. 較高層根據較低層迭代器提供的空間對映執行操作。

迭代可能涉及檔案邏輯偏移範圍到物理區段的對映,但儲存層資訊並非必需,例如用於遍歷快取的檔案資訊。該庫匯出了各種 API,用於實現檔案操作,例如:

  • 頁快取讀寫

  • 頁快取的 Folio 寫故障

  • 髒 folio 的回寫

  • 直接 I/O 讀寫

  • fsdax I/O 讀、寫、載入和儲存

  • FIEMAP

  • lseek SEEK_DATASEEK_HOLE

  • 交換檔案啟用

該庫最初來源於 XFS 曾經使用的檔案 I/O 路徑;現在它已擴充套件到覆蓋其他幾個操作。

1.2. 誰應該閱讀本文件?

本文件的目標讀者是檔案系統、儲存和頁快取程式設計師以及程式碼審查員。

如果您正在從事 PCI、機器架構或裝置驅動程式方面的工作,您很可能找錯了地方。

1.3. 它有何改進?

與將檔案 I/O 分解為小單元(通常是記憶體頁或塊)並基於該單元查詢空間對映的經典 Linux I/O 模型不同,iomap 模型要求檔案系統為給定檔案操作建立最大的空間對映,並在此基礎上啟動操作。這種策略提高了檔案系統對正在執行的操作大小的可見性,使其能夠在可能的情況下透過更大的空間分配來對抗碎片。更大的空間對映透過將對映函式呼叫的成本分攤到更多資料上,從而提高執行時效能。

從高層次來看,iomap 操作如下所示

  1. 對於操作範圍內的每個位元組...

    1. 透過 ->iomap_begin 獲取空間對映

    2. 對於每個子工作單元...

      1. 重新驗證對映,並在必要時返回到上述 (1)。目前只有頁快取操作需要這樣做。

      2. 執行工作

    3. 增加操作遊標

    4. 如有必要,透過 ->iomap_end 釋放對映

每個 iomap 操作將在下面詳細介紹。該庫之前已被一篇 LWN 文章KernelNewbies 頁面介紹過。

本文件的目標是簡要討論 iomap 的設計和功能,然後詳細介紹 iomap 提供的介面。如果您修改了 iomap,請更新此設計文件。

1.4. 檔案範圍迭代器

1.4.1. 定義

  • 緩衝區頭(buffer head):舊緩衝區快取的殘餘。

  • fsblock:檔案塊大小,也稱為 i_blocksize

  • i_rwsem:VFS struct inode 讀寫訊號量。程序以共享模式持有此訊號量以讀取檔案狀態和內容。某些檔案系統可能允許共享模式進行寫入。程序通常以獨佔模式持有此訊號量以更改檔案狀態和內容。

  • invalidate_lock:頁快取 struct address_space 讀寫訊號量,用於保護支援 EOF 以下 folio 穿孔的檔案系統,防止 folio 插入和移除。希望插入 folio 的程序必須以共享模式持有此鎖以防止移除,儘管允許併發插入。希望移除 folio 的程序必須以獨佔模式持有此鎖以防止插入。不允許併發移除。

  • dax_read_lock:dax 獲取的 RCU 讀鎖,用於防止裝置預關機鉤子在其他執行緒釋放資源之前返回。

  • 檔案系統對映鎖(filesystem mapping lock):此同步原語是檔案系統內部的,必須在取樣對映時保護檔案對映資料免受更新。檔案系統作者必須確定如何進行這種協調;它不一定是一個實際的鎖。

  • iomap 內部操作鎖(iomap internal operation lock):這是 iomap 函式在持有對映時獲取的同步原語的通用術語。一個具體的例子是在讀寫頁快取時獲取 folio 鎖。

  • 純覆蓋寫(pure overwrite):一種在提交或完成期間不需要任何元資料或清零操作的寫操作。這意味著檔案系統必須已經將磁碟空間分配為 IOMAP_MAPPED,並且檔案系統不得對 I/O 對齊或大小施加任何限制。I/O 對齊的唯一限制是裝置級別(最小 I/O 大小和對齊,通常是扇區大小)。

1.4.2. struct iomap

檔案系統透過以下結構將檔案的位元組範圍對映到儲存裝置的位元組範圍,並將其告知 iomap 迭代器:

struct iomap {
    u64                 addr;
    loff_t              offset;
    u64                 length;
    u16                 type;
    u16                 flags;
    struct block_device *bdev;
    struct dax_device   *dax_dev;
    void                *inline_data;
    void                *private;
    const struct iomap_folio_ops *folio_ops;
    u64                 validity_cookie;
};

欄位如下:

  • offsetlength 描述此對映覆蓋的檔案偏移量範圍(以位元組為單位)。這些欄位必須始終由檔案系統設定。

  • type 描述空間對映的型別:

    • IOMAP_HOLE:未分配儲存空間。此型別絕不能響應 IOMAP_WRITE 操作返回,因為寫入必須分配和對映空間,並返回對映。addr 欄位必須設定為 IOMAP_NULL_ADDR。iomap 不支援向空洞寫入(無論是透過頁快取還是直接 I/O)。

    • IOMAP_DELALLOC:承諾稍後分配空間(“延遲分配”)。如果檔案系統在此處返回 IOMAP_F_NEW 並且寫入失敗,->iomap_end 函式必須刪除預留。addr 欄位必須設定為 IOMAP_NULL_ADDR

    • IOMAP_MAPPED:檔案範圍對映到儲存裝置上的特定空間。裝置在 bdevdax_dev 中返回。裝置地址(以位元組為單位)透過 addr 返回。

    • IOMAP_UNWRITTEN:檔案範圍對映到儲存裝置上的特定空間,但該空間尚未初始化。裝置在 bdevdax_dev 中返回。裝置地址(以位元組為單位)透過 addr 返回。從此類對映讀取將向呼叫者返回零。對於寫入或回寫操作,ioend 應將對映更新為 MAPPED。有關詳細資訊,請參閱關於 ioend 的部分。

    • IOMAP_INLINE:檔案範圍對映到由 inline_data 指定的記憶體緩衝區。對於寫入操作,->iomap_end 函式大概會處理資料持久化。addr 欄位必須設定為 IOMAP_NULL_ADDR

  • flags 描述空間對映的狀態。這些標誌應由檔案系統在 ->iomap_begin 中設定:

    • IOMAP_F_NEW:對映下的空間是新分配的。未寫入的區域必須清零。如果寫入失敗且對映是空間預留,則必須刪除該預留。

    • IOMAP_F_DIRTY:inode 將包含訪問任何寫入資料所需未提交的元資料。fdatasync 需要提交這些更改到持久儲存。這需要考慮 I/O 完成時可能發生的元資料更改,例如直接 I/O 導致的檔案大小更新。

    • IOMAP_F_SHARED:對映下的空間是共享的。必須進行寫時複製以避免損壞其他檔案資料。

    • IOMAP_F_BUFFER_HEAD:此對映需要使用緩衝區頭進行頁快取操作。請勿增加此用途。

    • IOMAP_F_MERGED:多個連續塊對映被合併到此單個對映中。這僅對 FIEMAP 有用。

    • IOMAP_F_XATTR:此對映用於擴充套件屬性資料,而非常規檔案資料。這僅對 FIEMAP 有用。

    • IOMAP_F_BOUNDARY:這表示 I/O 及其完成不得與任何其他 I/O 或完成合並。檔案系統在向無法處理跨越某些 LBA(例如 ZNS 裝置)的 I/O 的裝置提交 I/O 時必須使用此標誌。此標誌僅適用於緩衝 I/O 回寫;所有其他函式都會忽略它。

    • IOMAP_F_PRIVATE:此標誌保留用於檔案系統私有用途。

    • IOMAP_F_ANON_WRITE:表示(寫入)I/O 尚未分配目標塊,檔案系統將在 bio 提交處理程式中完成此操作,並根據需要拆分 I/O。

    • IOMAP_F_ATOMIC_BIO:這表示寫入 I/O 必須在 bio 中設定 REQ_ATOMIC 標誌進行提交。檔案系統需要設定此標誌以通知 iomap 寫入 I/O 操作需要基於硬體解除安裝機制的撕裂防寫。它們還必須確保在 I/O 完成後,對映更新必須在單個元資料更新中執行。

    這些標誌可以由 iomap 在檔案操作期間自行設定。如果檔案系統需要觀察這些標誌,則應提供一個 ->iomap_end 函式:

    • IOMAP_F_SIZE_CHANGED:檔案大小因使用此對映而更改。

    • IOMAP_F_STALE:發現對映已過期。iomap 將對此對映呼叫 ->iomap_end,然後呼叫 ->iomap_begin 以獲取新對映。

    目前,這些標誌僅由頁快取操作設定。

  • addr 描述裝置地址,以位元組為單位。

  • bdev 描述此對映的塊裝置。僅在對映或未寫入操作時需要設定此項。

  • dax_dev 描述此對映的 DAX 裝置。僅在對映或未寫入操作時需要設定此項,並且僅適用於 fsdax 操作。

  • inline_data 指向一個記憶體緩衝區,用於涉及 IOMAP_INLINE 對映的 I/O。此值對於所有其他對映型別均被忽略。

  • private 是指向檔案系統私有資訊的指標。此值將保持不變地傳遞給 ->iomap_end

  • folio_ops 將在頁快取操作部分中介紹。

  • validity_cookie 是檔案系統設定的一個神奇的新鮮度值,應使用它來檢測過期的對映。對於頁快取操作,這對於正確操作至關重要,因為可能發生頁故障,這意味著在 ->iomap_begin->iomap_end 之間不應持有檔案系統鎖。具有完全靜態對映的檔案系統不需要設定此值。只有頁快取操作會重新驗證對映;有關詳細資訊,請參閱關於 iomap_valid 的部分。

1.4.3. struct iomap_ops

每個 iomap 函式都要求檔案系統傳遞一個操作結構以獲取對映並(可選地)釋放對映:

struct iomap_ops {
    int (*iomap_begin)(struct inode *inode, loff_t pos, loff_t length,
                       unsigned flags, struct iomap *iomap,
                       struct iomap *srcmap);

    int (*iomap_end)(struct inode *inode, loff_t pos, loff_t length,
                     ssize_t written, unsigned flags,
                     struct iomap *iomap);
};

1.4.3.1. ->iomap_begin

iomap 操作呼叫 ->iomap_begin,以獲取檔案 inode 指定的 poslength 位元組範圍的一個檔案對映。此對映應透過 iomap 指標返回。該對映必須至少覆蓋所提供檔案範圍的第一個位元組,但不需要覆蓋整個請求範圍。

每個 iomap 操作透過 flags 引數描述請求的操作。 flags 的確切值將在下面的操作特定部分中說明。這些標誌原則上可以普遍應用於 iomap 操作:

  • IOMAP_DIRECT 在呼叫者希望向塊儲存發出檔案 I/O 時設定。

  • IOMAP_DAX 在呼叫者希望向類記憶體儲存發出檔案 I/O 時設定。

  • IOMAP_NOWAIT 在呼叫者希望盡力避免任何導致提交任務阻塞的操作時設定。其意圖類似於網路 API 的 O_NONBLOCK - 它旨在讓非同步應用程式繼續執行其他工作,而不是等待特定的不可用檔案系統資源變為可用。實現 IOMAP_NOWAIT 語義的檔案系統需要使用 trylock 演算法。它們需要能夠用單個 iomap 對映滿足整個 I/O 請求範圍。它們需要避免同步讀寫元資料。它們需要避免阻塞記憶體分配。它們需要避免等待事務預留以允許修改發生。它們可能不應該分配新空間。等等。如果檔案系統開發者對任何特定的 IOMAP_NOWAIT 操作是否可能最終阻塞有任何疑問,那麼他們應該儘早返回 -EAGAIN,而不是啟動操作並強制提交任務阻塞。IOMAP_NOWAIT 通常代表 IOCB_NOWAITRWF_NOWAIT 進行設定。

  • IOMAP_DONTCACHE 在呼叫者希望執行緩衝檔案 I/O,並希望核心在 I/O 完成後丟棄頁快取(如果它未被其他執行緒使用)時設定。

如果有必要從不同裝置或裝置上的地址範圍讀取現有檔案內容,檔案系統應透過 srcmap 返回該資訊。只有頁快取和 fsdax 操作支援從一個對映讀取並寫入另一個對映。

1.4.3.2. ->iomap_end

操作完成後,如果存在,將呼叫 ->iomap_end 函式,以表示 iomap 已完成對對映的操作。通常,實現會使用此函式來拆除在 ->iomap_begin 中設定的任何上下文。例如,寫入可能希望提交對操作位元組的預留,並取消預留未操作的任何空間。written 如果沒有觸及任何位元組,可能為零。flags 將包含傳遞給 ->iomap_begin 的相同值。讀取的 iomap 操作不太可能需要提供此函式。

兩個函式在出錯時應返回負的 errno 程式碼,成功時返回零。

1.5. 檔案操作準備

iomap 僅處理對映和 I/O。檔案系統在啟動 I/O 操作之前,仍必須呼叫 VFS 來檢查輸入引數和檔案狀態。它不處理獲取檔案系統凍結保護、更新時間戳、剝奪許可權或訪問控制。

1.6. 鎖定層次結構

iomap 要求檔案系統提供自己的鎖定模型。就 iomap 而言,同步原語分為三類:

  • 上層原語由檔案系統提供,用於協調對不同 iomap 操作的訪問。確切的原語特定於檔案系統和操作,但通常是 VFS inode、頁快取失效或 folio 鎖。例如,檔案系統可能在呼叫 iomap_file_buffered_writeiomap_file_unshare 之前獲取 i_rwsem,以防止這兩個檔案操作相互覆蓋。頁快取回寫可能會鎖定 folio,以防止其他執行緒在回寫進行期間訪問 folio。

    • 下層原語由檔案系統在 ->iomap_begin->iomap_end 函式中獲取,用於協調對檔案空間對映資訊的訪問。iomap 物件的欄位應在此原語被持有時填充。上層同步原語(如果有)在獲取下層同步原語時仍被持有。例如,XFS 在取樣對映時獲取 ILOCK_EXCL,ext4 獲取 i_data_sem。具有不可變對映資訊的檔案系統可能不需要在此處進行同步。

    • 操作原語由 iomap 操作獲取,用於協調對其內部資料結構的訪問。上層同步原語(如果有)在獲取此原語時仍被持有。下層原語在獲取此原語時不被持有。例如,頁快取寫入操作將獲取檔案對映,然後抓取並鎖定 folio 以複製新內容。它也可能鎖定內部 folio 狀態物件以更新元資料。

確切的鎖定要求特定於檔案系統;對於某些操作,可以省略其中一些鎖。所有關於鎖定的進一步提及都是建議,而非強制。每個檔案系統作者都必須自行確定鎖定機制。

1.7. 缺陷和限制

  • 不支援 fscrypt。

  • 不支援壓縮。

  • 尚不支援 fsverity。

  • 強烈假定 I/O 應該像在 XFS 上那樣工作。

  • iomap 真的適用於非常規檔案資料嗎?

歡迎補丁!