用户空间块设备驱动程序(ublk 驱动程序)¶
概述¶
ublk 是一个用于从用户空间实现块设备逻辑的通用框架。其背后的动机是将虚拟块驱动程序(如 loop、nbd 和类似的驱动程序)移至用户空间,这会非常有帮助。它可以帮助实现新的虚拟块设备,如 ublk-qcow2(有几个尝试在内核中实现 qcow2 驱动程序的尝试)。
用户空间块设备很有吸引力,因为
它们可以用多种编程语言编写。
它们可以使用内核中不可用的库。
可以使用应用程序开发人员熟悉的工具进行调试。
崩溃不会导致机器内核崩溃。
与内核代码中的错误相比,错误可能具有较低的安全影响。
它们可以独立于内核安装和更新。
它们可以用于轻松地模拟具有用户指定参数/设置的块设备,以进行测试/调试
ublk 块设备(/dev/ublkb*
)由 ublk 驱动程序添加。设备上的任何 IO 请求都将转发到 ublk 用户空间程序。为方便起见,本文档中,ublk server
指的是通用的 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 驱动程序提供全局杂项设备节点(/dev/ublk-control
),用于在几个控制命令的帮助下管理和控制 ublk 设备
UBLK_CMD_ADD_DEV
添加一个 ublk 字符设备(
/dev/ublkc*
),该设备与 ublk 服务器 WRT 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
在服务器准备好用户空间资源(如创建用于处理 ublk IO 的每个队列 pthread 和 io_uring)后,此命令将发送到驱动程序,以分配和公开
/dev/ublkb*
。通过UBLK_CMD_SET_PARAMS
设置的参数将应用于创建设备。UBLK_CMD_STOP_DEV
暂停
/dev/ublkb*
上的 IO 并删除设备。当此命令返回时,ublk 服务器将释放资源(如销毁每个队列 pthread 和 io_uring)。UBLK_CMD_DEL_DEV
删除
/dev/ublkc*
。当此命令返回时,可以重用分配的 ublk 设备号。UBLK_CMD_GET_QUEUE_AFFINITY
添加
/dev/ublkc
时,驱动程序会创建块层标签集,以便每个队列的亲和性信息可用。服务器发送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_CMD_GET_DEV_INFO
,因为无法设置UBLK_F_UNPRIVILEGED_DEV
如果 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 服务端正在关闭/已关闭时(在驱动代码中称为nosrv
情况)I/O 的处理方式。仅设置
UBLK_F_USER_RECOVERY
时,当一个 ubq_daemon(ublk 服务端的 I/O 处理程序)正在关闭时,ublk 不会在整个恢复阶段删除/dev/ublkb*
,并且保留 ublk 设备 ID。ublk 服务端有责任通过自身的知识恢复设备上下文。尚未发送到用户空间的请求会被重新排队。已发送到用户空间的请求会被中止。额外设置
UBLK_F_USER_RECOVERY_REISSUE
时,当一个 ubq_daemon(ublk 服务端的 I/O 处理程序)正在关闭时,与UBLK_F_USER_RECOVERY
相反,已发送到用户空间的请求会被重新排队,并且在处理UBLK_CMD_END_USER_RECOVERY
后会重新发送到新的进程。UBLK_F_USER_RECOVERY_REISSUE
专为容忍双写的后端设计,因为驱动程序可能会两次发送相同的 I/O 请求。这可能对只读文件系统或虚拟机后端有用。额外设置
UBLK_F_USER_RECOVERY_FAIL_IO
时,在 ublk 服务端退出后,已发送到用户空间的请求会失败,随后发送的任何请求也会失败。针对设置了此标志的设备持续发出 I/O 的应用程序会看到一系列 I/O 错误,直到新的 ublk 服务端恢复设备。
通过传递 UBLK_F_UNPRIVILEGED_DEV
来支持非特权 ublk 设备。一旦设置了此标志,非特权用户可以发送所有控制命令。除了 UBLK_CMD_ADD_DEV
命令之外,ublk 驱动程序会对所有其他控制命令执行指定字符设备(/dev/ublkc*
)的权限检查,为此,字符设备的路径必须在这些命令的 payload 中由 ublk 服务端提供。通过这种方式,ublk 设备变得容器感知,并且在一个容器中创建的设备只能在该容器内部进行控制/访问。
数据平面¶
ublk 服务端需要创建每个队列的 I/O pthread 和 io_uring,以通过 io_uring 直通处理 I/O 命令。每个队列的 I/O pthread 专注于 I/O 处理,不应处理任何控制和管理任务。
每个 I/O 都分配了一个唯一的标签,该标签与 /dev/ublkb*
的 I/O 请求一一对应。
定义了 ublksrv_io_desc
的 UAPI 结构来描述来自驱动程序的每个 I/O。在 /dev/ublkc*
上提供了一个固定的 mmapped 区域(数组),用于将 I/O 信息导出到服务端,例如 I/O 偏移量、长度、OP/标志和缓冲区地址。每个 ublksrv_io_desc
实例可以通过队列 ID 和 I/O 标签直接索引。
以下 I/O 命令通过 io_uring 直通命令进行通信,每个命令仅用于转发 I/O 并使用命令数据中指定的 I/O 标签提交结果
UBLK_IO_FETCH_REQ
从服务端 I/O pthread 发送,用于获取目标为
/dev/ublkb*
的未来传入 I/O 请求。此命令仅从服务端 I/O pthread 发送一次,供 ublk 驱动程序设置 I/O 转发环境。UBLK_IO_COMMIT_AND_FETCH_REQ
当一个 I/O 请求的目标是
/dev/ublkb*
时,驱动程序会将 I/O 的ublksrv_io_desc
存储到指定的映射区域;然后,之前收到的此 I/O 标签的 I/O 命令(UBLK_IO_FETCH_REQ
或UBLK_IO_COMMIT_AND_FETCH_REQ)
)完成,因此服务端通过 io_uring 收到 I/O 通知。在服务端处理完 I/O 后,其结果通过发送
UBLK_IO_COMMIT_AND_FETCH_REQ
返回给驱动程序。一旦 ublkdrv 收到此命令,它会解析结果并将请求完成到/dev/ublkb*
。同时,设置环境以获取具有相同 I/O 标签的未来请求。也就是说,UBLK_IO_COMMIT_AND_FETCH_REQ
被重用以获取请求并提交 I/O 结果。UBLK_IO_NEED_GET_DATA
启用
UBLK_F_NEED_GET_DATA
后,WRITE 请求将首先发送到 ublk 服务端,而无需数据复制。然后,ublk 服务端的 I/O 后端接收到请求,它可以分配数据缓冲区并将地址嵌入到此新的 I/O 命令中。内核驱动程序获取命令后,数据从请求页面复制到此后端的缓冲区。最后,后端再次接收到带有待写入数据的请求,并且它可以真正处理请求。UBLK_IO_NEED_GET_DATA
增加了一个额外的往返过程和一个 io_uring_enter() 系统调用。任何认为它可能会降低性能的用户都不应启用 UBLK_F_NEED_GET_DATA。默认情况下,ublk 服务端会为每个 I/O 预先分配 I/O 缓冲区。任何新项目都应尝试使用此缓冲区与 ublk 驱动程序进行通信。但是,现有项目可能会中断或无法使用新的缓冲区接口;这就是添加此命令以实现向后兼容性的原因,以便现有项目仍然可以使用现有缓冲区。ublk 服务端 I/O 缓冲区和 ublk 块 I/O 请求之间的数据复制
驱动程序需要首先将块 I/O 请求页面复制到服务端的缓冲区(页面)中,以便在通知服务端即将到来的 I/O 之前用于 WRITE 操作,以便服务端可以处理 WRITE 请求。
当服务端处理 READ 请求并向服务端发送
UBLK_IO_COMMIT_AND_FETCH_REQ
时,ublkdrv 需要将服务端缓冲区(页面)读取到 I/O 请求页面。
未来发展¶
零拷贝¶
零拷贝是 nbd、fuse 或类似驱动程序的通用要求。[6] Xiaoguang 提到的一个问题是,使用现有的 mm 接口,映射到用户空间的页面在内核中无法再重新映射。当将直接 I/O 目标定为 /dev/ublkb*
时,可能会发生这种情况。此外,他报告说,大型请求(I/O 大小 >= 256 KB)可能会从零拷贝中受益匪浅。