用户空间块设备驱动程序 (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 设备,并且每个设备都具有非常大的队列深度,则可能不够

参考