使用者空間塊裝置驅動程式 (ublk driver)

概述

ublk 是一個通用框架,用於從使用者空間實現塊裝置邏輯。其背後的動機是將虛擬塊驅動程式(如 loop、nbd 和類似程式)移動到使用者空間,這會非常有幫助。它可以幫助實現新的虛擬塊裝置,例如 ublk-qcow2(已經有幾次嘗試在核心中實現 qcow2 驅動程式)。

使用者空間塊裝置很有吸引力,因為

  • 它們可以用多種程式語言編寫。

  • 它們可以使用核心中不可用的庫。

  • 可以使用應用程式開發人員熟悉的工具進行除錯。

  • 崩潰不會導致機器核心崩潰。

  • 與核心程式碼中的錯誤相比,錯誤可能具有較低的安全影響。

  • 它們可以獨立於核心進行安裝和更新。

  • 它們可用於使用使用者指定的引數/設定為測試/除錯目的輕鬆地模擬塊裝置

ublk 塊裝置 (/dev/ublkb*) 由 ublk 驅動程式新增。裝置上的任何 IO 請求都將轉發到 ublk 使用者空間程式。為方便起見,本文件中,ublk 伺服器 指的是通用 ublk 使用者空間程式。ublksrv [1] 是此類實現之一。它提供了 libublksrv [2] 庫,用於方便地開發特定的使用者塊裝置,同時還包括通用型別的塊裝置,例如 loop 和 null。Richard W.M. Jones 基於 libublksrv [2] 編寫了使用者空間 nbd 裝置 nbdublk [3]

在 IO 由使用者空間處理後,結果會提交回驅動程式,從而完成請求週期。透過這種方式,任何特定的 IO 處理邏輯都完全由使用者空間完成,例如 loop 的 IO 處理、NBD 的 IO 通訊或 qcow2 的 IO 對映。

/dev/ublkb* 由基於 blk-mq 請求的驅動程式驅動。每個請求都由一個佇列範圍內的唯一標籤分配。ublk 伺服器也為每個 IO 分配唯一標籤,該標籤與 /dev/ublkb* 的 IO 進行 1:1 對映。

IO 請求轉發和 IO 處理結果提交都透過 io_uring 直通命令完成;這就是 ublk 也是基於 io_uring 的塊驅動程式的原因。已經觀察到,使用 io_uring 直通命令可以提供比塊 IO 更好的 IOPS;這就是 ublk 是使用者空間塊裝置的高效能實現之一的原因:不僅 IO 請求通訊由 io_uring 完成,而且 ublk 伺服器中首選的 IO 處理也是基於 io_uring 的方法。

ublk 提供控制介面來設定/獲取 ublk 塊裝置引數。該介面是可擴充套件的,並且 kabi 相容:基本上任何 ublk 請求佇列的引數或 ublk 通用功能引數都可以透過該介面進行設定/獲取。因此,ublk 是通用的使用者空間塊裝置框架。例如,從使用者空間設定具有指定塊引數的 ublk 裝置很容易。

使用 ublk

ublk 需要使用者空間 ublk 伺服器來處理實際的塊裝置邏輯。

以下是使用 ublksrv 提供基於 ublk 的 loop 裝置的示例。

  • 新增裝置

    ublk add -t loop -f ublk-loop.img
    
  • 使用 xfs 格式化,然後使用它

    mkfs.xfs /dev/ublkb0
    mount /dev/ublkb0 /mnt
    # do anything. all IOs are handled by io_uring
    ...
    umount /mnt
    
  • 列出具有其資訊的裝置

    ublk list
    
  • 刪除裝置

    ublk del -a
    ublk del -n $ublk_dev_id
    

有關使用詳情,請參見 ublksrv [4] 的 README。

設計

控制平面

