自虚拟化技术诞生起,提升虚拟化场景中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应用程序使用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并使用。
virtio-blk/virtio-scsi的用户态驱动
virtio-blk和virtio-scsi的用户态驱动代码在 “SPDK/module/bdev/virtio” 目录中,其主要处理对象是 “Virtio block device” 和 “Virtio SCSI” 的pci controller,将virtio pci controller初始化成spdk中的spdk_bdev设备对外提供服务。virtio-blk和virtio-scsi的设备的代码流程和前述的virtio-net的驱动流程类似。具体以virtio-blk的流程梳理概述如下:
1. 根据rpc命令传递的参数匹配pci设备并进行pci设备基本初始化;
响应rpc bdev_virtio_attach_controller命令pci/blk类型操作的入口函数是bdev_virtio_pci_blk_dev_create。该函数执行中调用了spdk_pci_device_attach(其中主要是通过DPDK的rte_eal_hotplug_add接口来实现)来执行pci设备匹配。当给定pci地址的设备扫描添加成功后,会回调指定的回调函数virtio_pci_dev_probe来完成设备合法性判断、bar空间映射、基本操作函数集添加等公共初始化步骤。
2. 创建virtio_blk描述设备,并完成virtio设备层级和blk层级的初始化;
在virtio_pci_dev_probe函数执行的末尾,会回调在bdev_virtio_pci_blk_dev_create中设置的回调函数 bdev_virtio_pci_blk_dev_create_cb,并在其中完成virtio_blk设备的创建和初始化。这个初始化的过程也基本可以分成两个部分:
1) 在virtio_pci_dev_init中完成virtio设备的基本初始化,如feature协商、virtio设备基本操作函数集(如modern_ops)添加、virtqueue创建及与back-end设备同步等;
2) 在virtio_blk_dev_init中创建并注册bdev到SPDK框架,让virtio设备对外呈现为bdev提供服务。其中指定的bdev的操作函数集virtio_fn_table可以响应SPDK应用层的读写及配置命令与back-end设备交互。
3. SPDK应用程序通过bdev块设备层的访问接口对块设备进行IO及控制类操作。
SPDK应用主要通过呈现的bdev对virtio 设备进行访问,IO读写及控制类接口与其它普通bdev无异,其后台由前述的virtio_fn_table中的函数来进行转化处理。
需要注意的是,在上述流程中,会以virtio_blk描述结构为io_device通过spdk_io_device_register函数进行注册,以便当SPDK应用程序执行IO操作时能够通过设置的回调函数bdev_virtio_blk_ch_create_cb,在各个使用的CPU核上将bdev的io_channel和virtqueue(vring)关联起来。
同时,在BSC(Big Spring Canyon , 一种基于FPGA实现的Intel Smart NIC)上实现的blk-net的设备本质上也是virtio_blk设备的一种特殊形态,其代码逻辑和上述描述的过程大体对应。
对于virtio-blk的用户态front-end驱动的使用,可以通过SPDK的test程序bdevperf来进行。比如可以通过json配置文件或者rpc命令指定要使用的virtio block device的pci addr信息及对应创建的spdk_bdev设备名称,然后对该bdev设备跑测试。具体的使用在本文后续章节会进行描述。
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进行链接。其二者之间的数据较互也通过内存共享映射来完成。
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代码中的实现,可以大致概述其相关的流程如下:
1. 在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)。
virtio-blk-user/virtio-scsi-user驱动
virtio-blk-user/virtio-scsi-user的驱动代码存在于 “SPDK/module/bdev/virtio” 目录中。也即在SPDK代码中,处理pci controller和直接对接vhost-user的virtio front-end 驱动的实现是在一起的,以不同的分支存在。若以virtio-blk-user驱动为例,可以将其相关流程概述如下:
1. 根据rpc命令传递的参数创建virtio_blk_dev并进行初始化;
响应rpc bdev_virtio_attach_controller命令user/blk类型操作的入口函数是bdev_virtio_user_blk_dev_create。该函数执行中通过调用virtio_user_blk_dev_create来创建virtio_blk_dev并进行初始化。主要的初始化步骤由virtio_user_dev_init和virtio_blk_dev_init这两部分完成,前者执行virtio 相关的初始化如与back-end设备建立unix socket链接等,后者完成blk设备相关的初始化如创建并注册bdev设备。
1) virtio_user_dev_init函数中初始化了virtio back-end设备相关的操作函数集virtio_user_ops以及用户态驱动和back-end进行交互的virtio_user_backend_ops的函数集ops_user,并与back-end设备建立链接;
2) virtio_blk_dev_init完成了virtqueue的初始化,创建并注册bdev到SPDK框架,让virtio设备对外呈现为bdev提供服务。其中指定的bdev的操作函数集virtio_fn_table可以响应SPDK应用层的读写及配置命令与back-end设备交互。
2. SPDK应用程序通过bdev块设备层的访问接口对块设备进行IO及控制类操作。
SPDK应用主要通过呈现的bdev对virtio 设备进行访问,IO读写及控制类接口与其它普通bdev无异,其后台由前述的virtio_fn_table中的函数来进行转化处理。
virtio-blk-user/virtio-scsi-user驱动可以通过SPDK的test程序bdevperf来进行验证,其实用方式与前述pci controller模式的virtio-blk/virtio-scsi用户态驱动类似,只是注意指定-t的参数为user即可。具体步骤可以参考后续验证章节的例子描述。
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以达到更安全的使用目的。
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_info[vdev->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” 来指定编译,并参考http://doc.dpdk.org/guides/sample_app_ug/vhost.html# 的说明来使用。
vhost-blk/vhost-scsi back-end设备
vhost-blk/vhost-scsi back-end设备的实现代码在 “SPDK/lib/vhost” 目录下。其主要是以spdk中可见的bdev作为实际载体,将其attach到vhost blk/scsi controller共同作为virtio back-end设备对外提供服务。以vhost-blk为例,可以将其实现流程基本概述如下:
1.响应rpc命令创建并初始化vhost_blk 设备;
SPDK中vhost-blk的功能以rpc方式显示调用,响应的函数为spdk_vhost_blk_construct。在其中,会创建spdk_vhost_blk_dev,并获取指定的spdk_bdev设备的访问句柄供后续响应、处理virtio front-end驱动的请求时使用。同时在在vhost_blk 设备 vhost_dev_register -> vhost_user_dev_register -> vhost_register_unix_socket的调用中执行前述的DPDK库提供的vhost的3个基本的初始化函数。
2.接收来自virtio front-end驱动的消息,建立front-end和back-end的关联;
当virtio front-end驱动的建链请求被DPDK库中的函数处理后,vhost-blk侧即可以接收并处理控制消息。如前述,函数vhost_user_msg_handler在处理接收到的消息时,第一次当对应的front-end的virtio_net设备(vhost-blk基于DPDK的vhost框架实现,在DPDK库中back-end 设备与每个front-end驱动的链接对应一个virtio_net的数据结构,并以此跟踪标识后续的消息)还未置位VIRTIO_DEV_RUNNING标志,则会执行vhost_user_msg_handler -> start_device -> vhost_blk_start -> vhost_blk_start_cb的调用流程的调用流程,并在其中分配用于IO处理的task_pool及注册poller用于处理virtqueue中的数据(poller的处理函数为vdev_worker)。该过程执行完成后即会置位VIRTIO_DEV_RUNNING标志。
3.在注册的poller函数中处理virtqueue的队列数据;
对于每个从virtqueue的vring中取出的IO数据会被归集在spdk_vhost_blk_task数据结构中,由函数spdk_vhost_blk_task提交到vhost-blk关联的spdk bdev设备进行处理。
SPDK代码中,vhost的app程序由 “SPDK/app/vhost” 目录下的文件编译,默认即会编译。其使用参考见https://spdk.io/doc/vhost.html。
在DPDK代码 “DPDK/examples/vhost-blk” 目录下,同样有一个vhost-blk的逻辑实现,其工作流程与SPDK中的vhost-blk的基本一致,主要的区别点在于在后端实际处理IO数据的对应bdev设备实现时没有经过SPDK bdev的框架,且目前代码中仅实现了一种通过申请的内存模拟的bdev类型。DPDK代码中vhost-blk的测试程序可以通过meson配置编译时加上“-Dexpamples=vhost_blk”参数来生成,使用说明可以参考http://doc.dpdk.org/guides/sample_app_ug/vhost_blk.html。
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驱动的为例,将其主要机制概述如下:
1. 匹配设备并调用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” 来指定编译,并参考http://doc.dpdk.org/guides/sample_app_ug/vdpa.html 中的说明进行使用。
用户态virtio虚拟化
实现验证实践
贰
在本章节将对前边介绍的virtio实现进行验证举例。涉及的具体命令以引号和斜体标识。
virtio用户态实现验证及实践
前边介绍了virtio用户态实现的情况,接下来本文将继续描述针对部分用户态实现场景的验证情况。主要介绍两个方面的案例:其一,后端以vhost-blk提供Virtio block设备给KVM-Qemu VM使用;其二,通过virtio-blk-user的方式访问后端vhost-blk。
后端使用vhost-blk提供Virtio block设备给KVM-Qemu VM使用
该项测试就是在Host系统下启动基于SPDK的vhost-blk程序,以Malloc类型的spdk_bdev作为实际的IO处理单元来向Qemu进程启动的VM提供virtio的设备。对应的步骤可以分为如下几个部分来介绍。
创建vhost-blk设备
创建vhost-blk设备的操作在Host系统下执行。
当前vhost-blk的功能主要通过rpc命令的方式来支持。在spdk编译前的configure命令执行时加上“--with-vhost”参数。在编译后的文件目录“build/bin”目录下会有vhost的可执行程序,运行vhost后可以通过rpc命令创建vhost-blk的back-end设备。相关步骤命令及说明罗列如下。
1. 开启大页配置:“./scripts/setup.sh”。
2. 运行vhost程序:“./build/bin/vhost -S /var/tmp -m 0x3”。//其中,-S参数指定用户态vhost back-end设备对应创建socket的路径,-m指定使用的cpu核。
3. 创建Malloc类型的spdk_bdev设备:“./scripts/rpc.py bdev_malloc_create 64 512 -b Malloc0”。
4. 创建vhost-blk设备并添加spdk_bdev设备:“./scripts/rpc.py vhost_create_blk_controller --cpumask 0x1 vhost.1 Malloc0”。//其中cpumask参数指定了vhost-blk 设备能够使用的CPU核。
该命令执行后可以在指定的目录下看到对应的用于访问vhost设备的文件(标识对应的vhost server unix socket的句柄)。如此处应为/var/tmp/vhost.1。
启动虚拟机将vhost-blk设备分配给虚拟机
将上述创建的vhost-blk设备对应socket的访问文件传递给Qemu,即可以实现将vhost-blk设备分配给虚拟机。
启动虚拟机命令:“qemu-system-x86_64 -cpu host -smp 8 -m 4G -object memory-backend-file,id=mem,size=4G,mem-path=/dev/hugepages,share=on -numa node,memdev=mem -drive file=/home/zj/qemu_test/qemu-img/fedora/Fedora35_qcow2.img,if=none,id=disk -device ide-hd,drive=disk,bootindex=0 -net user,hostfwd=tcp::10020-:22 -net nic -chardev socket,id=char0,path=/var/tmp/vhost.1 -device vhost-user-blk-pci,chardev=char0,id=blk0 -vnc :1 --enable-kvm”。
命令中“-object”开始到“share=on”的部分是设置Guest的内存能够被vhost-blk back-end设备访问,“-chardev”开始的部分是指定Host下创建的vhost-blk设备给虚拟机使用,且Qemu通过path指定的路径来访问。
虚拟机下使用virtio-blk用户态front-end驱动访问block设备
虚拟机启动后可以看到系统下有Virtio block device的pci controller。
图5. 在虚拟机下看到的virtio block device pci controller
在虚拟机里下载SPDK的代码,执行configure命令时加上“--with-virtio” 和 “--with-fio=”,其中fio-path是指fio源码的路径,主要是为了使用fio的头文件和库(因而需要先下载fio代码)。若使用rpc命令方式来通过bdevperf对用户态virtio front-end驱动创建出来的spdk_bdev跑测试的具体步骤可以归集如下(以下各个命令均在虚拟机下执行)。
1.设置大页及将pci设备交由uio驱动接管: ./scripts/setup.sh。
需要注意两个点:其一,VM中默认未添加IOMMU的模拟设备,即无vIOMMU,因而默认在VM下会使用uio驱动来将pci设备资源映射到用户态(如需要添加vIOMMU,可以在Qemu命令行中加上-device intel-iommu,intremap=on参数);其二,setup.sh脚本中会根据SPDK/include/spdk/pci_ids.h中定义的VIRTIO设备的device/vendor ID信息来rebind设备。
图6. 使用SPDK setup.sh脚本rebind virtio block device pci controller
2. 启动SPDK应用程序bdevperf:./test/bdev/bdevperf -q 128 -o 4096 -w randread -t 160 -S 5 -z; //其中-S参数指定多久显示一次测试结果,-z参数标识启动后先不进行测试,由rpc命令来启动测试。
3.指定virtio block device的pci addr创建virtio-blk类型的spdk_bdev:“./scripts/rpc.py bdev_virtio_attach_controller -t pci -a "00:04.0" -d blk virtioblk0”。 //其中,-t pci指定virtio front-end驱动访问的是pci的设备, -d指定是blk类型的设备,创建的bdev的名称为virtioblk0。
图7. 虚拟机中通过rpc命令创建virtio blk bdev
4. 通过bdevperf脚本启动测试:“PYTHONPATH=$PYTHONPATH:./scripts/ test/bdev/bdevperf/bdevperf.py -t 160 perform_tests”。在执行该命令时需要注意SPDK代码中各个模块的rpc脚本的存放目录rpc在哪个地方,对于rpc的路径为“./scripts/rpc/”时(此时bdevperf.py中import的是rpc)可以直接执行前边给出的命令;对于rpc的路径为“./python/spdk/rpc/”时(此时bdevperf.py中import的是spdk.rpc),命令需要变为“PYTHONPATH=$PYTHONPATH:./python/ test/bdev/bdevperf/bdevperf.py -s /var/tmp/spdk1.sock -t 200 perform_tests”。
图8. 对virtio bdev跑bdevperf的测试结果显示
通过virtio-blk-user访问vhost-blk的后端
这项测试就是在Host系统下先创建vhost-blk的用户态back-end的设备,然后通过在Host系统下启动另一个SPDK进程模拟bare-metal应用的进程来访问back-end的设备。所有的操作命令均在Host系统下执行。
创建vhost-blk设备
创建vhost-blk设备的步骤与前边的case中的操作一样,但注意编译SPDK时加上”--with-virtio”和“--with-fio=”选项,其中fio-path是指fio源码的路径,主要是为了使用fio的头文件和库(因而需要先下载fio代码)。
且在启动vhost应用程序时加上-g参数,指定SPDK程序创建一个hugetlbfs file。
在host下启动另外的SPDK进程充当front-end驱动跑测试
启动另外的SPDK进程模拟bare-metal应用。仍然以bdevperf为例来进行介绍。
1.启动bdevperf进程:“./test/bdev/bdevperf/bdevperf -r /var/tmp/spdk1.sock -q 128 -o 4096 -w randread -t 200 -S 5 -z -g”。 //-r参数给新的SPDK进程指定新的rpc调用访问的socket句柄对应的文件,以和vhost-blk设备创建的SPDK进程区分开来,-g参数设置与vhost进程一致,-S参数指定多久显示一次测试结果,-z参数标识启动后先不进行测试,由rpc命令来启动测试。
2. 创建virtio-blk类型的spdk_bdev设备:“./scripts/rpc.py -s /var/tmp/spdk1.sock bdev_virtio_attach_controller -t user -a /var/tmp/vhostblk0 -d blk virtioblk0”。//-s参数是指定rpc命令server对应的socket句柄文件,-t user指定virtio back-end为user模式,通过-a参数指定的文对应的unix socket访问,-d指定bdev为blk类型,名称为virtioblk0 。
3. 通过bdevperf.py脚本启动针对创建的virtio blk spdk_bdev的测试:“PYTHONPATH=$PYTHONPATH:./scripts/ test/bdev/bdevperf/bdevperf.py -s /var/tmp/spdk1.sock -t 200 perform_tests”。在执行该命令时需要注意SPDK代码中各个模块的rpc脚本的存放目录rpc在哪个地方,对于rpc的路径为“./scripts/rpc/”时(此时bdevperf.py中import的是rpc)可以直接执行前边给出的命令;对于rpc的路径为“./python/spdk/rpc/”时(此时bdevperf.py中import的是spdk.rpc),命令需要变为“PYTHONPATH=$PYTHONPATH:./python/ test/bdev/bdevperf/bdevperf.py -s /var/tmp/spdk1.sock -t 200 perform_tests”。
图9. 使用bdevperf对virtio-user模式下的virtio bdev设备跑测试的结果显示
注: 有关vfio-user用户态实现分析及验证的内容将会在【下篇】发出,敬请期待!
参考信息
1. https://www.redhat.com/en/blog/introduction-virtio-networking-and-vhost-net
2. https://www.youtube.com/watch?v=paTvtJ6JdAc
3. https://www.redhat.com/en/blog/journey-vhost-users-realm
4. https://www.redhat.com/en/blog/how-deep-does-vdpa-rabbit-hole-go
5. https://spdk.io/doc/vhost.html
6. http://doc.dpdk.org/guides/sample_app_ug/vdpa.html
7. https://github.com/tmakatos/qemu/blob/master/docs/devel/vfio-user.rst
8. https://github.com/nutanix/libvfio-user/blob/master/docs/spdk.md
转载须知
DPDK与SPDK开源社区
公众号文章转载声明
推荐阅读
从SPDK Blobstore到 Blob FS
Intel Scalable IOV介绍及应用实例
SPDK Delay Bdev 介绍及应用实例
支持非对称命名空间访问的SPDK多路径验证
点点“赞”和“在看”,给我充点儿电吧~