本文链接: https://cloud.tencent.com/developer/article/2436757

术语

vduse: VDUSE(vDPA Device in Userspace) 用户态vdpa设备

VDUSE 简介:virtio 软件定义的数据路径

2022 年 7 月 14 日 谢永吉, 王杰森

标签: 存储 虚拟化

用户空间中的 vDPA 设备 ( VDUSE ) 是一种为虚拟机 (VM) 和容器工作负载提供软件定义存储和网络服务的新兴方法,vDPA(virtio 数据路径加速)内核子系统是 VDUSE 背后的引擎。不熟悉 vDPA 内核框架,请参阅我们的vDPA 内核框架和用于内核子系统交互的 vDPA 总线驱动程序简介博客,以熟悉vDPA 总线vDPA 总线驱动程序vDPA 设备等概念,因为我们假设读者很熟悉以及本博客中的这些主题。

简而言之,VDUSE 使您能够在用户空间中轻松实施软件模拟的 vDPA 设备,以服务虚拟机和容器工作负载。

vDPA 最初是为了帮助在专用硬件(例如 smartNIC)中实现virtio(一种开放标准控制和数据平面)而开发的,这只需要在硬件中支持 virtio 数据平面,并使用 vDPA 将供应商特定的控制平面转换为 virtio 控制平面。(显着简化供应商的工作)。

VDUSE 已发展为提供基于软件的 vDPA 设备(相对于之前的硬件 vDPA 设备),该设备可以利用 vDPA 内核子系统为虚拟机和容器工作负载提供标准接口,这对于优化的用户空间应用程序(例如存储性能开发)非常有用。套件 (SPDK) 和数据平面开发套件 (DPDK) 应用程序需要高效的接口来连接到计算机上运行的所有工作负载(虚拟机和容器)。

与硬件 vDPA 实现相比,vDPA 用户空间设备具有以下优点:

  1. 快速灵活的开发:您可以利用大量用户空间库并重用 QEMU 和 rust-vmm 中的设备模拟代码。
  2. 提高可维护性:例如,对用户空间应用程序执行实时升级比对内核模块或硬件执行实时升级更容易。
  3. 易于部署:没有硬件限制,用户空间应用程序可以轻松集成到云原生基础设施中。
  4. 强大的生态系统:可以将现有的用户空间数据平面(例如 SPDK 和 DPDK)用于虚拟机和容器。

本博客介绍了 VDUSE 架构,并回顾了几个演示其用法的示例。

VDUSE架构

VDUSE的基础设施包括两个关键块:位于用户空间的VDUSE守护进程和位于内核的VDUSE模块

img

图片

图 1:VDUSE 内核模块和用户空间守护进程

VDUSE 守护进程负责实现用户空间 vDPA 设备,它包含设备模拟和 virtio 数据平面。

  1. 设备模拟负责模拟 vDPA 设备,它包含两个主要功能:
  2. 设备初始化和配置是通过VDUSE模块提供的ioctl()接口完成的。
  3. 处理运行时控制消息,例如设置设备状态,是通过read()/write()接口实现的。
  4. virtio dataplane负责处理来自 virtio 设备驱动程序的请求,该请求的数据缓冲区应提前通过 mmap() 接口映射到用户空间。

内核中的VDUSE模块负责桥接VDUSE守护进程和vDPA框架,以便用户空间vDPA设备可以在vDPA框架下工作,它包含三个功能模块:

  1. VDUSE 使用char 设备接口将 vDPA 配置操作和内存映射信息中继到用户空间,它通过使用 ioctl()、read()、write() 和 mmap() 等用户空间接口来实现。
  2. vDPA 设备将VDUSE 模块连接到 vDPA 框架,通过将其附加(通过实现通用 vDPA 总线操作)到 vDPA 总线,VDUSE 模块可以接收来自 vDPA 框架的控制消息,然后 VDUSE 模块可以对其进行处理。将其放置或转发到 VDUSE 守护程序。
  3. 基于内存管理单元**(MMU) 的软件输入/输出转换后备缓冲区 (IOTLB)**使 VDUSE 守护进程能够访问内核空间中的数据缓冲区。它实现了**反弹缓冲**机制,以便用户空间可以安全地访问数据。

