《重识云原生系列》专题索引:
在第二章的计算章节,我们在KVM一节有介绍过QEMU,因相隔较远,这里再将其基本架构做一下简要回顾
virtio是通用虚拟化框架,在Qemu-kvm中的I/O是用qemu 来模拟的,性能比较差,用virtio来模拟I/O可以进一步提升I/O虚拟化的性能。
传统的qemu-kvm 工作模式:
1.Guest产生I/O请求,被KVM 截获;
2.Kvm 经过处理后将I/O请求存放在I/O共享页;
3.通知Qemu,I/O已经存入I/O共享页;
4.Qemu从I/O共享页拿到I/O请求;
5.Qemu模拟代码来模拟本次的I/O,并发送给相应的设备驱动;
6、7、8. 硬件去完成I/O操作并返回结果Qemu;
9. Qemu将结果放回I/O共享页;
10. Qemu通知Kvm去I/O共享页拿结果;
11. Kvm去I/O共享页拿到结果;
12 . Kvm将结果返回给Guest;
注意:
a)在这个操作中,客户机作为一个qemu进程在等待I/O时有可能被阻塞;
b)当客户机通过DMA访问大块内存时候,Qemu不会把结果放回I/O共享页,而是直接通过内存映射的方式将结果直接写到客户机的内存中去,然后通过KVM模块告诉客户机DMA操作已经完成;
在完全虚拟化的解决方案中,guest VM 要使用底层 host 资源,需要 Hypervisor 来截获所有的请求指令,然后模拟出这些指令的行为,这样势必会带来很多性能上的开销。virtio半虚拟化方案通过底层硬件辅助虚拟化的方式,将部分没必要虚拟化的指令通过硬件来完成,Hypervisor 只负责完成部分指令的虚拟化,以此提高IO性能。
纯软件模拟的设备和 Virtio 设备的区别:virtio 省去了纯模拟模式下的异常捕获环节,Guest OS 可以和 QEMU 的 I/O 模块直接通信。
要做到这点,需要 guest 来配合,guest 完成不同设备的前端驱动程序,Hypervisor 配合 guest 完成相应的后端驱动程序,这样两者之间通过某种交互机制就可以实现高效的虚拟化过程。
由于不同 guest 前端设备其工作逻辑大同小异(如块设备、网络设备、PCI设备、balloon驱动等),单独为每个设备定义一套接口实属没有必要,而且还要考虑扩平台的兼容性问题,另外,不同后端 Hypervisor 的实现方式也大同小异(如KVM、Xen等),这个时候,就需要一套通用框架和标准接口(协议)来完成两者之间的交互过程,virtio 就是这样一套标准,它极大地解决了这些不通用的问题。
virtio由Rusty Russell开发,对准虚拟化 hypervisor 中的一组通用模拟设备IO的抽象。Virtio是一种前后端架构,包括前端驱动(Guest内部)、后端设备(QEMU设备)、传输协议(vring)。框架如下图所示:
从总体上看,virtio 可以分为四层,包括前端 guest 中各种驱动程序模块,后端 Hypervisor (实现在Qemu上)上的处理程序模块,中间用于前后端通信的 virtio 层和 virtio-ring 层,virtio 这一层实现的是虚拟队列接口,算是前后端通信的桥梁,而 virtio-ring 则是该桥梁的具体实现,它实现了两个环形缓冲区,分别用于保存前端驱动程序和后端处理程序执行的信息。
其中前端驱动(frondend,如virtio-blk、virtio-net等)是在客户机中存在的驱动程序模块,而后端处理程序(backend)是在QEMU中实现的。在这前后端驱动之间,还定义了两层来支持客户机与QEMU之间的通信。其中,“virtio”这一层是虚拟队列接口,它在概念上将前端驱动程序附加到后端处理程序。一个前端驱动程序可以使用0个或多个队列,具体数量取决于需求。例如,virtio-net网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而virtio-blk块驱动程序仅使用一个虚拟队列。虚拟队列实际上被实现为跨越客户机操作系统和hypervisor的衔接点,但它可以通过任意方式实现,前提是客户机操作系统和virtio后端程序都遵循一定的标准,以相互匹配的方式实现它。而virtio-ring实现了环形缓冲区(ring buffer),用于保存前端驱动和后端处理程序执行的信息,并且它可以一次性保存前端驱动的多次I/O请求,并且交由后端去批量处理,最后实际调用宿主机中设备驱动实现物理上的I/O操作,这样做就可以根据约定实现批量处理而不是客户机中每次I/O请求都需要处理一次,从而提高客户机与hypervisor信息交换的效率。
严格来说,virtio 和 virtio-ring 可以看做是一层,virtio-ring 实现了 virtio 的具体通信机制和数据流程。或者这么理解可能更好,virtio 层属于控制层,负责前后端之间的通知机制(kick,notify)和控制流程,而 virtio-vring 则负责具体数据流转发。
Virtio半虚拟化驱动的方式,可以获得很好的I/O性能,其性能几乎可以达到和native(即:非虚拟化环境中的原生系统)差不多的I/O性能。所以,在使用KVM之时,如果宿主机内核和客户机都支持virtio的情况下,一般推荐使用virtio以达到更好的性能。当然,virtio也是有缺点的,它必须要客户机安装特定的Virtio驱动使其知道是运行在虚拟化环境中,且按照Virtio的规定格式进行数据传输,不过客户机中可能有一些老的Linux系统不支持virtio和主流的Windows系统需要安装特定的驱动才支持Virtio。不过,较新的一些Linux发行版(如RHEL 6.3、Fedora 17等)默认都将virtio相关驱动编译为模块,可直接作为客户机使用virtio,而且对于主流Windows系统都有对应的virtio驱动程序可供下载使用。
从代码上看,virtio的代码主要分两个部分:QEMU和内核驱动程序。Virtio设备的模拟就是通过QEMU完成的,QEMU代码在虚拟机启动之前,创建虚拟设备。虚拟机启动后检测到设备,调用内核的virtio设备驱动程序来加载这个virtio设备。
对于KVM虚拟机,都是通过QEMU这个用户空间程序创建的,每个KVM虚拟机都是一个QEMU进程,虚拟机的virtio设备是QEMU进程模拟的,虚拟机的内存也是从QEMU进程的地址空间内分配的。
VRING是由虚拟机virtio设备驱动创建的用于数据传输的共享内存,QEMU进程通过这块共享内存获取前端设备递交的IO请求。
如下图所示,虚拟机IO请求的整个流程:
1) 虚拟机产生的IO请求会被前端的virtio设备接收,并存放在virtio设备散列表scatterlist里;
2) Virtio设备的virtqueue提供add_buf将散列表中的数据映射至前后端数据共享区域Vring中;
3) Virtqueue通过kick函数来通知后端qemu进程。Kick通过写pci配置空间的寄存器产生kvm_exit;
4) Qemu端注册ioport_write/read函数监听PCI配置空间的改变,获取前端的通知消息;
5) Qemu端维护的virtqueue队列从数据共享区vring中获取数据;
6) Qemu将数据封装成virtioreq;
7) Qemu进程将请求发送至硬件层。
前后端主要通过PCI配置空间的寄存器完成前后端的通信,而IO请求的数据地址则存在vring中,并通过共享vring这个区域来实现IO请求数据的共享。
从上图中可以看到,Virtio设备的驱动分为前端与后端:前端是虚拟机的设备驱动程序,后端是host上的QEMU用户态程序。为了实现虚拟机中的IO请求从前端设备驱动传递到后端QEMU进程中,Virtio框架提供了两个核心机制:前后端消息通知机制和数据共享机制。
消息通知机制,前端驱动设备产生IO请求后,可以通知后端QEMU进程去获取这些IO请求,递交给硬件。
数据共享机制,前端驱动设备在虚拟机内申请一块内存区域,将这个内存区域共享给后端QEMU进程,前端的IO请求数据就放入这块共享内存区域,QEMU接收到通知消息后,直接从共享内存取数据。由于KVM虚拟机就是一个QEMU进程,虚拟机的内存都是QEMU申请和分配的,属于QEMU进程的线性地址的一部分,因此虚拟机只需将这块内存共享区域的地址传递给QEMU进程,QEMU就能直接从共享区域存取数据。
接下来,我们以目前使用最广泛的QEMU/KVM场景为例子进一步解释virtio的基本原理。虚拟机在物理主机上是一个QEMU的进程,运行在用户态。虚拟机内部的virtio前端驱动所申请的缓存被映射到设备空间中,也在QEMU的地址空间里,这样QEMU就可以通过共享内存的方式对这些缓存进行读写操作。通过这样的方式,实现了virtio前端驱动程序(虚拟机Linux内核的驱动)和后端模拟设备(QEMU后端设备模拟程序)之间数据传输的零复制,进而大幅度提高了虚拟机的I/O性能。
virtio在虚QEMU拟机内核中实现了前端驱动,在QEMU中实现了后端模拟设备,前后端之间通过虚拟队列(Virtqueue)通信交换数据。针对不同的总线机制,virtio设备有不同的实现方式,因为PCI设备是最广泛使用的设备,所以我们以virtio的PCI网卡为例子进行讲解。virtio-net前后端的实现如图2-1所表示。
图2-1. virtio-net前后端在QEMU/KVM中的实现
在虚拟机启动之后,virtio前端驱动会把自己标识成一个PCI设备,其中包括PCI厂家标识符,PCI设备标识符。这样虚拟机的内核可以基于这个标识符判断使用哪种驱动程序。因为虚拟机中的Linux内核已经包括了virtio驱动程序,所以virtio驱动会被调用去初始化这个virtio设备。除了完成PCI设备通常的初始化操作之外,virtio前端驱动还在初始化的过程中和后端设备模拟程序协商特性位(Feature Bits),并把最终的结果记录在设备状态(Device Status)中。具体的实现代码可以参考内核代码在linux-3.10.0-957.1.3.el7/drivers/virtio/virtio.c中的virtio_dev_probe()函数,如图2-2所示。
图2-2. virtio设备初始化,协商特性并最终设置设备状态位
这里有两个比较重要的数据结构需要介绍一下。
虚拟队列(Virtqueue)是被用来在virtio前端驱动和virtio后端模拟设备之间双向数据传输的数据结构。每个virtio设备都维护着一个或者多个虚拟队列。以virtio网络设备为例,它至少维护两个虚拟队列,一个用来存储要发送的数据,一个用来存储接收的收据。每个虚拟队列数据结构都由三部分组成,分别是descriptor table,available ring和used ring。
图2-3. Virtio规范中虚拟队列的定义
图2-4. used ring和available ring在virtio规范中的定义
下面我们以虚拟机发送数据为例,结合Linux 3.10和QEMU1.5的代码实现,详细说明一下在QEMU/KVM场景下具体的实现过程。
QEMU虚拟机内的virtio网卡驱动在初始化的时候,会和其他的网络驱动一样注册发送函数xmit_skb()。具体的实现如图5,6所示,所以虚拟机内的virtio网卡发送数据的时候,会调用预先注册的函数xmit_skb()。要发送的数据会调用virtqueue_add_outbuf()放置在available ring中。最终在virtqueue_add_outbuf()函数中,会调用virtqueue_kick()函数,并进一步调用virtqueue_notify()函数。在virtqueue_notify()函数中,如图7所表示的virtio前端通过I/O写寄存器的方式通知virtio后端模拟设备。这部分前端驱动的代码在drivers/virtio/virtio_ring.c中。
图2-5. virtio设备发送数据报文
图2-6. virtio前端驱动通知QEMU
图2-7. virtio通知函数最终会写寄存器
虚拟机virtio前端驱动程序发送通知的函数最终是执行I/O写指令。在QEMU/KVM环境中,虚拟机执行I/O指令,会触发VMExit。在KVM的VMExit代码中会判断退出的原因,I/O操作对应的处理函数是handle_io(),具体的代码在linux-3.10.0-957.1.3.el7/arch/x86/kvm/vmx.c,如图8所示。最终再经由KVM通知到QEMU中的virtio-net后端模拟设备,其中还涉及到KVM和eventfd等通信机制,因限于篇幅在这里不详细描述了。
图2-8. KVM中处理I/O操作导致的VMExit代码
如图8所表示的,在接收到来自KVM的通知之后,QEMU后端设备模拟程序会调用virtio_queue_host_notifier_read()函数,进而调用预先注册的函数virtio_ioprt_write()处理来自前端驱动的I/O写操作。在接收到前端发来的通知之后,会调用virtio_queue_notify()函数进行处理。在接收网络数据包的时候,virtio_queue_notify()会再进一步调用virtio-net网络设备注册的数据包接收函数virtio_net_handle_rx()。如图9所表示的,在qemu_flush_queued_packets()中,QEMU会把数据复制到对应的队列中(QEMU中对应后端的不同tap都维护着不同的队列),之后再调用qemu_notify_event()通知virtio前端,最终会调用kvm_set_irq()触发vCPU的中断的方式通知virtio前端。
图2-9. virtio后端设备接收通知后的处理
图2-10. virtio-net预先注册的数据报接收函数
图2-11. 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原理 - 简书
virtio-blk简介_sdulibh的博客-CSDN博客
virtio-net原理(二) - 蓝色魔兽 - 博客园
virtio-net - 网络半虚拟化 - 知乎
DPU和CPU互联的接口之争:Virtio还是SR-IOV? - 极术社区 - 连接开发者与智能计算生态
virtio简介(一)—— 框架分析 - Edver - 博客园
virtio简介(二) —— virtio-balloon guest侧驱动
virtio简介(三) —— virtio-balloon qemu设备创建
virtio简介(四)—— 从零实现一个virtio设备 - Edver - 博客园
virtio简介(五)—— virtio_blk设备分析 - Edver - 博客园
KVM之Virtio介绍 (十五) - 程序员大本营
虚拟化之Virtio-Net基础篇-51CTO.COM
KVM 虚拟化详解 - 知乎
virtio 与vhost_net介绍_魏言华的博客-CSDN博客_vhost-net
用户态虚拟化IO通道实现概览及实践(上)
用户态虚拟化IO通道实现概览及实践(下)