virtio简介(一)--框架分析


1. 概述

     在传统的设备模拟中,虚拟机内部设备驱动完全不知道自己处在虚拟化环境中,所以I/O操作会完整的走 虚拟机内核栈->QEMU->宿主机内核栈,产生很多VM Exit和VM Entry,导致性能很差。Virtio方案旨在提高I/O性能。在改方案中虚拟机能够感知到自己处于虚拟化环境中,并且会加载相应的virtio总线驱动和virtio设备驱动,执行自己定义的 协议进行数据传输,减少VM Exit和VM Entry操作。

2. 架构

    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中读取数据和放入数据。
     

3. 原理 

    3.1 整体流程

    从代码上看,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就能直接从共享区域存取数据。