LWN: 基于io_uring的用户空间块设备驱动!
关注了就能看到更多这么棒的文章哦~
An io_uring-based user-space block driver
By Jonathan Corbet
August 8, 2022
DeepL assisted translation
https://lwn.net/Articles/903855/
在 6.0 合并窗口期间,很容易会忽略掉新加入的 ublk 驱动;它被深埋在 io_uring 的 pull request 之中,而且完全没有任何文档能提示我们需要对它进行额外的研究。Ublk 的目标是促进在用户空间中实现高性能的 block driver。因此,它使用了 io_uring 跟内核进行通信。这个驱动目前被认为是实验性的;如果最终成功了,那么可能会是一个预兆,预示着将来内核会有巨变。
编者已经花了相当多的时间去研究 ublk 驱动的源代码,以及实现了用户空间组件的 ubdsrv server。从这个未加注释以及缺乏元音的代码中探索出来的景象,很可能在某些细节上是不正确的,不过整体理解应该相当接近现实了。
How ublk works
ublk 驱动首先创建了一个叫做 /dev/ublk-control 的特殊设备。用户空间的服务器进程(可以是很多个进程)通过打开该设备并建立一个 io_uring 的 ring 来与之通信。在这个层面的操作,基本上都是 ioctl() 命令,但是/dev/ublk-control 并没有 ioctl() 处理程序。相反,所有的操作都是通过 io_uring 的命令来发送的。既然最终目的是在 io_uring 的基础上实现一个设备,那么确实没有理由不从一开始就直接使用 io_uring 的功能。
服务器进程通常会以 UBLK_CMD_ADD_DEV 命令作为开始;正如人们所期望的,它可以将一个新的 ublk 设备添加到系统中。server 进程可以对这个设备的各个方面进行描述,包括它声称要实现的硬件队列有几个、块大小、最大传输大小以及设备可以容纳的 block 数量。在这个命令成功之后,在 ublk 驱动看来,就已经有这个设备了,并且可以通过/dev/ublkcN 来访问到,其中 N 是创建设备时所返回的设备 ID。但是,该设备此时尚未被添加到 block layer 中。
server 进程应该打开新增的 /dev/ublkcN 设备来进行如下步骤,其中第一个步骤是用 mmap()调用将设备上的一个区域映射到 server 的地址空间内。这个区域是描述 I/O 请求的 ublksrv_io_desc 结构的数组:
struct ublksrv_io_desc {/* op: bit 0-7, flags: bit 8-31 */__u32 op_flags;__u32 nr_sectors;__u64 start_sector;__u64 addr; };
后续的 I/O 请求通知将从 io_uring 接收到。为了达到这个目的,服务器必须在新创建的设备上排队等候一组 UBLK_IO_FETCH_REQ 请求;通常情况下,为设备所声明的每个 "hardware queue"都会有一个队列,这也可能是跟 server 内运行的线程数量一一对应的。这个请求中还必须提供一个 memory buffer,用来容纳设备创建时所声明的 max request size 的数量。
在这些设置完成之后,可以使用 UBLK_CMD_START_DEV 操作来让 ublk 驱动真正创建一个对系统其他部分可见的块设备。当 block 子系统向这个设备发送一个请求时,队列中的某个 UBLK_IO_FETCH_REQ 操作就会完成。返回给用户空间 server 进程的 completion 数据中就包括了描述该请求的 ublkserv_io_desc 结构的索引,server 进程现在应该执行该请求。对于一个写请求来说,需要写入的数据就会放在 server 所提供的 buffer 中;对于读请求来说,数据应该会放在同一个 buffer 中。
在这个操作完成后,server 必须通知内核这个进展;也就是通过在 ring 中放一个 UBLK_IO_COMMIT_AND_FETCH_REQ 操作来进行通知的。它会把操作的结果反馈给 block 子系统,但同时也会把 buffer 放到队列里去准备接收下一个请求,从而避免了还需要专门进行这个操作。
还有 UBLK_CMD_STOP_DEV 和 UBLK_CMD_DEL_DEV 操作来让现有的设备消失,还有几个其他操作用来查询现有设备的信息。还有一些细节在这里并没有涉及到,这主要是为了提高性能。此外,配置 ublk 协议的目的是要实现 zero-copy I/O,但在目前的代码中没有实现。
server 代码中实现了两个 target:null 和 loop。正如人们所期望的,null 这个 target 是一个过于复杂的、针对 block 设备的/dev/null;它没有什么用,但使人们可以摒弃不相干的细节来直接看到这个驱动的工作效果。loop 这个 target 使用了一个现有的文件作为虚拟 block 设备的备份存储。根据作者 Ming Lei 的说法,使用这种循环实现,"性能甚至优于具有相同设置的内核 loop 设备"。
Implications
人们可能会问,我们为什么要做这项工作(而且显然得到了 Red Hat 的支持);如果世界一直在吵着要一个基于 io_uring 的、用户空间的、更快的 loop block 设备,其实它就已经悄悄地出现了。patch 的 cover letter 中提到的一个好处是,block 驱动代码的开发可以更容易地在用户空间完成。另一个好处是高性能的 qcow2 支持。patch cover letter 还引用了其他开发者的说法,希望有一个快速的用户空间 block 设备的机制。
不过,一个有趣的问题是,这种机制是否最终会促进一些设备驱动挪到内核之外,也许不仅仅局限在 block 设备驱动领域。将设备驱动放到用户空间的代码中,是一些 security-system 设计中的基本观点,包括 microkernel 系统。但是,这些设计方案总是有两个组件之间的通信开销的问题,尤其是它们不再在同一地址空间内运行。Io_uring 可能是解决这个问题的一个令人信服的答案。
如果今后真的如此发展了,那么未来的内核可能与我们现在的内核有很大的不同;它们可能更小,许多复杂的逻辑在独立的用户空间组件中运行。这是否是 Lei 对 ublk 的愿景的一部分?目前还不得而知,而且可能最终完全不会走到这一步。但 ublk 显然是一个有趣的实验,可能会导致下一步的重大变动。不过,在今后统治世界之前,还是需要先把自己的文档补充起来。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~