VDUSE 对容器的支持

VDUSE 容器支持的关键点是用户空间 vDPA 设备所附加的 vDPA 总线驱动程序。目前,vDPA 内核框架支持两种类型的 vDPA 总线驱动程序:virtio-vdpa(用于容器)和vhost-vdpa(用于虚拟机, 如QEMU中的GUEST)

如果您想通过 VDUSE 为容器工作负载提供接口,则 vDPA 设备应与 virtio-vdpa 绑定(如下所示)。

图 2:通过 VDUSE 提供容器工作负载

在这种情况下,virtio-vDPA 总线驱动程序提供了一个 virtio 设备,可以将各种内核子系统连接到该 virtio 设备以供用户空间应用程序使用。

如前所述,为了使用户空间 VDUSE 守护进程能够访问 virtio 设备驱动程序中的数据缓冲区,在数据平面的 VDUSE 内核模块中引入了具有反弹缓冲机制的基于 MMU 的软件 IOTLB。

数据从内核空间中的原始数据缓冲区到反弹缓冲区并返回,具体取决于传输方向,然后用户空间守护程序只需将反弹缓冲区映射到其地址空间,而不是原始地址空间,这可能会发生。在同一页中包含其他私有内核数据。

VDUSE 对虚拟机的支持

如果 vDPA 设备与 vhost-vdpa 绑定,则 VDUSE 守护程序可以为虚拟机工作负载提供服务,如下所示。

图 3:通过 VDUSE 提供虚拟机工作负载

在这种情况下,虚拟主机 (vhost) 设备由 vhost-vDPA 总线驱动程序提供,因此它可以用作在 VM 内运行的 virtio 驱动程序的 vhost 后端。

共享内存机制: 在数据平面中,VM 的内存将与 VDUSE 守护进程共享,这样,VDUSE 守护进程可以直接访问驻留在用户空间内存区域中的数据缓冲区,而无需依赖反弹缓冲机制。

VDUSE端到端解决方案

现在您已经熟悉了 VDUSE 如何连接到容器和虚拟机工作负载,接下来看一下为这两种工作负载类型提供服务的整体解决方案。

图4:VDUSE架构概览

在图 4 中,核心组件 — VDUSE 守护进程(用户空间)和 VDUSE 模块(内核) — 用红线勾勒出轮廓。

VDUSE 用户空间守护程序可以通过将 VDUSE 内核模块创建的 vDPA 设备绑定到不同的 vDPA 总线驱动程序,为容器或虚拟机工作负载提供软件定义的存储和网络服务。

VDUSE 用例

以下用例演示了VDUSE 有价值的两种示例。

使用 VDUSE 访问远程存储

计算和存储分离的架构意味着您通常需要一种从计算节点中的虚拟机和容器访问远程存储服务的方法,VDUSE 为这种情况实现了可靠的解决方案

图 5:用于远程存储访问的 VDUSE 解决方案

VDUSE存储守护进程是整个解决方案的核心组件,它使用VDUSE框架来模拟vDPA块设备,然后通过网络将来自VM或容器的I/O请求转发到远程存储。

与其他解决方案相比,VDUSE 方法:

  1. 提供服务于虚拟机和容器工作负载的统一存储堆栈。
  2. 提供比现有解决方案更好的性能,例如用于容器工作负载的网络块设备 (NBD),这是因为数据平面的 VDUSE 方法基于共享内存通信,其系统调用和数据副本较少

启用 SPDK 应用程序来为容器提供服务

您还可以使用 VDUSE 启用专注于 VM 工作负载的现有 SPDK 应用程序(使用虚拟主机用户界面),为容器工作负载提供相同的服务,图 6 显示了其工作原理。