ublk 驅動程式提供全域性 misc 裝置節點 (/dev/ublk-control),用於藉助幾個控制命令管理和控制 ublk 裝置

  • UBLK_CMD_ADD_DEV

    新增 ublk 字元裝置 (/dev/ublkc*),該裝置與 ublk 伺服器就 IO 命令通訊進行對話。基本裝置資訊與此命令一起傳送。它設定 ublksrv_ctrl_dev_info 的 UAPI 結構,例如 nr_hw_queuesqueue_depth 和最大 IO 請求緩衝區大小,這些資訊與驅動程式協商併發送回伺服器。完成此命令後,基本裝置資訊將不可變。

  • UBLK_CMD_SET_PARAMS / UBLK_CMD_GET_PARAMS

    設定或獲取裝置的引數,這些引數可以是通用功能相關的,也可以是請求佇列限制相關的,但不能是 IO 邏輯特定的,因為驅動程式不處理任何 IO 邏輯。必須在傳送 UBLK_CMD_START_DEV 之前傳送此命令。

  • UBLK_CMD_START_DEV

    在伺服器準備好使用者空間資源(例如建立 I/O 處理程式執行緒 & io_uring 以處理 ublk IO)後,此命令將傳送到驅動程式以分配 & 公開 /dev/ublkb*。透過 UBLK_CMD_SET_PARAMS 設定的引數將應用於建立裝置。

  • UBLK_CMD_STOP_DEV

    停止 /dev/ublkb* 上的 IO 並刪除裝置。當此命令返回時,ublk 伺服器將釋放資源(例如銷燬 I/O 處理程式執行緒 & io_uring)。

  • UBLK_CMD_DEL_DEV

    刪除 /dev/ublkc*。當此命令返回時,可以重用分配的 ublk 裝置號。

  • UBLK_CMD_GET_QUEUE_AFFINITY

    新增 /dev/ublkc 時,驅動程式會建立塊層 tagset,以便每個佇列的親和性資訊都可用。伺服器傳送 UBLK_CMD_GET_QUEUE_AFFINITY 以檢索佇列親和性資訊。它可以有效地設定每個佇列的上下文,例如將仿射 CPU 與 IO pthread 繫結,並嘗試在 IO 執行緒上下文中分配緩衝區。

  • UBLK_CMD_GET_DEV_INFO

    用於透過 ublksrv_ctrl_dev_info 檢索裝置資訊。伺服器有責任在使用者空間中儲存 IO 目標特定資訊。

  • UBLK_CMD_GET_DEV_INFO2UBLK_CMD_GET_DEV_INFO 的用途相同,但是 ublk 伺服器必須提供 /dev/ublkc* 的字元裝置路徑,供核心執行許可權檢查,並且此命令是為支援非特權 ublk 裝置而新增的,並與 UBLK_F_UNPRIVILEGED_DEV 一起引入。只有擁有請求裝置的使用者才能檢索裝置資訊。

    如何處理使用者空間/核心相容性

    1. 如果核心能夠處理 UBLK_F_UNPRIVILEGED_DEV

    如果 ublk 伺服器支援 UBLK_F_UNPRIVILEGED_DEV

    ublk 伺服器應傳送 UBLK_CMD_GET_DEV_INFO2,因為每當非特權應用程式需要查詢當前使用者擁有的裝置時,應用程式都不知道是否設定了 UBLK_F_UNPRIVILEGED_DEV,因為功能資訊是無狀態的,並且應用程式應始終透過 UBLK_CMD_GET_DEV_INFO2 檢索它

    如果 ublk 伺服器不支援 UBLK_F_UNPRIVILEGED_DEV

    UBLK_CMD_GET_DEV_INFO 始終傳送到核心,並且 UBLK_F_UNPRIVILEGED_DEV 的功能對於使用者不可用

    1. 如果核心無法處理 UBLK_F_UNPRIVILEGED_DEV

    如果 ublk 伺服器支援 UBLK_F_UNPRIVILEGED_DEV

    首先嚐試 UBLK_CMD_GET_DEV_INFO2,並且將失敗,然後由於無法設定 UBLK_F_UNPRIVILEGED_DEV,因此需要重試 UBLK_CMD_GET_DEV_INFO

    如果 ublk 伺服器不支援 UBLK_F_UNPRIVILEGED_DEV

    UBLK_CMD_GET_DEV_INFO 始終傳送到核心,並且 UBLK_F_UNPRIVILEGED_DEV 的功能對於使用者不可用

  • UBLK_CMD_START_USER_RECOVERY

    如果啟用了 UBLK_F_USER_RECOVERY 功能,則此命令有效。在舊程序已退出、ublk 裝置已靜默並且 /dev/ublkc* 已釋放後,將接受此命令。使用者應在他啟動重新開啟 /dev/ublkc* 的新程序之前傳送此命令。當此命令返回時,ublk 裝置已準備好用於新程序。

  • UBLK_CMD_END_USER_RECOVERY

    如果啟用了 UBLK_F_USER_RECOVERY 功能,則此命令有效。在 ublk 裝置已靜默並且新程序已開啟 /dev/ublkc* 並且所有 ublk 佇列都已準備好之後,將接受此命令。當此命令返回時,ublk 裝置將被取消靜默,並且新的 I/O 請求將傳遞到新程序。

  • 使用者恢復功能描述

    為使用者恢復添加了三個新功能:UBLK_F_USER_RECOVERYUBLK_F_USER_RECOVERY_REISSUEUBLK_F_USER_RECOVERY_FAIL_IO。要在 ublk 伺服器退出後啟用 ublk 裝置的恢復,ublk 伺服器應在建立裝置時指定 UBLK_F_USER_RECOVERY 標誌。ublk 伺服器還可以指定 UBLK_F_USER_RECOVERY_REISSUEUBLK_F_USER_RECOVERY_FAIL_IO 中的至多一個,以修改在 ublk 伺服器正在死亡/已死亡時如何處理 I/O(這在驅動程式程式碼中稱為 nosrv 情況)。

    僅設定 UBLK_F_USER_RECOVERY 後,在 ublk 伺服器退出後,ublk 在整個恢復階段都不會刪除 /dev/ublkb*,並且 ublk 裝置 ID 將保留。ublk 伺服器有責任透過自己的知識恢復裝置上下文。尚未傳送到使用者空間的請求將被重新排隊。已傳送到使用者空間的請求將被中止。

    另外設定 UBLK_F_USER_RECOVERY_REISSUE 後,在 ublk 伺服器退出後,與 UBLK_F_USER_RECOVERY 相反,已傳送到使用者空間的請求將被重新排隊,並且在處理 UBLK_CMD_END_USER_RECOVERY 後將重新發送到新程序。UBLK_F_USER_RECOVERY_REISSUE 專為容忍雙寫的後端而設計,因為驅動程式可能會兩次發出相同的 I/O 請求。它可能對只讀 FS 或 VM 後端有用。

    另外設定 UBLK_F_USER_RECOVERY_FAIL_IO 後,在 ublk 伺服器退出後,已傳送到使用者空間的請求將失敗,隨後發出的任何請求也是如此。應用程式會看到一系列 I/O 錯誤,直到新的 ublk 伺服器恢復裝置為止,並且會持續針對設定此標誌的裝置發出 I/O。

