用户空间块设备驱动程序 (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_queues
、queue_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_INFO2
与UBLK_CMD_GET_DEV_INFO
的用途相同,但是 ublk 服务器必须提供/dev/ublkc*
的字符设备路径,供内核运行权限检查,并且此命令是为支持非特权 ublk 设备而添加的,并与UBLK_F_UNPRIVILEGED_DEV
一起引入。只有拥有请求设备的用户才能检索设备信息。如何处理用户空间/内核兼容性
如果内核能够处理
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 的功能对于用户不可用如果内核无法处理
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_RECOVERY
、UBLK_F_USER_RECOVERY_REISSUE
和UBLK_F_USER_RECOVERY_FAIL_IO
。要在 ublk 服务器退出后启用 ublk 设备的恢复,ublk 服务器应在创建设备时指定UBLK_F_USER_RECOVERY
标志。ublk 服务器还可以指定UBLK_F_USER_RECOVERY_REISSUE
和UBLK_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_REQ
或UBLK_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_BUF
和 UBLK_IO_UNREGISTER_IO_BUF
命令手动进行缓冲区注册/注销的需要,然后 ublk 服务器中的 IO 处理可以避免依赖于两个 uring_cmd 操作。
如果这些 IO 之间存在任何依赖关系,则无法同时向 io_uring 发出 IO。因此,这种方式不仅简化了 ublk 服务器的实现,而且通过消除对缓冲区注册和注销命令的依赖关系,使并发 IO 处理成为可能。
使用要求¶
ublk 服务器必须在用于
UBLK_IO_FETCH_REQ
和UBLK_IO_COMMIT_AND_FETCH_REQ
的同一io_ring_ctx
上创建稀疏缓冲区表。如果在不同的io_ring_ctx
上发出 uring_cmd,则需要手动注销缓冲区。缓冲区注册数据必须通过 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
。ublk_auto_buf_reg
中的所有保留字段都必须清零。可选标志可以通过
ublk_auto_buf_reg.flags
传递。
回退行为¶
如果自动缓冲区注册失败
启用
UBLK_AUTO_BUF_REG_FALLBACK
时uring_cmd 已完成
在
ublksrv_io_desc.op_flags
中设置了UBLK_IO_F_NEED_REG_BUF
ublk 服务器必须手动处理故障,例如手动注册缓冲区,或者使用用户复制功能检索数据以处理 ublk IO
如果未启用回退
ublk I/O 请求静默失败
uring_cmd 将不会完成
限制¶
所有操作都需要相同的
io_ring_ctx
在回退情况下可能需要手动缓冲区管理
io_ring_ctx 缓冲区表的最大大小为 16K,如果此单个 io_ring_ctx 处理过多的 ublk 设备,并且每个设备都具有非常大的队列深度,则可能不够