《重识云原生系列》专题索引:
virtio是一种I/O半虚拟化解决方案,是一套通用I/O设备虚拟化的程序,是对半虚拟化Hypervisior中的一组通用I/O设备的抽象。virtio分为前端和后端,一个backend组件和一个frontend组件。backend组件是virtio接口的host端,frontend组件是virtio的guest端。
Linux中的vhost驱动程序提供了内核态virtio设备仿真解决思路。 通常,QEMU在用户态进程模拟guest的I / O访问,而Vhost将virtio仿真代码放到了内核态中, 这就允许设备仿真代码直接调用内核子系统,而不是从用户态执行系统调用。
故在vhost-net/virtio-net体系中,vhost-net是在host kernel space中运行的backend,virtio-net是在guest kernel space中运行的frontend。
在Linux 3.0中,vhost代码存放在drivers / vhost /中。 所有设备使用的通用代码在drivers / vhost / vhost.c中。 这包括virtio vring访问功能,所有virtio设备需要为了与客户进行通信。 vhost-net代码存放在drivers / vhost / net.c中。
Virtio网络设备是一种虚拟的以太网卡,支持多队列的网络包收发。在virtio的架构中,有前后端驱动之分,所谓的前端驱动即是虚拟机中的virtio-net网卡驱动,而后端驱动的实现则多种多样,后端驱动的演化往往也标志着virtio网络的演化。
上图中的后端即是QEMU的实现版本,也是最原始的virtio-net后端(设备)。virtio标准将其对于队列的抽象称为Virtqueue。Vring即是对Virtqueue的具体实现。
一个Virtqueue由一个Available Ring和Used Ring组成,前者用于前端向后端发送数据,而后者反之。而在virtio网络中的TX/RX Queue均由一个Virtqueue实现。所有的I/O通信架构都有数据平面与控制平面之分。而对于virtio来说,通过PCI传输协议实现的virtio控制平面正是为了确保Vring能够用于前后端正常通信,并且配置好自定义的设备特性。而数据平面正是使用这些通过共享内存实现的Vring来实现虚拟机与主机之间的通信。
举例来说,当virtio-net驱动发送网络数据包时,会将数据放置于Available Ring中之后,会触发一次通知(Notification)。这时QEMU会接管控制,将此网络包传递到TAP设备。接着QEMU将数据放于Used Ring中,并发出一次通知,这次通知会触发虚拟中断的注入。虚拟机收到这个中断后,就会到Used Ring中取得后端已经放置的数据。至此一次发送操作就完成了。接收网络数据包的行为也是类似,只不过这次virtio-net驱动是将空的buffer放置于队列之中,以便后端将收到的数据填充完成而已。
vhost-net是处于内核态的后端驱动。QEMU实现的用户态virtio网络后端驱动所表现的网络性能并不如意,究其原因是因为频繁的用户态与内核态上下文切换,低效的数据拷贝、线程间数据同步等。于是演化出一种在内核态实现的virtio网络后端驱动方案,名为vhost-net。
与之而来的是一套新的vhost协议。vhost协议可以将允许VMM将virtio的数据面offload到另一个组件上,而这个组件正是vhost-net。
vhost-net驱动程序在主机上创建一个/ dev / vhost-net字符设备。 此字符设备作为配置vhost-net实例的接口。
当使用-netdev tap启动QEMU时,vhost = on将打开/ dev / vhost-net并使用几个ioctl调用初始化vhost-net实例。 这些必须将QEMU进程与vhost-net实例关联,准备virtio功能协商,并将guest虚拟机物理内存映射传递到vhost-net驱动程序。
在初始化期间,vhost驱动程序创建一个名为vhost- pid的内核线程,其中 pid是QEMU进程pid。 这个线程被称为“vhost工作线程”。 工作线程的任务是处理I / O事件并执行设备仿真。
Vhost不会模拟一个完整的virtio PCI适配器。 相反,它只将自己限制为virtqueue操作。 QEMU仍然用于执行virtio特性协商和实时迁移。 这意味着vhost驱动程序不是完整的virtio设备实现,它依赖于用户空间来处理控制平面,而数据平面在内核中完成。
vhost工作线程等待virtqueue kick,然后处理放在virtqueue上的缓冲区。 在vhost-net这意味着从tx virtque的数据包并且通过tap文件描述符发送它们。
文件描述符轮询也由vhost工作线程完成。 在vhost-net中,当数据包进入tap文件描述符时,工作线程被唤醒,并将其放置到rx virtqueue中,以便客户端可以接收它们。
vhost架构的一个令人惊讶的方面是它不以任何方式绑定到KVM。 Vhost是一个用户空间接口,不依赖于KVM内核模块。这意味着其他用户空间代码(如libpcap)在理论上可以使用vhost设备,如果他们发现它们方便的高性能I / O接口。
当客户端kick主机,因为它已经将缓冲区放在一个virtqueue,需要一种方式来通知vhost工作线程有工作要做。由于vhost不依赖于KVM内核模块,它们不能直接通信。相反,vhost实例使用vhost工作线程监视活动的eventfd文件描述符进行设置。 KVM内核模块具有称为ioeventfd的功能,用于获取eventfd并将其挂接到特定的guest虚拟机I / O出口。 QEMU用户空间注册一个ioeventfd用于VIRTIO_PCI_QUEUE_NOTIFY硬件寄存器访问,它能kick virtqueue。这是当guest虚拟机kick virtqueue,vhost工作线程被KVM内核模块通知的方式。
在从vhost工作线程返回到中断客户端时,使用了类似的方法。 Vhost需要一个“call”文件描述符,写这个文件描述符是为了去kick guest。 KVM内核模块有一个称为irqfd的功能,它允许eventfd触发客户机中断。 QEMU用户空间注册一个irqfd为virtio PCI设备中断并将其交给vhost实例。这是vhost工作线程如何中断客户端。
因此,vhost实例只知道guest虚拟机内存映射,kick eventfd和call eventfd。
在这套实现中,QEMU和vhost-net内核驱动使用ioctl来交换vhost消息,并且用eventfd来实现前后端的通知。当vhost-net内核驱动加载后,它会暴露一个字符设备在/dev/vhost-net。而QEMU会打开并初始化这个字符设备,并调用ioctl来与vhost-net进行控制面通信,其内容包含virtio的特性协商、将虚拟机内存映射传递给vhost-net等。对比最原始的virtio网络实现,控制平面在原有的基础上转变为vhost协议定义的ioctl操作(对于前端而言仍是通过PCI传输层协议暴露的接口),基于共享内存实现的Vring转变为virtio-net与vhost-net共享,数据平面的另一方转变为vhost-net,并且前后端通知方式也转为基于eventfd的实现。
如下图所示,可以注意到,vhost-net仍然通过读写TAP设备来与外界进行数据包交换。而读到这里的读者不禁要问,那虚拟机是如何与本机上的其他虚拟机与外界的主机通信的呢?答案就是通过类似Open vSwitch (OVS)之类的软件交换机实现的。OVS相关的介绍这里就不再赘述。
Vhost-net为后端的virtio网络架构图
DPDK系列之十二:基于virtio、vhost和OVS-DPDK的容器数据通道_cloudvtech的博客-CSDN博客_dpdk容器化
DPDK系列之六:qemu-kvm网络后端的加速技术_cloudvtech的博客-CSDN博客_dpdk kvm
DPDK系列之十五:Virtio技术分析之一,virtio基础架构_cloudvtech的博客-CSDN博客_virtio
从dpdk1811看virtio1.1 的实现—packed ring-lvyilong316-ChinaUnix博客
qemu-kvm中的virtio浅析 - 骑着蜗牛追太阳 - 博客园
Qemu模拟IO和半虚拟化Virtio的区别以及I/O半虚拟化驱动介绍_weixin_34051201的博客-CSDN博客
virtio-blk简介_sdulibh的博客-CSDN博客
DPU和CPU互联的接口之争:Virtio还是SR-IOV? - 极术社区 - 连接开发者与智能计算生态
virtio简介(一)—— 框架分析 - Edver - 博客园
vhost前后端(vhost_net/virtio_net)转发流程详解-lvyilong316-ChinaUnix博客