非特權 ublk 裝置透過傳遞 UBLK_F_UNPRIVILEGED_DEV 支援。設定此標誌後,所有控制命令都可以由非特權使用者傳送。除了 UBLK_CMD_ADD_DEV 命令之外,ublk 驅動程式還會對所有其他控制命令執行對指定字元裝置 (/dev/ublkc*) 的許可權檢查,為此,字元裝置的路徑必須在這些命令的有效負載中從 ublk 伺服器提供。透過這種方式,ublk 裝置成為容器感知的,並且在一個容器中建立的裝置只能在此容器內部進行控制/訪問。

資料平面

ublk 伺服器應建立專用執行緒來處理 I/O。每個執行緒都應具有其自己的 io_uring,透過該 io_uring 通知它有新的 I/O,並且透過該 io_uring 它可以完成 I/O。這些專用執行緒應專注於 IO 處理,並且不應處理任何控制和管理任務。

The's IO 由唯一標籤分配,該標籤與 /dev/ublkb* 的 IO 請求進行 1:1 對映。

ublksrv_io_desc 的 UAPI 結構被定義為描述來自驅動程式的每個 IO。在 /dev/ublkc* 上提供了一個固定的 mmapped 區域(陣列),用於將 IO 資訊匯出到伺服器;例如 IO 偏移量、長度、OP/標誌和緩衝區地址。每個 ublksrv_io_desc 例項都可以透過佇列 ID 和 IO 標籤直接索引。