图 6:为容器重用 vhost-user 解决方案

上面介绍了一个 VDUSE Agent 代理来桥接容器和 SPDK 守护进程,一方面,它使用 VDUSE 框架来模拟绑定到 virtio-vdpa 总线驱动程序的 vDPA 块设备,另一方面,它充当 vhost用户客户端(vhost-user-client)与 SPDK 守护程序中的 vhost-user-server进行通信。

通过 VDUSE 框架,VDUSE 代理可以获取 virtio-blk 设备驱动程序数据平面中使用的内存区域(包括可用环、已用环、描述符表和包含 virtio 请求数据的反弹缓冲区)。

然后,通过 vhost-user 协议,VDUSE 代理可以将它们传输到 SPDK 守护程序,因此,当现有 SPDK 数据平面遵循 virtio 规范访问这些内存区域时,它会访问内核 virtio-blk 设备驱动程序中的数据。在此流程中,VDUSE 模块负责将数据到反弹缓冲区或从反弹缓冲区数据(MMU和软件IOTLB机制)。

总结

  • VDUSE 是基于 vDPA 的新内核框架,它使您能够在用户空间中模拟软件 vDPA 设备。
  • 该技术旨在提供一种新的用户空间方法,用于提供服务于容器和虚拟机工作负载的存储和网络服务。
  • 存算分离: 容器和虚机场景, 结合SPDK和vhost协议, MMU/IOTLB/弹跳缓冲区等机制, 支持拉远存储

作者简介

谢永继

软件工程师,字节跳动

YongJi Xie 是字节跳动的软件工程师,致力于 QEMU 和 Linux 内核中的 I/O 虚拟化主题。

王杰森

首席软件工程师

经验丰富的 Red Hat 高级软件工程师,具有在计算机软件行业工作的经验。Linux virtio、vhost 和 vdpa 驱动程序的共同维护者。

阅读完整的简历

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
字节跳动:bytedance, vduse, commit: https://github.com/ssbandjl/linux/commit/c8a6153b6c59d95c0e091f053f6f180952ade91e
kernel_doc: https://docs.kernel.org/userspace-api/vduse.html
vduse在用户空间引入 VDUSE - vDPA 设备此 VDUSE 驱动程序支持在用户空间实现软件模拟的 vDPA 设备vDPA 设备由 /dev/vduse/control 上的 ioctl(VDUSE_CREATE_DEV) 创建然后将字符设备接口 (/dev/vduse/$NAME) 导出到用户空间进行设备模拟。为了使设备模拟更安全,设备的控制路径在内核中处理。引入了一种消息机制来将一些数据平面相关的控制消息转发到用户空间。并且在数据路径中,DMA 缓冲区将通过不同的方式映射到用户空间地址空间,具体取决于 vDPA 设备所连接的 vDPA 总线。在 virtio-vdpa 情况下,使用基于 MMU 的软件 IOTLB 来实现这一点。在 vhost-vdpa 情况下,DMA 缓冲区位于用户空间内存区域中,可以通过传输 shmfd 将其共享给 VDUSE 用户空间进程。有关 VDUSE 设计和使用的更多详细信息,请参阅后续文档提交
modprobe vduse
insmod vduse.ko
drivers/vdpa/vdpa_user/vduse_dev.c
module_init(vduse_init)
   class_register(&vduse_class)
      .name = "vduse"
       "vduse/%s"
   alloc_chrdev_region(&vduse_major, 0, VDUSE_DEV_MAX, "vduse")
   cdev_init(&vduse_ctrl_cdev, &vduse_ctrl_fops)
   cdev_add(&vduse_ctrl_cdev, vduse_major, 1)
   dev = device_create(&vduse_class, NULL, vduse_major, NULL, "control") -> /dev/vduse/control
   cdev_init(&vduse_cdev, &vduse_dev_fops) -> /dev/vduse/$DEVICE
   cdev_add(&vduse_cdev, MKDEV(MAJOR(vduse_major), 1), VDUSE_DEV_MAX - 1)
   vduse_irq_wq = alloc_workqueue("vduse-irq", WQ_HIGHPRI | WQ_SYSFS | WQ_UNBOUND, 0)
   vduse_irq_bound_wq = alloc_workqueue("vduse-irq-bound", WQ_HIGHPRI, 0)
   vduse_domain_init
       iova_cache_get() -> 内核启动时通过该函数声明了 struct iova 结构体专用的slab分配器
           iova_cache = kmem_cache_create("iommu_iova", sizeof(struct iova), 0, SLAB_HWCACHE_ALIGN, NULL)
   vduse_mgmtdev_init
       vduse_mgmt->mgmt_dev.ops = &vdpa_dev_mgmtdev_ops
       vdpa_mgmtdev_register(&vduse_mgmt->mgmt_dev)

