VDUSE - “用户空间的 vDPA 设备”¶
vDPA (virtio 数据路径加速) 设备是一种使用符合 virtio 规范的数据路径,并具有供应商特定控制路径的设备。 vDPA 设备可以物理位于硬件上,也可以由软件模拟。 VDUSE 是一个框架,使得可以在用户空间中实现软件模拟的 vDPA 设备。 为了使设备仿真更加安全,模拟的 vDPA 设备的控制路径在内核中处理,而只有数据路径在用户空间中实现。
请注意,VDUSE 框架现在仅支持 virtio 块设备,这可以降低安全风险,因为实现数据路径的用户空间进程由非特权用户运行。 在相应的设备驱动程序的安全问题在将来得到澄清或修复后,可以添加对其他设备类型的支持。
创建/销毁 VDUSE 设备¶
VDUSE 设备的创建方式如下
在 /dev/vduse/control 上使用 ioctl(VDUSE_CREATE_DEV) 创建一个新的 VDUSE 实例。
在 /dev/vduse/$NAME 上使用 ioctl(VDUSE_VQ_SETUP) 设置每个 virtqueue。
开始处理来自 /dev/vduse/$NAME 的 VDUSE 消息。 当 VDUSE 实例附加到 vDPA 总线时,第一批消息将到达。
发送 VDPA_CMD_DEV_NEW netlink 消息以将 VDUSE 实例附加到 vDPA 总线。
VDUSE 设备的销毁方式如下
发送 VDPA_CMD_DEV_DEL netlink 消息以将 VDUSE 实例从 vDPA 总线分离。
关闭引用 /dev/vduse/$NAME 的文件描述符。
在 /dev/vduse/control 上使用 ioctl(VDUSE_DESTROY_DEV) 销毁 VDUSE 实例。
netlink 消息可以通过 iproute2 中的 vdpa 工具发送,也可以使用以下示例代码
static int netlink_add_vduse(const char *name, enum vdpa_command cmd)
{
struct nl_sock *nlsock;
struct nl_msg *msg;
int famid;
nlsock = nl_socket_alloc();
if (!nlsock)
return -ENOMEM;
if (genl_connect(nlsock))
goto free_sock;
famid = genl_ctrl_resolve(nlsock, VDPA_GENL_NAME);
if (famid < 0)
goto close_sock;
msg = nlmsg_alloc();
if (!msg)
goto close_sock;
if (!genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, famid, 0, 0, cmd, 0))
goto nla_put_failure;
NLA_PUT_STRING(msg, VDPA_ATTR_DEV_NAME, name);
if (cmd == VDPA_CMD_DEV_NEW)
NLA_PUT_STRING(msg, VDPA_ATTR_MGMTDEV_DEV_NAME, "vduse");
if (nl_send_sync(nlsock, msg))
goto close_sock;
nl_close(nlsock);
nl_socket_free(nlsock);
return 0;
nla_put_failure:
nlmsg_free(msg);
close_sock:
nl_close(nlsock);
free_sock:
nl_socket_free(nlsock);
return -1;
}
VDUSE 如何工作¶
如上所述,VDUSE 设备通过在 /dev/vduse/control 上调用 ioctl(VDUSE_CREATE_DEV) 来创建。 通过此 ioctl,用户空间可以指定一些基本配置,例如设备名称(唯一标识 VDUSE 设备)、virtio 特性、virtio 配置空间、virtqueue 的数量等等,用于此模拟设备。 然后,将字符设备接口 (/dev/vduse/$NAME) 导出到用户空间以进行设备仿真。 用户空间可以使用 /dev/vduse/$NAME 上的 VDUSE_VQ_SETUP ioctl 来添加每个 virtqueue 的配置,例如 virtqueue 的最大大小。
初始化后,可以通过 VDPA_CMD_DEV_NEW netlink 消息将 VDUSE 设备附加到 vDPA 总线。 用户空间需要在 /dev/vduse/$NAME 上进行 read()/write() 操作,以便从 VDUSE 内核模块接收/回复一些控制消息,如下所示
static int vduse_message_handler(int dev_fd)
{
int len;
struct vduse_dev_request req;
struct vduse_dev_response resp;
len = read(dev_fd, &req, sizeof(req));
if (len != sizeof(req))
return -1;
resp.request_id = req.request_id;
switch (req.type) {
/* handle different types of messages */
}
len = write(dev_fd, &resp, sizeof(resp));
if (len != sizeof(resp))
return -1;
return 0;
}
VDUSE 框架现在引入了三种类型的消息
VDUSE_GET_VQ_STATE: 获取 virtqueue 的状态,用户空间应返回拆分 virtqueue 的可用索引,或者返回设备/驱动程序环绕计数器以及打包 virtqueue 的可用和已用索引。
VDUSE_SET_STATUS: 设置设备状态,用户空间应遵循 virtio 规范:https://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.html 来处理此消息。 例如,如果设备无法接受从 VDUSE_DEV_GET_FEATURES ioctl 获取的协商 virtio 特性,则无法设置 FEATURES_OK 设备状态位。
VDUSE_UPDATE_IOTLB: 通知用户空间更新指定 IOVA 范围的内存映射,用户空间应首先删除旧的映射,然后通过 VDUSE_IOTLB_GET_FD ioctl 设置新的映射。
通过 VDUSE_SET_STATUS 消息设置 DRIVER_OK 状态位后,用户空间可以开始数据平面处理,如下所示
使用 VDUSE_VQ_GET_INFO ioctl 获取指定 virtqueue 的信息,包括大小、描述符表的 IOVA、可用环和已用环、状态和就绪状态。
将上述 IOVA 传递给 VDUSE_IOTLB_GET_FD ioctl,以便可以将这些 IOVA 区域映射到用户空间。 以下显示了一些示例代码
static int perm_to_prot(uint8_t perm)
{
int prot = 0;
switch (perm) {
case VDUSE_ACCESS_WO:
prot |= PROT_WRITE;
break;
case VDUSE_ACCESS_RO:
prot |= PROT_READ;
break;
case VDUSE_ACCESS_RW:
prot |= PROT_READ | PROT_WRITE;
break;
}
return prot;
}
static void *iova_to_va(int dev_fd, uint64_t iova, uint64_t *len)
{
int fd;
void *addr;
size_t size;
struct vduse_iotlb_entry entry;
entry.start = iova;
entry.last = iova;
/*
* Find the first IOVA region that overlaps with the specified
* range [start, last] and return the corresponding file descriptor.
*/
fd = ioctl(dev_fd, VDUSE_IOTLB_GET_FD, &entry);
if (fd < 0)
return NULL;
size = entry.last - entry.start + 1;
*len = entry.last - iova + 1;
addr = mmap(0, size, perm_to_prot(entry.perm), MAP_SHARED,
fd, entry.offset);
close(fd);
if (addr == MAP_FAILED)
return NULL;
/*
* Using some data structures such as linked list to store
* the iotlb mapping. The munmap(2) should be called for the
* cached mapping when the corresponding VDUSE_UPDATE_IOTLB
* message is received or the device is reset.
*/
return addr + iova - entry.start;
}
使用 VDUSE_VQ_SETUP_KICKFD ioctl 为指定的 virtqueue 设置 kick eventfd。 kick eventfd 由 VDUSE 内核模块用于通知用户空间使用可用的环。 这是可选的,因为用户空间可以选择轮询可用的环。
监听 kick eventfd(可选)并使用可用的环。 在访问之前,描述符表中的描述符所描述的缓冲区也应通过 VDUSE_IOTLB_GET_FD ioctl 映射到用户空间。
在填充已用环后,使用 VDUSE_INJECT_VQ_IRQ ioctl 为特定 virtqueue 注入中断。
有关 uAPI 的更多详细信息,请参阅 include/uapi/linux/vduse.h。