以下 IO 命令透過 io_uring 直通命令進行通訊,並且每個命令僅用於轉發 IO 並使用命令資料中指定的 IO 標籤提交結果

  • UBLK_IO_FETCH_REQ

    從伺服器 IO pthread 傳送,用於提取傳送到 /dev/ublkb* 的未來傳入 IO 請求。此命令僅從伺服器 IO pthread 傳送一次,用於 ublk 驅動程式設定 IO 轉發環境。

    一旦執行緒針對給定的 (qid,tag) 對發出此命令,該執行緒就會將自己註冊為該 I/O 的守護程序。將來,只有該 I/O 的守護程序才允許針對 I/O 發出命令。如果任何其他執行緒嘗試針對該執行緒不是守護程序的 (qid,tag) 對發出命令,則該命令將失敗。只有透過恢復才能重置守護程序。

    每個 (qid,tag) 對都具有其自己的獨立守護程序任務的能力由 UBLK_F_PER_IO_DAEMON 功能指示。如果驅動程式不支援此功能,則守護程序必須是每個佇列的 - 即,與單個 qid 關聯的所有 I/O 都必須由同一任務處理。

  • UBLK_IO_COMMIT_AND_FETCH_REQ

    當 IO 請求傳送到 /dev/ublkb* 時,驅動程式會將 IO 的 ublksrv_io_desc 儲存到指定的對映區域;然後,此 IO 標籤的先前接收到的 IO 命令(UBLK_IO_FETCH_REQUBLK_IO_COMMIT_AND_FETCH_REQ))完成,因此伺服器透過 io_uring 獲得 IO 通知。

    在伺服器處理 IO 後,其結果透過發回 UBLK_IO_COMMIT_AND_FETCH_REQ 提交回驅動程式。一旦 ublkdrv 收到此命令,它就會解析結果並完成對 /dev/ublkb* 的請求。同時,設定環境以使用相同的 IO 標籤提取將來的請求。也就是說,UBLK_IO_COMMIT_AND_FETCH_REQ 被重用於提取請求和提交回 IO 結果。

  • UBLK_IO_NEED_GET_DATA

    啟用 UBLK_F_NEED_GET_DATA 後,WRITE 請求將首先發送到 ublk 伺服器,而無需資料複製。然後,ublk 伺服器的 IO 後端接收請求,並且它可以分配資料緩衝區並將地址嵌入到此新 IO 命令中。在核心驅動程式獲取命令後,資料複製將從請求頁面完成到此後端的緩衝區。最後,後端再次接收到要寫入資料的請求,並且它可以真正處理該請求。

    UBLK_IO_NEED_GET_DATA 添加了一個額外的往返過程和一個 io_uring_enter() 系統呼叫。任何認為這可能會降低效能的使用者都不應啟用 UBLK_F_NEED_GET_DATA。預設情況下,ublk 伺服器為每個 IO 預分配 IO 緩衝區。任何新專案都應嘗試使用此緩衝區與 ublk 驅動程式通訊。但是,現有專案可能會中斷或無法使用新的緩衝區介面;這就是新增此命令以實現向後相容性的原因,以便現有專案仍然可以使用現有緩衝區。

  • ublk 伺服器 IO 緩衝區和 ublk 塊 IO 請求之間的資料複製

    驅動程式需要首先將塊 IO 請求頁面複製到伺服器緩衝區(頁面)中,以便在將即將到來的 IO 通知伺服器之前用於 WRITE,以便伺服器可以處理 WRITE 請求。

    當伺服器處理 READ 請求並將 UBLK_IO_COMMIT_AND_FETCH_REQ 傳送到伺服器時,ublkdrv 需要將讀取的伺服器緩衝區(頁面)複製到 IO 請求頁面。

零複製

ublk 零複製依賴於 io_uring 的固定核心緩衝區,該緩衝區提供了兩個 API:io_buffer_register_bvec()io_buffer_unregister_bvec

