《重识云原生系列》专题索引:
- 第一章——不谋全局不足以谋一域
- 第二章计算第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.7.2节——virtio网络半虚拟化简介
- 第四章云网络4.7.3节——Vhost-net方案
- 第四章云网络4.7.4节vhost-user方案——virtio的DPDK卸载方案
- 第四章云网络4.7.5节vDPA方案——virtio的半硬件虚拟化实现
- 第四章云网络4.7.6节——virtio-blk存储虚拟化方案
- 第四章云网络4.7.8节——SR-IOV方案
- 第四章云网络4.7.9节——NFV
- 第四章云网络4.8.1节——SDN总述
- 第四章云网络4.8.2.1节——OpenFlow概述
- 第四章云网络4.8.2.2节——OpenFlow协议详解
- 第四章云网络4.8.2.3节——OpenFlow运行机制
- 第四章云网络4.8.3.1节——Open vSwitch简介
- 第四章云网络4.8.3.2节——Open vSwitch工作原理详解
- 第四章云网络4.8.4节——OpenStack与SDN的集成
- 第四章云网络4.8.5节——OpenDayLight
- 第四章云网络4.8.6节——Dragonflow
1 前言
1.1 QEMU回顾
在第二章的计算章节,我们在KVM一节有介绍过QEMU,因相隔较远,这里再将其基本架构做一下简要回顾
1.1.1 Qemu中的I/O请求工作流程
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操作已经完成;
1.1.2 virtio诞生背景
在完全虚拟化的解决方案中,guest VM 要使用底层 host 资源,需要 Hypervisor 来截获所有的请求指令,然后模拟出这些指令的行为,这样势必会带来很多性能上的开销。virtio半虚拟化方案通过底层硬件辅助虚拟化的方式,将部分没必要虚拟化的指令通过硬件来完成,Hypervisor 只负责完成部分指令的虚拟化,以此提高IO性能。
纯软件模拟的设备和 Virtio 设备的区别:virtio 省去了纯模拟模式下的异常捕获环节,Guest OS 可以和 QEMU 的 I/O 模块直接通信。
要做到这点,需要 guest 来配合,guest 完成不同设备的前端驱动程序,Hypervisor 配合 guest 完成相应的后端驱动程序,这样两者之间通过某种交互机制就可以实现高效的虚拟化过程。
1.2 virtio工作原理
virtio由Rusty Russell开发,对准虚拟化 hypervisor 中的一组通用模拟设备IO的抽象。Virtio是一种前后端架构,包括前端驱动(Guest内部)、后端设备(QEMU设备)、传输协议(vring)。框架如下图所示:
- 前端驱动:
- 虚拟机内部的 virtio模拟设备对应的驱动。作用为接收用户态的请求,然后按照传输协议对请求进行封装,再写I/O操作,发送通知到QEMU后端设备。
- 后端设备:
- 在QEMU中创建,用来接收前端驱动发送的I/O请求,然后按照传输协议进行解析,在对物理设备进行操作,之后通过终端机制通知前端设备。
- 传输协议:
- 使用virtio队列(virtio queue,virtqueue)完成。设备有若干个队列,每个队列处理不同的数据传输(如virtio-balloon包含ivq、dvq、svq三个)。
- virtqueue通过vring实现。Vring是虚拟机和QEMU之间共享的一段环形缓冲区,QEMU和前端设备都可以从vring中读取数据和放入数据。
1.2.1 基本原理
从总体上看,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 则负责具体数据流转发。
1.2.2 优缺点
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驱动程序可供下载使用。
1.2.3 virtio 数据流交互机制
从代码上看,virtio的代码主要分两个部分:QEMU和内核驱动程序。Virtio设备的模拟就是通过QEMU完成的,QEMU代码在虚拟机启动之前,创建虚拟设备。虚拟机启动后检测到设备,调用内核的virtio设备驱动程序来加载这个virtio设备。
对于KVM虚拟机,都是通过QEMU这个用户空间程序创建的,每个KVM虚拟机都是一个QEMU进程,虚拟机的virtio设备是QEMU进程模拟的,虚拟机的内存也是从QEMU进程的地址空间内分配的。
VRING是由虚拟机virtio设备驱动创建的用于数据传输的共享内存,QEMU进程通过这块共享内存获取前端设备递交的IO请求。
如下图所示,虚拟机IO请求的整个流程:
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就能直接从共享区域存取数据。
2 virtio机制详解
接下来,我们以目前使用最广泛的QEMU/KVM场景为例子进一步解释virtio的基本原理。虚拟机在物理主机上是一个QEMU的进程,运行在用户态。虚拟机内部的virtio前端驱动所申请的缓存被映射到设备空间中,也在QEMU的地址空间里,这样QEMU就可以通过共享内存的方式对这些缓存进行读写操作。通过这样的方式,实现了virtio前端驱动程序(虚拟机Linux内核的驱动)和后端模拟设备(QEMU后端设备模拟程序)之间数据传输的零复制,进而大幅度提高了虚拟机的I/O性能。
2.1 virtio前后端在QEMU/KVM中的实现
virtio在虚QEMU拟机内核中实现了前端驱动,在QEMU中实现了后端模拟设备,前后端之间通过虚拟队列(Virtqueue)通信交换数据。针对不同的总线机制,virtio设备有不同的实现方式,因为PCI设备是最广泛使用的设备,所以我们以virtio的PCI网卡为例子进行讲解。virtio-net前后端的实现如图2-1所表示。
图2-1. virtio-net前后端在QEMU/KVM中的实现
- virtio设备发现和初始化
在虚拟机启动之后,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设备初始化,协商特性并最终设置设备状态位
这里有两个比较重要的数据结构需要介绍一下。
- 特性位(Feature Bits),用来表示设备所能支持的特性。在virtio设备初始化的时候,驱动会去读取特性位,并且告诉设备哪些是它能接受特性。如果后端模拟设备升级了,使能了某个新特性,但是虚拟机里面的驱动还不能识别的话,那么两者就是通过特性进行协商。
- 设备状态位(Device Status),用来表示设备的当前状态。在virtio设备发现,初始化和特性协商的过程中,都可以查看设备状态位的方式查看virtio设备的状态。比如,virtio_CONFIG_S_FEATURES_OK表示特性协商成功,virtio_CONFIG_S_DRIVER_OK表示驱动已经配置成功。
- virtio网卡发送数据处理过程
虚拟队列(Virtqueue)是被用来在virtio前端驱动和virtio后端模拟设备之间双向数据传输的数据结构。每个virtio设备都维护着一个或者多个虚拟队列。以virtio网络设备为例,它至少维护两个虚拟队列,一个用来存储要发送的数据,一个用来存储接收的收据。每个虚拟队列数据结构都由三部分组成,分别是descriptor table,available ring和used ring。
- descriptor table用来描述一组缓存,是virtio前端驱动创建的。和缓存相关的信息主要是物理地址和长度;缓存数组的数量是有队列大小(Queue Size)决定的;
- available ring是给virtio前端驱动给virtio后端模拟设备传输数据时使用的,比如虚拟机用virtio-net设备发送数据的时候,所发送的数据就会先缓存在这里,再通知virtio后端模拟设备来读取;也就是说available ring的缓存,只能让前端写,后端读;
- used ring是给virtio后端模拟设备给virtio前端驱动传输数据时使用的,比如virtio后端模拟设备从tap网络接口收到数据之后,会把收到的数据缓存到这里,再通知virtio前端驱动程序;也就是说used ring的缓存,只能让后端写,前端读;
图2-3. Virtio规范中虚拟队列的定义
图2-4. used ring和available ring在virtio规范中的定义
2.2 具体实现示例
下面我们以虚拟机发送数据为例,结合Linux 3.10和QEMU1.5的代码实现,详细说明一下在QEMU/KVM场景下具体的实现过程。
2.2.1 virtio前端驱动填充数据包,并发出通知
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通知函数最终会写寄存器
2.2.2 KVM截获I/O后通知后端
虚拟机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代码
2.2.3 virtio后端模拟设备处理通知
如图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简介_sdulibh的博客-CSDN博客
DPU和CPU互联的接口之争:Virtio还是SR-IOV? - 极术社区 - 连接开发者与智能计算生态
virtio简介(一)—— 框架分析 - Edver - 博客园
virtio简介(二) —— virtio-balloon guest侧驱动
virtio简介(三) —— virtio-balloon qemu设备创建
virtio简介(四)—— 从零实现一个virtio设备 - Edver - 博客园
virtio简介(五)—— virtio_blk设备分析 - Edver - 博客园