0%

VDUSE(vDPA Device in Userspace)技术简介

VDUSE简介

VDUSE(vDPA Device in Userspace)技术是字节跳动2020年10月向 Linux 内核社区正式开源的一项技术,通过VDUSE可以在一个用户进程实现一个软件定义的 vDPA 设备,并可以通过上述 vDPA 框架接入 virtio 或者 vhost 子系统,供容器或者虚机使用。此技术将在Linux 5.15 版本中首次引入。

3eb77592e0fcd81865e863680e4fadde.png

vDPA(virtio data path acceleration)设备是一个使用符合virtio规范的数据路径的设备,具有厂商特定的控制路径。vDPA设备既可以物理上位于硬件上,也可以通过软件模拟。VDUSE是一个框架,它使在用户空间实现软件仿真的vDPA设备成为可能。为了使设备仿真更加安全,仿真的vDPA设备的控制路径在内核中处理,只有数据路径在用户空间中实现。

需要注意的是,现在VDUSE框架只支持virtio块设备,当实现数据路径的用户空间进程由非特权用户运行时,才可以减少安全风险。对其他设备类型的支持可以在将来相应设备驱动的安全问题得到修复后加入。

创建/删除VDUSE设备

  • VDUSE设备的创建过程如下:
  1. 通过 ioctl(VDUSE_CREATE_DEV) 在 /dev/vduse/control 上创建一个新的 VDUSE 实例。

  2. 在 /dev/vduse/$NAME 上通过 ioctl(VDUSE_VQ_SETUP) 设置每个 virtqueue。

  3. 开始处理来自 /dev/vduse/$NAME 的 VDUSE 消息。第一个消息将在将 VDUSE 实例附加到 vDPA 总线时到达。

  4. 发送 VDPA_CMD_DEV_NEW netlink 消息以将 VDUSE 实例连接到 vDPA 总线。

  • VDUSE设备的删除过程如下:
  1. 发送 VDPA_CMD_DEV_DEL netlink 消息,将 VDUSE 实例从 vDPA 总线上分离。

  2. 关闭 /dev/vduse/$NAME 的文件描述符。

  3. 在 /dev/vduse/control 上通过 ioctl(VDUSE_DESTROY_DEV) 销毁 VDUSE 实例。

netlink信息可以通过iproute2中的vdpa工具发送,或者使用下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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配置空间、virtqueues的数量等等。然后,一个char设备接口(/dev/vduse/$NAME)被输出到用户空间用于设备仿真。用户空间可以在/dev/vduse/$NAME上使用VDUSE_VQ_SETUP ioctl来添加每个virtqueue的配置,如virtqueue的最大长度到设备上。

在初始化之后,VDUSE设备可以通过VDPA_CMD_DEV_NEW网链消息连接到vDPA总线。用户空间需要在/dev/vduse/$NAME上read()/write(),以receive/replay 接收或者发送 VDUSE 内核模块的一些控制信息,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 spec: 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设置新的映射。

在DRIVER_OK状态位通过VDUSE_SET_STATUS消息被设置后,用户空间能够开始数据平面的处理,如下所示。

  1. 通过VDUSE_VQ_GET_INFO ioctl获得指定虚拟队列的信息,包括大小、描述符表的IOVA、可用ring和已用ring 、状态和就绪状态。
  2. 将上述IOVAs传递给VDUSE_IOTLB_GET_FD ioctl,以便这些IOVA区域可以被映射到用户空间。下面是一些示例代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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;
}
  1. 用VDUSE_VQ_SETUP_KICKFD ioctl为指定的虚拟队列设置kick eventfd。kick eventfd被VDUSE内核模块用来通知用户空间消耗可用的ring。这是可选的,因为用户空间可以选择轮询可用ring。
  2. 监听kick事件fd(可选)并消耗可用的ring。描述符表中描述符所描述的缓冲区在访问前也应该通过VDUSE_IOTLB_GET_FD ioctl映射到用户空间。
  3. 在使用的ring被填满后,通过VDUSE_INJECT_VQ_IRQ ioctl为特定的虚拟队列注入一个中断。

关于uAPI的更多细节,请参见include/uapi/linux/vduse.h。

欢迎关注我的其它发布渠道