virtio基础篇
1. virtio的提出
系统虚拟化技术是云计算最重要的核心技术之一。云计算平台的资源池化,资源统一管理以及后续的动态分配都是基于系统虚拟化技术才得以实现的。在计算机系统中,主要有计算资源,存储资源和网络资源。所以,系统虚拟化技术又可以细分为计算虚拟化,存储虚拟化和网络虚拟化。其中存储设备和网络设备一般都是以外设的形式和CPU通过I/O总线连接起来的,所以存储虚拟化和网络虚拟化又可以统一归类为I/O虚拟化技术。我们这里介绍的virtio就是当前最流行的I/O虚拟化技术。I/O虚拟化技术可以分成两种,一种是全虚拟化,另外一种是半虚拟化。
全虚拟化:Guest OS不知道自己是虚拟机,所以也就不需要修改Guest OS的驱动程序。这样Guest OS移植性好,但是虚拟机的I/O性能不高。
半虚拟化:Guest OS知道自己是虚拟机,通过前端驱动和后端模拟设备互相配合实现IO虚拟化。和全虚拟化相比,半虚拟化技术可以帮助大幅度提高虚拟机I/O的性能。目前在云计算场景中广泛使用的就是virtio在Linux上的具体实现,而且virtio已经成为虚拟化I/O技术的抽象接口规范和事实标准,即使非Linux系统也可以适用。
2. virtio总体设计思想
virtio的总体设计思想可以概括为以下四点:直观,高效,标准化,可扩展。1) 直观:virtio设备被设计的和之前的硬件设备一样,支持常用的总线机制,比如中断和DMA。这样的设计使得驱动开发人员能够快速上手,完成virtio设备驱动的开发。2) 高效:virtio设备能支持单独的输入和输出通道,在大量数据传送的时候也能保持高效。3) 标准化:对硬件架构和操作系统环境没有依赖,支持多种不同总线的传输机制(在virtio规范1.1中支持PCI, MMIO和Channel I/O总线。比如在那些不支持PCI的嵌入式设备还可以通过使用MMIO的方式使用virtio)。4) 可扩展:在virtio设备发现和设备初始化的时候通过引入特性位(Feature Bits)标识符实现动态适配的协商机制,能够保证virtio前端驱动和后端模拟设备之间的兼容性。
3. virtio基本原理
接下来,我们以目前使用最广泛的QEMU/KVM场景为例子进一步解释virtio的基本原理。虚拟机在物理主机上是一个QEMU的进程,运行在用户态。虚拟机内部的virtio前端驱动所申请的缓存被映射到设备空间中,也在QEMU的地址空间里,这样QEMU就可以通过共享内存的方式对这些缓存进行读写操作。通过这样的方式,实现了virtio前端驱动程序(虚拟机Linux内核的驱动)和后端模拟设备(QEMU后端设备模拟程序)之间数据传输的零复制,进而大幅度提高了虚拟机的I/O性能。
virtio前后端在QEMU/KVM中的实现
virtio在虚QEMU拟机内核中实现了前端驱动,在QEMU中实现了后端模拟设备,前后端之间通过虚拟队列(Virtqueue)通信交换数据。针对不同的总线机制,virtio设备有不同的实现方式,因为PCI设备是最广泛使用的设备,所以我们以virtio的PCI网卡为例子进行讲解。virtio-net前后端的实现如图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. 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的缓存,只能让后端写,前端读;
图3. Virtio规范中虚拟队列的定义
图4. used ring和available ring在virtio规范中的定义
下面我们以虚拟机发送数据为例,结合Linux 3.10和QEMU1.5的代码实现,详细说明一下在QEMU/KVM场景下具体的实现过程。
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中。
图5. virtio设备发送数据报文
图6. virtio前端驱动通知QEMU
图7. virtio通知函数最终会写寄存器
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等通信机制,因限于篇幅在这里不详细描述了。
图8. KVM中处理I/O操作导致的VMExit代码
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前端。
图9. virtio后端设备接收通知后的处理
图10. virtio-net预先注册的数据报接收函数
图
原文标题:孙雷: 虚拟化之——virtio-net基础篇
文章出处:【微信公众号:Linuxer】欢迎添加关注!文章转载请注明出处。
责任编辑:haq
全部0条评论
快来发表一下你的评论吧 !