《重识云原生系列》专题索引:
- 第一章——不谋全局不足以谋一域
- 第二章计算第1节——计算虚拟化技术总述
- 第三章云存储第1节——分布式云存储总述
- 第四章云网络第一节——云网络技术发展简述
- 第四章云网络4.2节——相关基础知识准备
- 第四章云网络4.3节——重要网络协议
- 第四章云网络4.3.1节——路由技术简述
- 第四章云网络4.3.2节——VLAN技术
- 第四章云网络4.3.3节——RIP协议
- 第四章云网络4.3.4节——OSPF协议
- 第四章云网络4.3.5节——EIGRP协议
- 第四章云网络4.3.6节——IS-IS协议
- 第四章云网络4.3.7节——BGP协议
- 第四章云网络4.3.7.2节——BGP协议概述
- 第四章云网络4.3.7.3节——BGP协议实现原理
- 第四章云网络4.3.7.4节——高级特性
- 第四章云网络4.3.7.5节——实操
- 第四章云网络4.3.7.6节——MP-BGP协议
- 第四章云网络4.3.8节——策略路由
- 第四章云网络4.3.9节——Graceful Restart(平滑重启)技术
- 第四章云网络4.3.10节——VXLAN技术
- 第四章云网络4.3.10.2节——VXLAN Overlay网络方案设计
- 第四章云网络4.3.10.3节——VXLAN隧道机制
- 第四章云网络4.3.10.4节——VXLAN报文转发过程
- 第四章云网络4.3.10.5节——VXlan组网架构
- 第四章云网络4.3.10.6节——VXLAN应用部署方案
- 第四章云网络4.4节——Spine-Leaf网络架构
- 第四章云网络4.5节——大二层网络
- 第四章云网络4.6节——Underlay 和 Overlay概念
- 第四章云网络4.7.1节——网络虚拟化与卸载加速技术的演进简述
4 vhost-user
4.1 什么是 vhost-user
在 vhost_net 的方案中,由于 vhost_net 实现在内核中,guest 与 vhost_net 的通信,相较于原生的 virtio 方式性能上有了一定程度的提升,从 guest 到 kvm.ko 的交互只有一次用户态的切换以及数据拷贝。这个方案对于不同 host 之间的通信,或者 guest 到 host nic 之间的通信是比较好的,但是对于某些用户态进程间的通信,比如数据面的通信方案,openvswitch 和与之类似的 SDN 的解决方案,guest 需要和 host 用户态的 vswitch 进行数据交换,如果采用 vhost_net 的方案,guest 和 host 之间又存在多次的上下文切换和数据拷贝,为了避免这种情况,业界就想出将 vhost_net从内核态移到用户态。这就是 vhost-user 的实现。
4.2 vhost-user 的实现简述
vhost-user 和 vhost_net 的实现原理是一样,都是采用 vring 完成共享内存,eventfd 机制完成事件通知。不同在于 vhost_net 实现在内核中,而 vhost-user 实现在用户空间中,用于用户空间中两个进程之间的通信,其采用共享内存的通信方式。
vhost-user 基于 C/S 的模式,采用 UNIX 域套接字(UNIX domain socket)来完成进程间的事件通知和数据交互,相比 vhost_net 中采用 ioctl 的方式,vhost-user 采用 socket 的方式大大简化了操作。
vhost-user 基于 vring 这套通用的共享内存通信方案,只要 client 和 server 按照 vring 提供的接口实现所需功能即可,常见的实现方案是 client 实现在 guest OS 中,一般是集成在 virtio 驱动上,server 端实现在 qemu 中,也可以实现在各种数据面中,如 OVS,Snabbswitch 等虚拟交换机。
如果使用 qemu 作为 vhost-user 的 server 端实现,在启动 qemu 时,我们需要指定 -mem-path 和 -netdev 参数,如:
$ qemu -m 1024 -mem-path /hugetlbfs,prealloc=on,share=on
-netdev type=vhost-user,id=net0,file=/path/to/socket
-device virtio-net-pci,netdev=net0
指定 -mem-path 意味着 qemu 会在 guest OS 的内存中创建一个文件,share=on 选项允许其他进程访问这个文件,也就意味着能访问 guest OS 内存,达到共享内存的目的。
-netdev type=vhost-user 指定通信方案,file=/path/to/socket 指定 socket 文件。
当 qemu 启动之后,首先会进行 vring 的初始化,并通过 socket 建立 C/S 的共享内存区域和事件机制,然后 client 通过 eventfd 将 virtio kick 事件通知到 server 端,server 端同样通过 eventfd 进行响应,完成整个数据交互。
4.3 vhost-user使用DPDK加速
DPDK社区一直致力于加速数据中心的网络数据平面,而virtio网络作为当今云环境下数据平面必不可少的一环,自然是DPDK优化的方向。而vhost-user就是结合DPDK的各方面优化技术得到的用户态virtio网络后端驱动方案。这些优化技术包括:处理器亲和性、大页内存的使用、轮询模式驱动等。除了vhost-user,DPDK还有自己的virtio PMD作为高性能的前端,本文将以vhost-user作为重点介绍。
基于vhost协议,DPDK设计了一套新的用户态协议,名为vhost-user协议,这套协议允许qemu将virtio设备的网络包处理offload到任何DPDK应用中(例如OVS-DPDK)。vhost-user协议和vhost协议最大的区别其实就是通信信道的区别。Vhost协议通过对vhost-net字符设备进行ioctl实现,而vhost-user协议则通过unix socket进行实现。通过这个unix socket,vhost-user协议允许QEMU通过以下重要的操作来配置数据平面的offload:
- 特性协商:virtio的特性与vhost-user新定义的特性都可以通过类似的方式协商,而所谓协商的具体实现就是QEMU接收vhost-user的特性,与自己支持的特性取交集。
- 内存区域配置:QEMU配置好内存映射区域,vhost-user使用mmap接口来映射它们。
- Vring配置:QEMU将Virtqueue的个数与地址发送给vhost-user,以便vhost-user访问。
- 通知配置:vhost-user仍然使用eventfd来实现前后端通知。
基于DPDK的Open vSwitch(OVS-DPDK)一直以来就对vhost-user提供了支持,读者可以通过在OVS-DPDK上创建vhost-user端口来使用这种高效的用户态后端。
DPDK vhost-user架构图
4.4 基于DPDK/SPDK的virtio front-end\back-end设备驱动实现
鉴于DPDK/SPDK用户态驱动在性能优化方面的独到特性,virtio front-end驱动和back-end设备的用户态版本,主要基于DPDK/SPDK框架来进行开发。接下来本文将从front-end驱动和back-end设备的角度来分别介绍virtio的用户态实现及其主要的代码结构和运行机制。
4.4.1 virtio front-end 驱动的用户态实现
virtio front-end 驱动主要工作于Guest系统中,对基于KVM-Qemu的VM系统,其主要的处理对象是Qemu虚拟出来的virtio类型的pci controller;对于bare-metal模式的其他系统,其可以通过用户态unix socket来与后端设备交互。目前主流的virtio front-end用户态驱动主要有:virtio-net、virtio-blk/virtio-scsi、virtio-user-net、virtio-user-blk/virtio-user-scsi这几种。
4.4.1.1 virtio-net的用户态驱动
virtio-net用户态驱动的实现代码在 “DPDK/drivers/net/virtio” 目录下,其主要处理对象是 “Virtio network device” 的pci controller。结合代码可以大致将相关流程梳理如下:
- DPDK程序启动时,注册支持的设备id-table 和驱动函数;
1)设备id-table通过“pci_id_virtio_map”数据结构定义了支持的pci vendor和pci device的ID并使用RTE_PMD_REGISTER_PCI_TABLE(net_virtio, pci_id_virtio_map)进行注册;
2)驱动由“rte_virtio_net_pci_pmd”数据结构定义,并在 “rte_pci_register(&rte_virtio_net_pci_pmd) ”中进行注册。
2. 在DPDK初始化流程中完成设备探测匹配并加载驱动;
在DPDK初始化流程中主要藉由DPDK eal提供的用户态bus、device、driver的匹配模型进行设备探测、匹配和驱动加载。函数rte_eal_init会根据DPDK程序启动参数中指定的允许访问的设备信息进行设备探测和匹配(默认情况下DPDK/SPDK均不会主动加载通过vfio、uio驱动管理的设备,只会在参数显示指定后进行处理) 。当匹配到前述已注册的virtio-net的设备后,就会回调加载对应注册的驱动并调用eth_virtio_pci_probe进行设备初始化。
3. 初始化virtio network device pci controller;
初始化的入口函数是eth_virtio_pci_probe,除基本的pci相关的初始化外,其中主要可以分为两个层面的资源处理,其一是virtio设备本身层面的,如在vtpci_init函数中根据设备类型填充vtpci_ops类型的函数集,用于执行virtio设备的基本配置操作;其二,是rte_eth_dev设备层面的,在函数eth_virtio_dev_init中指定virtio-net设备关于网络特性相关的eth_dev_ops函数集,用于支持rte_eth_dev的特性操作。需要注意的是在此处也会在virtio_init_device函数中根据配置创建virtqueue并与硬件同步。
4. DPDK应用程序使用virtio-net设备相关的函数接口进行数据收发;
在DPDK应用程序中执行rte_eth_dev_start操作,设置rx\tx 处理函数、中断处理函数,并将virtqueue的状态通知给back-end设备。在这之后DPDK应用程序就可以通过rte_eth_tx_burst和rte_eth_rx_burst来执行数据的发送和接收。
在DPDK应用程序启动时通过-a指定“Virtio network device”的pci controller的bdf地址,virtio-net的用户态驱动会被匹配并加载。在该过程中会有如“EAL: Probe PCI driver: net_virtio (1af4:1000) device: 0000:00:04.0 (socket 0)”的打印信息,标识virtio-net驱动已初始化了virtio network device并将其注册成为了一个DPDK框架下的eth 端口,之后可以通过RTE_ETH_FOREACH_DEV类似的函数来查找并获取对应的port并使用。
4.4.1.2 virtio-user的用户态驱动
virtio-user模式的front-end驱动,本质上是为了在抛开Qemu等hypervisor的场景下和用户态实现的vhost back-end设备进行对接而实现的。其主要的使用场景可参考在bare-metal模式下的相关应用,或者针对vhost设备的简单测试需求。目前在DPDK/SPDK代码中所存在的virtio-user驱动的实现主要是两类,其一是DPDK中的virtio-net-user驱动,其二是SPDK中的virtio-blk-user/virtio-scsi-user驱动。
virtio-user模式的front-end驱动主要是通过unix socket来与back-end 的设备进行控制信息的协商和交互,并且作为client的角色。通信初始时会根据与vhost侧约定的文件来创建unix socket并基于此和vhost server进行链接。其二者之间的数据较互也通过内存共享映射来完成。
4.4.1.3 virtio-net-user驱动
virtio-net-user驱动代码存在于 “DPDK/drivers/net/virtio/virtio-user” 目录下。其计划支持对接的back-end的设备类型有:vhost-user、vhost-kernel、vhost-vdpa,此处仅结合vhost-user类型的back-end设备来进行介绍。结合DPDK代码中的实现,可以大致概述其相关的流程如下:
- 在rte_eal_init函数中通过传入的参数扫描并匹配对应virtio 类型的rte_vdev设备;
1)virtio-net-user驱动作为一种rte_vdev_bus总线设备的驱动以RTE_PMD_REGISTER_VDEV(net_virtio_user, virtio_user_driver)的方式注册到了DPDK的vdev驱动链表中;
2)当ret_eal_init函数执行时,其中会执行ret_bus_scan和rte_bus_probe的操作,若DPDK的启动参数中指定了rte_vdev设备时,就会在rte_vdev_bus总线类型设备的probe操作中对从vdev_device_list获取的设备依次从vdev_driver_list中查找到相对应的驱动并进行加载。对于net_virtio_user类型的设备,该过程最终回调的驱动函数就是virtio_user_pmd_probe。
2.调用注册的virtio_user驱动完成相关初始化并对外提供为通用的用户态网络设备;
在virtio_user_pmd_probe函数中,初始化操作的类型大致也可以概述为两个方面:其一,virtio设备层面的初始化,例如指定virtio的基本操作函数集virtio_user_ops,在virtio_user_dev_init函数中初始化virtio_user_dev的virtio_user_backend_ops类型指针,与back-end设备建立链接等;其二,在eth_virtio_dev_init函数中初始化rte_eth_dev类型的设备,如设置其网络特性的操作函数集virtio_eth_dev_ops,初始化virtqueue等。
3.DPDK应用程序通过用户态网络设备访问的函数接口完成数据的收发操作;
在DPDK应用程序中执行rte_eth_dev_start操作,设置rx\tx 处理函数、中断处理函数,并将virtqueue的状态通知给back-end设备。在这之后DPDK应用程序就可以通过rte_eth_tx_burst和rte_eth_rx_burst来执行数据的发送和接收。
virtio-net-user用户态驱动可以通过testpmd程序来进行测试,由--vdev参数指定vdev驱动的名字(形如net_virtio_user,或者net_virtio_user*,用于匹配virtio-net-user驱动)和用于链接用户态back-end设备(vhost-net)的socket句柄文件(形如path=/var/tmp/socket0)。
4.4.2 基于DPDK/SPDK的virtio back-end 驱动的用户态实现
virtio back-end设备用户态实现逻辑工作在Host系统的DPDK/SPDK进程中。当前支持的back-end设备类型主要有: vhost-net、vhost-blk/vhost-scsi和vshot-vdpa。在DPDK/SPDK进程中实现的virtio back-end设备一方面可以与Qemu进程配合,将实现的设备呈现为Guest系统下可见的pci controller供VM使用;另一方面也可以抛开Qemu直接和另外的用户态进程(如Container进程等)直接通过unix socket对接,为bare-metal应用提供服务,此时的back-end设备所在进程充当server的角色。
virtio back-end设备用户态实现的基本框架接口代码是在 “DPDK/lib/vhost”目录中,其主要的3个接口是:rte_vhost_driver_register、rte_vhost_driver_callback_register和rte_vhost_driver_start。对于上述几种不同的virtio back-end设备的用户态实现,都是在这个接口框架下来进行工作。这也意味着virtio back-end设备的各种类型实现(其中包括vdpa)都是以vhost的形态对外提供服务,或者都是在vhost 的接口框架下和front-end驱动进行交互。
在rte_vhost_driver_register函数中,根据指定的文件路径创建unix socket句柄,并设置其为server的角色。对于各个virtio back-end的各个类型设备,其均为server的角色。对于KVM-Qemu使用场景下的Qemu进程,其与vhost-user的back-end设备链接时,其角色为client。通过函数rte_vhost_driver_callback_register,各个不同的back-end设备可以设置各自的创建和销毁与一个front-end关联关系的notify_ops操作函数集。而rte_vhost_driver_start函数的执行则是启动vhost unix socket的监听,并设置请求处理的回调函数以实现在vhost_user_server_new_connection函数(其中可能会回调上述notify_ops中的new_connection函数)中来处理从front-end驱动发送过来的建链请求,且在其后以vhost_user_msg_handler来处理从链接上过来的消息(控制类消息)。
用户态virtio back-end设备实现中,与front-end驱动之间的virtqueue的vring对应的内存通过共享内存方式来实现,并且由virtio front-end驱动来分配,vhost back-end设备侧根据front-end发送过来的控制消息调用vhost_user_set_mem_table函数进行映射。
在vhost和vdpa的使用场景中,推荐VM配置中添加vIOMMU以达到更安全的使用目的。
4.4.2.1 vhost-net back-end设备驱动实现
vhost-net的back-end设备实现逻辑在 “DPDK/examples/vhost” 和 “DPDK/driver/net/vhost” 目录下均有实现,前者是实现了vhost-switch的功能,可以为VM提供数据转发的功能;后者则是在DPDK框架中注册一个eth端口,其背后实际是一个vhost-net的设备,以实现像使用DPDK的普通eth设备(port)一样处理virtio的数据和报文---从该eth port发送的报文被放到vritqueue中由vhost-net对应的front-end驱动接收,该eth port从vhost-net的virtqueue中接收来自front-ned驱动发送过来的报文。此处以“DPDK/examples/vhost”的实现为例来概述vhost-net back-end设备的相关工作流程:
1.设置vhost back-end处理函数并开启vhost server监听;
主要是根据用户传入的参数调用前述的“rte_vhost_driver_register、rte_vhost_driver_callback_register和rte_vhost_driver_start” 函数来完成。需要注意的是通过rte_vhost_driver_callback_register设置的virtio_net_device_ops函数集到vsocket->notify_ops中用于处理和front-end驱动关联时的相应操作。
2.响应front-end驱动的建链请求建立back-end设备与front-end驱动的关联;
当有virtio front-end驱动链接vhost back-end设备时,每个front-end对应的关联关系会在vhost_user_add_connection函数中创建一个virtio_net类型的数据结构进行标识。后续针对该virtio-front驱动的消息处理均以此数据结构归集和查找。并且针对初始发送过来的数据,在vhost_user_msg_handler会回调dev->notify_ops->new来进行具体back-end定义的私有化处理。在当前的流程中主要是进一步创建了vhost_dev来跟踪该链接关系,并将其添加到分配的CPU核对应的链表lcore_infovdev->coreid.vdev_list中。
3.循环处理virtqueue中的数据;
该程序逻辑在每个可用的CPU核上创建了一个线程循环运行switch_worker函数,循环处理各个vhost_dev对应的数据收发。数据收发的过程可以分别简单归纳如下:
1)接收方向主要在drain_eth_rx函数中实现
A.通过rte_eth_rx_burst从指定的DPDK eth port收包;
B.通过rte_vhost_enqueue_burst将数据放入virtqueue传给virtio front-end驱动。
2)发送方向主要在drain_virtio_tx函数中实现
A.先通过vs_dequeue_pkts从virtqueue中接收报文;
B.再通过virtio_tx_route将数据放入其他front-end设备对应的virtqueue的接收buf或者通过指定的DPDK eth prot发送出去。
DPDK代码中的这个vhost-net back-end设备的测试程序可以通过编译DPDK时由 “-Dexamples=vhost” 来指定编译,并参考34. Vhost Sample Application — Data Plane Development Kit 22.07.0-rc1 documentation 的说明来使用。
4.4.2.2 vhost-vdpa back-end设备
vhost-vdpa back-end设备的实现流程大体与vhost-net/vhost-blk/vhost-scsi一致。其主要差别在于:其一,支持vdpa的设备驱动通过rte_vdpa_register_device函数注册了rte_device和支持的功能接口到vdpa_device_list链表中,如此应用程序可以通过rte_vhost_driver_attach_vdpa_device将其赋给vhost_user_socket并最终与标识vhost设备的virtio_net数据结构关联;其二,vdpa的vritqueue的处理均由硬件完成,因而不需要实现函数来周期性处理virtqueue中的数据。当前vhost-vdpa的实现都放在DPDK代码中,且初步的后续计划也是在DPDK中进行支持。vhost-vdpa的功能涉及的代码主要是两个部分,其一是vhost基本机制的代码,即前以提到的“DPDK/lib/vhost” 目录下的代码;其二是vdpa设备驱动,主要在“DPDK/dirvers/vdpa” 目录下,该部分代码实现的功能主要通过上述的rte_vdpa_register_device来对vhost 设备呈现并产生关联。vDPA的主要结构框图可参见图4所示。
图4. vDPA用户态驱动实现时的模块关系
(IO路径如红色线标识)
DPDK代码中现有支持的vdpa的设备驱动目前主要有:ifc、mlx5和sfc这三种。这里以ifc驱动的为例,将其主要机制概述如下:
- 匹配设备并调用vdpa设备驱动进行初始化;
“DPDK/drivers/vdpa/ifc” 目录下为Intel的SmartNIC的对应vdpa驱动,其在DPDK框架中以普通的pci设备驱动注册,如RTE_PMD_REGISTER_PCI(net_ifcvf, rte_ifcvf_vdpa)所示。驱动定义了支持的设备vendor ID和device ID,当启动时通过“-a”参数传给DPDK进程允许使用的设备被匹配到时,就会调用ifcvf_pci_probe设备进行初始化。在ifcvf_pci_probe函数中,即会调用rte_vdpa_register_device来注册vdpa设备和函数接口;
2. vhost机制获取注册的vdpa设备并在frond-end建链时关联;
这个过程主要可以分成三个阶段:
1) DPDK的vdpa应用在调用前边提到的rte_vhost_driver_start启动vhost server之前通过函数 rte_vhost_driver_attach_vdpa_device,将vdpa驱动注册的vdpa设备添加到标识server的vhost_user_socket 句柄中描述数据结构中;
2) 当vhost server响应front-end驱动发起的建链请求时,即会在vhost_user_add_connection中执行vhost_attach_vdpa_device将vdpa设备复制给描述与这个front-end驱动关系链接的virtio_net数据结构中;
3) 当vhost_user_msg_handler处理front-end发送过来的控制消息时,会获取vdpa的设备,且第一执行时会通过“vdpa_dev->ops->dev_conf(dev->vid, vdpa_qid)” 调用来通知硬件设备配置好virtqueue。
3. vdpa硬件设备处理front-end驱动的IO请求并进行回应;
vdpa模式下,virtqueue的数据处理由硬件完成,因而在注册的 “notify_ops->new_device” 函数接口中,不需要执行virtqueue相关的处理操作。
DPDK代码中现可跑起来的vdpa的example在 “DPDK/examples/vdpa” 目录下。可以通过编译DPDK时由 “-Dexamples=vdpa” 来指定编译,并参考37. Vdpa Sample Application — Data Plane Development Kit 22.07.0-rc1 documentation 中的说明进行使用。
参考链接
dpdk对虚拟化的支持调研 - allcloud - 博客园
DPDK系列之十二:基于virtio、vhost和OVS-DPDK的容器数据通道_cloudvtech的博客-CSDN博客_dpdk容器化
DPDK系列之六:qemu-kvm网络后端的加速技术_cloudvtech的博客-CSDN博客_kvm加速
DPDK系列之十五:Virtio技术分析之一,virtio基础架构_cloudvtech的博客-CSDN博客_virtio
从dpdk1811看virtio1.1 的实现—packed ring-lvyilong316-ChinaUnix博客
Qemu模拟IO和半虚拟化Virtio的区别以及I/O半虚拟化驱动介绍_weixin_34051201的博客-CSDN博客
virtio-net - 网络半虚拟化 - 知乎
DPU和CPU互联的接口之争:Virtio还是SR-IOV? - 极术社区 - 连接开发者与智能计算生态
virtio简介(一)—— 框架分析 - Edver - 博客园
KVM之Virtio介绍 (十五) - 程序员大本营
虚拟化之Virtio-Net基础篇-51CTO.COM
KVM 虚拟化详解 - 知乎
Linux Kernel Vhost 架构 - 于杨 - 博客园
virtio,vhost 和vhost-user - allcloud - 博客园
详解vhost-user协议及其在OVS DPDK、QEMU和virtio-net驱动中的实现_redwingz的博客-CSDN博客_vhost协议