ublk 添加了 UBLK_IO_REGISTER_IO_BUF 的 IO 命令來呼叫 io_buffer_register_bvec(),以便 ublk 伺服器將客戶端請求緩衝區註冊到 io_uring 緩衝區表中,然後 ublk 伺服器可以使用已註冊的緩衝區索引提交 io_uring IO。IO 命令 UBLK_IO_UNREGISTER_IO_BUF 呼叫 io_buffer_unregister_bvec() 來登出緩衝區,保證在呼叫 io_buffer_register_bvec()io_buffer_unregister_bvec() 之間是活動的。任何支援這種核心緩衝區的 io_uring 操作都將獲取對緩衝區的一個引用,直到操作完成。

實現零複製或使用者複製的 ublk 伺服器必須具有 CAP_SYS_ADMIN 並且是可信的,因為 ublk 伺服器有責任確保 IO 緩衝區填充了資料以處理讀取命令,並且 ublk 伺服器必須在處理 READ 命令時將正確的結果返回給 ublk 驅動程式,並且結果必須與填充到 IO 緩衝區的位元組數匹配。否則,未初始化的核心 IO 緩衝區將被公開給客戶端應用程式。

ublk 伺服器需要將 struct ublk_param_dma_align 的引數與後端對齊,以便零複製能夠正確工作。

為了達到最佳 IO 效能,ublk 伺服器應將其 struct ublk_param_segment 的分段引數與後端對齊,以避免不必要的 IO 拆分,這通常會損害 io_uring 效能。

自動緩衝區註冊

UBLK_F_AUTO_BUF_REG 功能自動處理 I/O 請求的緩衝區註冊和登出,從而簡化了緩衝區管理過程並減少了 ublk 伺服器實現中的開銷。

這是用於零複製的另一個功能標誌,它與 UBLK_F_SUPPORT_ZERO_COPY 相容。

功能概述

此功能會在將 I/O 命令傳遞到 ublk 伺服器之前,自動將請求緩衝區註冊到 io_uring 上下文中,並在完成 I/O 命令時登出它們。這消除了透過 UBLK_IO_REGISTER_IO_BUFUBLK_IO_UNREGISTER_IO_BUF 命令手動進行緩衝區註冊/登出的需要,然後 ublk 伺服器中的 IO 處理可以避免依賴於兩個 uring_cmd 操作。

如果這些 IO 之間存在任何依賴關係,則無法同時向 io_uring 發出 IO。因此,這種方式不僅簡化了 ublk 伺服器的實現,而且透過消除對緩衝區註冊和登出命令的依賴關係,使併發 IO 處理成為可能。

使用要求

  1. ublk 伺服器必須在用於 UBLK_IO_FETCH_REQUBLK_IO_COMMIT_AND_FETCH_REQ 的同一 io_ring_ctx 上建立稀疏緩衝區表。如果在不同的 io_ring_ctx 上發出 uring_cmd,則需要手動登出緩衝區。

  2. 緩衝區註冊資料必須透過 uring_cmd 的 sqe->addr 傳遞,並具有以下結構

    struct ublk_auto_buf_reg {
        __u16 index;      /* Buffer index for registration */
        __u8 flags;       /* Registration flags */
        __u8 reserved0;   /* Reserved for future use */
        __u32 reserved1;  /* Reserved for future use */
    };
    

    ublk_auto_buf_reg_to_sqe_addr() 用於將上述結構轉換為 sqe->addr

  3. ublk_auto_buf_reg 中的所有保留欄位都必須清零。

  4. 可選標誌可以透過 ublk_auto_buf_reg.flags 傳遞。

回退行為

如果自動緩衝區註冊失敗

  1. 啟用 UBLK_AUTO_BUF_REG_FALLBACK

    • uring_cmd 已完成

    • ublksrv_io_desc.op_flags 中設定了 UBLK_IO_F_NEED_REG_BUF

    • ublk 伺服器必須手動處理故障,例如手動註冊緩衝區,或者使用使用者複製功能檢索資料以處理 ublk IO

  2. 如果未啟用回退

    • ublk I/O 請求靜默失敗

    • uring_cmd 將不會完成

限制

  • 所有操作都需要相同的 io_ring_ctx

  • 在回退情況下可能需要手動緩衝區管理

  • io_ring_ctx 緩衝區表的最大大小為 16K,如果此單個 io_ring_ctx 處理過多的 ublk 裝置,並且每個裝置都具有非常大的佇列深度,則可能不夠

參考