自虚拟化技术诞生起,提升虚拟化场景中IO设备性能和驱动的兼容性、可扩展性一直是备受关注和追求的目标。随着半虚拟化技术的出现,virtio设备及驱动也很快流行并逐步变成了虚拟化应用中的主要IO通道形态。例如,virtio现已支持实现的设备涵盖了网络设备(virtio-net)、块设备(virtio-blk)、串口设备在(virtio-console)等等。其后,通过DPDK/SPDK技术加持,virtio技术全用户态化的实现也如火如荼地落地并广泛应用。
不仅如此,沿着虚拟IO全用户态化的思路,vfio-user技术也应运而生,且被SPDK集成用于为虚拟机提供了高性能模拟的NVMe设备。
接下来,本文将主要结合virtio和vfio-user的角度来对主要的虚拟化IO通道的用户态实现方式进行简单的介绍。
壹
virtio的
用户态实现分析
对于virtio的用户态实现技术,此处将主要从virtio标准、virtio front-end和back-end的用户态实现情况分别进行说明。
virtio标准
virtio机制让客户机(Guest)系统知道自己是运行在虚拟化环境中,通过与hypervisor配合协作,借助CPU提供的虚拟化技术(如Intel VT-d或AMD-v)达到更好的性能。同时,virtio机制也给众多的虚拟化平台和系统提供了统一的IO设备及驱动模型,提升了多平台&多系统扩展性、兼容性,降低了跨系统移植复用时的复杂度。
virtio的机制从2008年在论文中提出后,已经过了v0.95、v1.0 和v1.1 这3个标准。其主要构成可以大致分为前端(front-end)驱动、传输队列(virtqueue)和后端(back-end) 设备这几个部分。Front-end驱动运行在虚拟机(Guest)系统或者bare-metal应用进程中,back-end设备存在于主机(Host)系统的hypervisor进程(如Qemu)或者单独的进程(或模块)中,而virtqueue则主要通过内存共享映射在front-end驱动和back-end设备之间传递数据。virtqueue的主要实现结构是vring,在virtio的的各个标准版本中有split和packed两种实现方式。
图1. Virtio机制主要结构
(transport即指virtqueue,通过virng实现)
virtio的back-end主要实现
为了尽可能的提升性能,virtio back-end设备的实现方式一直在不断变化。以KVM-Qemu应用场景为例,back-end设备的实现就从最初完全在Qemu进程中逐步变为“控制面通过Qemu、IO面基于独立进程或模块(如vhost)”以及“控制面基于Qemu、IO面基于硬件(vDPA)”的形式。
完全基于Qemu的实现
在back-end设备完全基于Qemu的实现方式中,virtqueue back-end侧的处理在Qemu进程中完成,涉及的数据由Qemu进程解析后再和实际进行数据处理的设备驱动交互。虽然这种方式使用过程中Qemu进程可以不依赖于其他的进程或模块,但这的确也是性能最差的virtio back-end设备实现方式。
图2. 完全基于Qemu的virtio实现举例
(IO路径如见虚线)
Vhost实现
vhost实现就是将virtqueue的IO面(数据处理逻辑)从Qemu进程中拿出来由Host下的其他模块或者进程来实现,控制面(如virtqueue的参数协商等)仍需经过Qemu进程完成。得益于CPU的虚拟化技术,可以实现Host系统和Guest系统下内存地址的直接映射和快速转换,从而提升了IO访问的性能。
vhost的IO处理逻辑最初是在内核态实现,Qemu通过ioctl的机制和内核态vhost模块进行交互。随着后续的发展,逐步有了基于用户态进程的实现 --- Qemu直接在用户态通过unix socket与vhost进程进行交互 。
对基于用户态实现的vhost,其既可以通过Qemu等hypervisor向Guest系统呈现pci形态的virtio设备来与运行在Guest下的virtio front-end驱动交互;也能够单独以user通道(基于unix socket)的方式来向其他进程(例如,与Qemu无关的Container等 bare-metal模式应用)提供virtio back-end设备功能。对于第二种情况,其对应的virtio front-end 驱动也是基于virtio-user类型的实现。
图3. vhost back-end实现示例
vDPA实现
vDPA(virtual Data Path Acceleration)就是在vhost的思路上更进一步,将back-end侧对virtqueue的数据处理逻辑完全由硬件来实现。这样可以进一步降低Host侧CPU的消耗,从而提升性能。不仅如此,这种实现方式还带来了其他的价值。vDPA的实现和SR-IOV应用时的pci pass-through的方式有点类似(不过SR-IOV方式下控制面和数据面都卸载到硬件来执行)。但相较于pci pass-through模式在Guest系统下应用时不同设备需要各自不同驱动来支持以及无法实现VM在线迁移(live migration)的限制,vDPA使得在Guest系统下使用统一的front-end驱动来支持不同的设备以及实现在线迁移特性成为可能。
virtio用户态virtio front-end驱动\back-end设备实现
鉴于DPDK/SPDK用户态驱动在性能优化方面的独到特性,virtio front-end驱动和back-end设备的用户态版本,主要基于DPDK/SPDK框架来进行开发。接下来本文将从front-end驱动和back-end设备的角度来分别介绍virtio的用户态实现及其主要的代码结构和运行机制。
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这几种。
virtio-net的用户态驱动
virtio-net用户态驱动的实现代码在 “DPDK/drivers/net/virtio” 目录下,其主要处理对象是 “Virtio network device” 的pci controller。结合代码可以大致将相关流程梳理如下:
1. 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应用程序使