从内核到用户空间(2) — 初探 ublk

本文以我的视角对 ublk 进行了最基本的分析,希望也为你带来帮助。

ublk

ublk 是一个 6.X 内核全新的实现用户态块设备驱动的内核框架,之前的类似框架还有 TCMU、vdpa-user (VDUSE) 和 NBD。ublk 框架中,一个额外的 ublk Server 用户态进程作为 ublk 块设备的服务后端,实现了主要的存储逻辑。区别于其他用户态块设备框架,ublk 采用 io_uring 作为内核与用户态通信的传输机制。ublk 架构图如下:

使用 ublk 框架,内核会多出几种设备,包括一个唯一的 ublk_ctl 设备,多个名为 /dev/ublkcN 的字符设备,以及同样数量的 /dev/ublkbN 块设备。其中,

  • 块设备是实际的存储服务设备,可以格式化文件系统或者作为裸设备使用,这也是 ublk 存在的最终目的;
  • 字符设备是 ublk 框架的数据面接口,主要被用户态 ublk Server 进程用于与内核通信,处理块设备的实际 IO 请求;
  • ublk_ctl 设备(/dev/ublk-control)则可以看作的是 ublk 框架的控制面通道,ublk Server 通过请求 ublk_ctl 设备来创建出多对字符设备和块设备,

类似于其他用户态驱动框架,ublk 为了方便用户态 ublk-server 的开发,也提供了用户态 SDK 库 ublksrv (ublksrv/lib at master · ublk-org/ublksrv (github.com))。

控制面通信协议

/*
 * Admin commands, issued by ublk server, and handled by ublk driver.
 */
#define    UBLK_CMD_GET_QUEUE_AFFINITY 0x01
#define    UBLK_CMD_GET_DEV_INFO   0x02
#define    UBLK_CMD_ADD_DEV        0x04
#define    UBLK_CMD_DEL_DEV        0x05
#define    UBLK_CMD_START_DEV  0x06
#define    UBLK_CMD_STOP_DEV   0x07
#define    UBLK_CMD_SET_PARAMS 0x08
#define    UBLK_CMD_GET_PARAMS 0x09
#define    UBLK_CMD_START_USER_RECOVERY    0x10
#define    UBLK_CMD_END_USER_RECOVERY  0x11
#define    UBLK_CMD_GET_DEV_INFO2      0x12

ublk_ctl 所对应的控制面命令用于控制各个 ublk 块设备的创建、删除、开始、停止等生命周期管理。

数据面通信协议

/*
 * IO commands, issued by ublk server, and handled by ublk driver.
 *
 * FETCH_REQ: issued via sqe(URING_CMD) beforehand for fetching IO request
 *      from ublk driver, should be issued only when starting device. After
 *      the associated cqe is returned, request's tag can be retrieved via
 *      cqe->userdata.
 *
 * COMMIT_AND_FETCH_REQ: issued via sqe(URING_CMD) after ublkserver handled
 *      this IO request, request's handling result is committed to ublk
 *      driver, meantime FETCH_REQ is piggyback, and FETCH_REQ has to be
 *      handled before completing io request.
 *
 * NEED_GET_DATA: only used for write requests to set io addr and copy data
 *      When NEED_GET_DATA is set, ublksrv has to issue UBLK_IO_NEED_GET_DATA
 *      command after ublk driver returns UBLK_IO_RES_NEED_GET_DATA.
 *
 *      It is only used if ublksrv set UBLK_F_NEED_GET_DATA flag
 *      while starting a ublk device.
 */

#define    UBLK_IO_FETCH_REQ       0x20
#define    UBLK_IO_COMMIT_AND_FETCH_REQ    0x21
#define    UBLK_IO_NEED_GET_DATA   0x22

数据面通信流程

ublk-server 在实际收到块层读写请求前就需要通过UBLK_IO_FETCH_REQ 向 iouring SQE 来“预取”请求  (1) 。用户对文件读写的请求  (2) 最终会转为一个块层到 ublk 块设备请求 (3),ublk 驱动则是通过一个 iouring 完成请求 (CQE) 来通知 ublk-server (4) 。ublk-server 通过请求其他存储服务 (5) 来处理实际的读写IO。比如,它可以解析本地 qcow2 文件,或者请求远程分布式后端。最后,ublk-server 会提交一个 COMMIT_AND_FETCH_REQ 请求 SQE (6),完成这一请求。

从一个 server 实现者的角度来看,这个模式不是很自然。通常,iouring Submission Queue 用于提交请求,而Completion Queue用于接收完成通知;然而,在ublk中,这一过程似乎颠倒了:Completion Queue用于提交块请求,而Submission Queue则用于处理请求完成。导致这种反直觉的原因是 iouring 通常是在内核端都是作为用户请求的前端向内核更底层的文件系统、设备驱动提交文件IO、网络IO 等请求,请求的前端位于用户态,请求的后端位于内核中比 iouring 更后端的设备驱动层。而 ublk-driver 与 ublk-server 通信的情况正好相反,请求更上游的 ublk-driver 位于内核态,而更下游的 ublk-server 位于用户态,这就造成了用户态-内核态通信的上下游颠倒。virtio-balloon 等特殊的 virtio 设备在需要进行 host 到 guest 方向的通信时,也会有类似的反直觉思路。

结语

关于 ublk,首先它是一种全新的用户态块设备框架,由 RedHat 所开发;其次它依赖 io_uring 这一同样处于活跃开发的”内核-用户态”通信机制。他的稳定无疑需要时间,它的发展也将取决于块存储的需求和 io_uring 生态的发展。但无论如何,它都是有趣的。


TCM Userspace Design — The Linux Kernel documentation
VDUSE – “vDPA Device in Userspace” — The Linux Kernel documentation
ublksrv/lib at master · ublk-org/ublksrv (github.com)
Zero-copy I/O for ublk, three different ways [LWN.net]
An io_uring-based user-space block driver [LWN.net]
SPDK: ublk Target
Userspace block device driver (ublk driver) — The Linux Kernel documentation

Leave a Reply

Your email address will not be published. Required fields are marked *