管理设备回调和DMA回调

 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
static const struct vdpa_mgmtdev_ops vdpa_dev_mgmtdev_ops = {
.dev_add = vdpa_dev_add,
       dev = vduse_find_dev(name)
           idr_for_each_entry(&vduse_idr, dev, id) <- static DEFINE_IDR(vduse_idr
       vduse_dev_init_vdpa(dev, name)
           vdev = vdpa_alloc_device(struct vduse_vdpa, vdpa, dev->dev, &vduse_vdpa_config_ops, 1, 1, name, true)
           set_dma_ops(&vdev->vdpa.dev, &vduse_dev_dma_ops)
       dev->domain = vduse_domain_create(VDUSE_IOVA_SIZE - 1, dev->bounce_size)
           domain->iotlb = vhost_iotlb_alloc(0, 0)
           file = anon_inode_getfile("[vduse-domain]", &vduse_domain_fops, domain, O_RDWR)
           init_iova_domain(&domain->stream_iovad,PAGE_SIZE, IOVA_START_PFN)
           iova_domain_init_rcaches(&domain->stream_iovad)
       _vdpa_register_device(&dev->vdev->vdpa, dev->vq_num) -> __vdpa_register_device(vdev, nvqs)
           dev = bus_find_device(&vdpa_bus, NULL, dev_name(&vdev->dev), vdpa_name_match)
           device_add(&vdev->dev)
.dev_del = vdpa_dev_del,
};


static const struct dma_map_ops vduse_dev_dma_ops = {
.map_page = vduse_dev_map_page,
       vduse_domain_map_page(domain, page, offset, size, dir, attrs)
           dma_addr_t iova = vduse_domain_alloc_iova(iovad, size, limit)
               iova_pfn = alloc_iova_fast(iovad, iova_len, limit >> shift, true) -> allocates an iova from rcache -> 首先尝试从缓存中查找满足条件的 I/O 虚拟地址空间地址段如果找到就成功返回否则调用 alloc_iova() 函数分配 IOVA 内存段如果需要刷新缓存则释放 CPU 缓存的所有 IOVA 范围
                   iova_pfn = iova_rcache_get
                   new_iova = alloc_iova(iovad, size, limit_pfn, true)
               return (dma_addr_t)iova_pfn << shift
           vduse_domain_init_bounce_map
               vduse_iotlb_add_range(domain, 0, domain->bounce_size - 1, 0, VHOST_MAP_RW, domain->file, 0)
                   map_file->file = get_file(file)
                   vhost_iotlb_add_range_ctx
           vduse_domain_map_bounce_page
               map->bounce_page = alloc_page(GFP_ATOMIC)
           vduse_domain_bounce
.unmap_page = vduse_dev_unmap_page,
.alloc = vduse_dev_alloc_coherent,
       addr = vduse_domain_alloc_coherent(domain, size, (dma_addr_t *)&iova, flag, attrs)
           dma_addr_t iova = vduse_domain_alloc_iova(iovad, size, limit)
           alloc_pages_exact -> 分配精确数量的物理连续页面
           vduse_iotlb_add_range virt_to_phys
       *dma_addr = (dma_addr_t)iova
.free = vduse_dev_free_coherent,
.max_mapping_size = vduse_dev_max_mapping_size,
};


static const struct file_operations vduse_domain_fops = {
.owner = THIS_MODULE,
.mmap = vduse_domain_mmap,
.release = vduse_domain_release,
};

vdpa_config_ops 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
static const struct vdpa_config_ops vduse_vdpa_config_ops = {
.set_vq_address= vduse_vdpa_set_vq_address,
       vq->desc_addr = desc_area;
       vq->driver_addr = driver_area;
       vq->device_addr = device_area;
.kick_vq= vduse_vdpa_kick_vq,
       schedule_work(&vq->kick)
       vduse_vq_kick(vq)
.set_vq_cb= vduse_vdpa_set_vq_cb,
.set_vq_num             = vduse_vdpa_set_vq_num,
.set_vq_ready= vduse_vdpa_set_vq_ready,
.get_vq_ready= vduse_vdpa_get_vq_ready,
.set_vq_state= vduse_vdpa_set_vq_state,
.get_vq_state= vduse_vdpa_get_vq_state,
.get_vq_align= vduse_vdpa_get_vq_align,
.get_device_features= vduse_vdpa_get_device_features,
.set_driver_features= vduse_vdpa_set_driver_features,
.get_driver_features= vduse_vdpa_get_driver_features,
.set_config_cb= vduse_vdpa_set_config_cb,
.get_vq_num_max= vduse_vdpa_get_vq_num_max,
.get_device_id= vduse_vdpa_get_device_id,
.get_vendor_id= vduse_vdpa_get_vendor_id,
.get_status= vduse_vdpa_get_status,
.set_status= vduse_vdpa_set_status,
.get_config_size= vduse_vdpa_get_config_size,
.get_config= vduse_vdpa_get_config,
.set_config= vduse_vdpa_set_config,
.get_generation= vduse_vdpa_get_generation,
.set_vq_affinity= vduse_vdpa_set_vq_affinity,
       if (cpu_mask)
           cpumask_copy(&dev->vqs[idx]->irq_affinity, cpu_mask)
       else
           cpumask_setall(&dev->vqs[idx]->irq_affinity)
.get_vq_affinity= vduse_vdpa_get_vq_affinity,
.reset= vduse_vdpa_reset,
.set_map= vduse_vdpa_set_map,
.free= vduse_vdpa_free,
};

内核态创建块设备及IOCTL回调函数/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
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
控制设备回调
static const struct file_operations vduse_ctrl_fops = {
.owner= THIS_MODULE,
.open= vduse_open,
       control = kmalloc(sizeof(struct vduse_control), GFP_KERNEL)
       file->private_data = control
.release= vduse_release,
.unlocked_ioctl= vduse_ioctl,
       switch (cmd)
       case VDUSE_CREATE_DEV -> #define VDUSE_CREATE_DEV_IOW(VDUSE_BASE, 0x02, struct vduse_dev_config)
           vduse_create_dev(&config, buf, control->api_version)
               dev = vduse_dev_create()
                   INIT_LIST_HEAD(&dev->send_list)
                   INIT_LIST_HEAD(&dev->recv_list)
                   INIT_WORK(&dev->inject, vduse_dev_irq_inject)
                       dev->config_cb.callback(dev->config_cb.private)
               dev->device_id = config->device_id <- from qemu -> vblk_exp->dev = vduse_dev_create(vblk_opts->name, VIRTIO_ID_BLOCK, 0,features, num_queues,sizeof(struct virtio_blk_config),(char *)&config, &vduse_blk_ops,vblk_exp)
               dev->bounce_size = VDUSE_BOUNCE_SIZE -> 64MB
               idr_alloc(&vduse_idr, dev, 1, VDUSE_DEV_MAX, GFP_KERNEL)
               dev->dev = device_create_with_groups(&vduse_class, NULL, MKDEV(MAJOR(vduse_major), dev->minor), dev, vduse_dev_groups, "%s", config->name) -> new kernel -> creates a device and registers it with sysfs
               vduse_dev_init_vqs(dev, config->vq_align, config->vq_num)
                   dev->vqs = kcalloc(dev->vq_num, sizeof(*dev->vqs), GFP_KERNEL)
                   for (i = 0; i < vq_num; i++)
                       dev->vqs[i]->irq_effective_cpu = IRQ_UNBOUND
                       INIT_WORK(&dev->vqs[i]->inject, vduse_vq_irq_inject)
                           vq->cb.callback(vq->cb.private)
                       INIT_WORK(&dev->vqs[i]->kick, vduse_vq_kick_work) -> vduse_vq_kick(vq)
                           if (vq->kickfd)
                               eventfd_signal(vq->kickfd) -> vq->kickfd = ctx <- vduse_kickfd_setup
                                   -> to qemu -> on_vduse_vq_kick ->通知QEMU进行处理
                           else
                               vq->kicked = true
                       kobject_add(&dev->vqs[i]->kobj, &dev->dev->kobj, "vq%d", i)
               __module_get(THIS_MODULE) -> 增加模块的引用计数
       case VDUSE_DESTROY_DEV
           name[VDUSE_NAME_MAX - 1] = '\0'
           vduse_destroy_dev(name)
               vduse_dev_reset(dev)
               device_destroy(&vduse_class, MKDEV(MAJOR(vduse_major), dev->minor))
               idr_remove(&vduse_idr, dev->minor)
               vduse_dev_deinit_vqs(dev)
               vduse_domain_destroy(dev->domain)
               module_put(THIS_MODULE)
.compat_ioctl= compat_ptr_ioctl,
.llseek= noop_llseek,
};

// vduse设备回调函数
static const struct file_operations vduse_dev_fops = {
.owner= THIS_MODULE,
.open= vduse_dev_open,
.release= vduse_dev_release,
.read_iter= vduse_dev_read_iter,
.write_iter= vduse_dev_write_iter,
.poll= vduse_dev_poll,
.unlocked_ioctl= vduse_dev_ioctl,
       case VDUSE_IOTLB_GET_FD
           map = vhost_iotlb_itree_first(dev->domain->iotlb, entry.start, entry.last)
           receive_fd(f, NULL, perm_to_file_flags(entry.perm))
       case VDUSE_DEV_GET_FEATURES
           ret = put_user(dev->driver_features, (u64 __user *)argp)
       case VDUSE_DEV_SET_CONFIG
           copy_from_user(dev->config + config.offset, argp + size, config.length)
       case VDUSE_DEV_INJECT_CONFIG_IRQ
            vduse_dev_queue_irq_work(dev, &dev->inject, IRQ_UNBOUND)
       case VDUSE_VQ_SETUP
           index = array_index_nospec(config.index, dev->vq_num)
           dev->vqs[index]->num_max = config.max_size
       case VDUSE_VQ_GET_INFO
           vq = dev->vqs[index];
           vq_info.desc_addr = vq->desc_addr;
           vq_info.driver_addr = vq->driver_addr;
           vq_info.device_addr = vq->device_addr;
           vq_info.num = vq->num;
       case VDUSE_VQ_SETUP_KICKFD
           vduse_kickfd_setup(dev, &eventfd)
               ctx = eventfd_ctx_fdget(eventfd->fd)
               vq->kickfd = ctx
       case VDUSE_VQ_INJECT_IRQ
           vduse_dev_queue_irq_work(dev, &dev->vqs[index]->inject,dev->vqs[index]->irq_effective_cpu)
               queue_work(vduse_irq_wq, irq_work)
               or queue_work_on(irq_effective_cpu, vduse_irq_bound_wq, irq_work)
       case VDUSE_IOTLB_REG_UMEM
           vduse_dev_reg_umem(dev, umem.iova, umem.uaddr, umem.size)
               npages = size >> PAGE_SHIFT;
            page_list = __vmalloc(array_size(npages, sizeof(struct page *)), GFP_KERNEL_ACCOUNT);
            umem = kzalloc(sizeof(*umem), GFP_KERNEL)
               pinned = pin_user_pages(uaddr, npages, FOLL_LONGTERM | FOLL_WRITE, page_list) -> pin user pages in memory for use by other devices
               vduse_domain_add_user_bounce_pages(dev->domain, page_list, pinned)
                   memcpy_to_page(pages[i], 0, page_address(map->bounce_page), PAGE_SIZE)
               mmgrab(current->mm)
       case VDUSE_IOTLB_DEREG_UMEM
           vduse_dev_dereg_umem(dev, umem.iova, umem.size)
       case VDUSE_IOTLB_GET_INFO
           map = vhost_iotlb_itree_first(dev->domain->iotlb, info.start, info.last)
.compat_ioctl= compat_ptr_ioctl,
.llseek= noop_llseek,
};

QEMU侧源码分析

1
2
3
4
5
# launch QSD exposing the VM image as `vduse1` vDPA device 通过vduse-blk将qcow2导出为用户态块设备
$ qemu-storage-daemon \
--blockdev file,filename=Fedora-Cloud-Base-39-1.5.x86_64.qcow2,node-name=file \
--blockdev qcow2,file=file,node-name=qcow2 \
--export vduse-blk,id=vduse1,name=vduse1,num-queues=1,node-name=qcow2,writable=on &

QEMU调用栈, 导出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
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
storage-daemon/qemu-storage-daemon.c
int main(int argc, char *argv[])
   qemu_init_exec_dir(argv[0])
   os_setup_signal_handling()
   process_options(argc, argv, true) <- --export vduse-blk,id=vduse1,name=vduse1,num-queues=1,node-name=qcow2,writable=on
       case OPTION_EXPORT
           qmp_block_export_add
               blk_exp_add
                   blk_exp_find_driver
                       for (i = 0; i < ARRAY_SIZE(blk_exp_drivers); i++)
   bdrv_init
   init_qmp_commands
   qemu_init_main_loop(&error_fatal)
   process_options(argc, argv, false)
   pid_file_init
   os_setup_post
   main_loop_wait(false)
   blk_exp_close_all



static const BlockExportDriver *blk_exp_drivers[] = {
   &blk_exp_nbd,
#ifdef CONFIG_VHOST_USER_BLK_SERVER
   &blk_exp_vhost_user_blk,
#endif
#ifdef CONFIG_FUSE
   &blk_exp_fuse,
#endif
#ifdef CONFIG_VDUSE_BLK_EXPORT
   &blk_exp_vduse_blk,
#endif
};

const BlockExportDriver blk_exp_vduse_blk = {
  .type               = BLOCK_EXPORT_TYPE_VDUSE_BLK,
  .instance_size      = sizeof(VduseBlkExport),
  .create             = vduse_blk_exp_create,
       config.capacity = cpu_to_le64(blk_getlength(exp->blk) >> VIRTIO_BLK_SECTOR_BITS)
       vblk_exp->dev = vduse_dev_create(vblk_opts->name, VIRTIO_ID_BLOCK, 0,features, num_queues,sizeof(struct virtio_blk_config),(char *)&config, &vduse_blk_ops,vblk_exp) -> 用户态QEMU创建块设备
       subprojects/libvduse/libvduse.c
           ioctl(ctrl_fd, VDUSE_CREATE_DEV, dev_config)
       vduse_dev_setup_queue(vblk_exp->dev, i, queue_size)
       aio_set_fd_handler(exp->ctx, vduse_dev_get_fd(vblk_exp->dev), on_vduse_dev_kick, NULL, NULL, NULL, vblk_exp->dev)
       blk_add_aio_context_notifier(exp->blk, blk_aio_attached, blk_aio_detach, vblk_exp)
       blk_set_dev_ops(exp->blk, &vduse_block_ops, exp)
       blk_set_disable_request_queuing(exp->blk, true)
  .delete             = vduse_blk_exp_delete,
  .request_shutdown   = vduse_blk_exp_request_shutdown,
};


static const VduseOps vduse_blk_ops = {
  .enable_queue = vduse_blk_enable_queue,
       aio_set_fd_handler(vblk_exp->export.ctx, vduse_queue_get_fd(vq), on_vduse_vq_kick, NULL, NULL, NULL, vq)
           on_vduse_vq_kick -> QEMU收到VQ内核通知,执行IO处理函数
               vduse_blk_vq_handler(dev, vq)
                   while (1)
                       vduse_queue_pop(vq, sizeof(VduseBlkReq))
                       Coroutine *co = qemu_coroutine_create(vduse_blk_virtio_process_req, req)
                           in_len = virtio_blk_process_req(handler, in_iov, out_iov, in_num, out_num)
                               case VIRTIO_BLK_T_IN:
                               case VIRTIO_BLK_T_OUT
                                   if (is_write)
                                       qemu_iovec_init_external(&qiov, out_iov, out_num);
                                   else read
                                       qemu_iovec_init_external(&qiov, in_iov, in_num)
                                   write -> ret = blk_co_pwritev(blk, offset, qiov.size, &qiov, 0)
                                       blk_co_pwritev_part
                                           blk_co_do_pwritev_part
                                   read -> ret = blk_co_preadv(blk, offset, qiov.size, &qiov, 0)
                               case VIRTIO_BLK_T_WRITE_ZEROES
                                   in->status = virtio_blk_discard_write_zeroes(handler, out_iov, out_num, type)
                           vduse_blk_req_complete(req, in_len)
                           vduse_blk_inflight_dec(vblk_exp)
                       vduse_blk_inflight_inc(vblk_exp)
                       qemu_coroutine_enter(co)
       eventfd_write(vduse_queue_get_fd(vq), 1)
  .disable_queue = vduse_blk_disable_queue,
};

SPDK与VDUSE

整体架构

[RFC]vduse:vduse 服务的 SPDK 后端:

Vduse 是一种高效的方法,可将存储服务导出到本地主机,供容器和其他进程使用。其 umem 注册扩展已合并到 Kernel 6.0 中。virtio-blk 相关代码与 vhost-blk 重复,在进一步重构 virtio-blk-tgt 后应删除重复代码。

1
2
3
4
5
6
SPDK中的VDUSE设备创建命令参考如下:
vduse device can be created/deleted by rpc methods like:
$./build/bin/spdk_tgt &
$./scripts/rpc.py vduse_create_blk_controller vduse0 null0 -n 3 -s 1024
$./scripts/rpc.py vduse_delete_controller vduse0
And attach/detach them with vdpa tool.

参考

VDUSE简介-virtio 的软件定义数据路径: https://www.redhat.com/ja/blog/introducing-vduse-software-defined-datapath-virtio

网络虚拟化——vduse: https://blog.csdn.net/dillanzhou/article/details/121689985

SPDK VDUSE: https://review.spdk.io/gerrit/plugins/gitiles/spdk/spdk/+/3ab3bf7d382e8137fc1b25b4cb69ce7d1d9ffa92

基于SPDK的Ublk和Vduse的用户空间块服务: https://blog.csdn.net/weixin_37097605/article/details/137362344

晓兵(ssbandjl)

博客: https://cloud.tencent.com/developer/user/5060293/articles | https://logread.cn | https://blog.csdn.net/ssbandjl | https://www.zhihu.com/people/ssbandjl/posts

https://chattoyou.cn(吐槽/留言)

DPU专栏

https://cloud.tencent.com/developer/column/101987

技术会友: 欢迎对DPU/智能网卡/卸载/网络,存储加速/安全隔离等技术感兴趣的朋友加入DPU技术交流群

weixin: ssbandjl

公众号: 云原生云

云原生云