VirtIO 规范

VirtIO驱动定义了一组规范,只要guest和host按照此规范进行数据操作,就可以使虚拟机IO绕过内核空间而直接再用户空间的两个进程间传输数据,以此达到提高IO性能的目的。

真实硬件具有复杂的细节,模拟复杂且效率低,让Guest访问虚拟设备的寄存器or内存时,会trap到QEMU的设备模拟代码。当我们只需要最基本的内外传输的功能时,VirtIO是很好的选择。

VirtIO属于半虚拟化的一种,即Guest知道自己是虚拟出来的。

具体实现有多种,最常见的是VirtIO over PCI,也有VirtIO over MMIO 和 VirtIO over Channel IO。

#VirtIO 架构

virtio-arch

位于Guest中的部分称为前端。对于不同类型的设备有不同类型的驱动。源码位于Linux内核中。

不管是哪种类型的设备,底层的数据通道都是一样的。

位于Host上的部分为后端。源码位于hypervisor中。

#前端 VirtIO 设备

1
2
3
4
struct virtio_device {
struct list_head vqs; // VQ
u64 features;
};

features 是 virtio_driver & virtio_device 同时支持的通信特性,也就是前后端最终协商的通信特性。

1
2
3
4
5
struct virtio_driver {
int (*probe)(struct virtio_device *dev);
int (*scan)(struct virtio_device *dev);
int (*remove)(struct virtio_device *dev);
};

设备加载和注销等

#VirtQueue

Host和Guest之前通信的抽象数据通道称为 VirtQueue,具有双向数据收发的能力。

根据实际设备收发能力,可以拥有一个或者多个VirtQueue。

virtio-net默认有两条VirtQueue。

VQ创建于PCI设备的probe阶段,由Guest侧创建,然后把地址传递给后端。

#virtqueue_add() 发送数据

VQ发送函数,负责把一个SG list放进VQ。

#virtqueue_kick() 通知后端

#VRing

VirtQueue的实现是利用VRing,即vring_virtqueue。

VRing是实际进行数据传输的实现,整体结构包含三部门:Descriptor表和两个存放Descriptor*的Ring Buffer。

两个Ring分别是Avail Ring和Used Ring,用于双向传递数据。Avail用于从内向外,Used用于从外向内,是站在Host角度命名的。

每个Descriptor描述了一块数据区,主要包含Guest缓冲区的物理地址和长度。

Host会从Avail Ring中取Descriptor,收取or填充数据并放入Used Ring中。

#virtio-net

virtio-net把VQ进一步封装成send_queuereceive_queue

特定时机会调用try_fill_recv来向receive_queue中填充空buffer

#virtio-blk

#Virtio 演进

#Vhost

开发者发现,原始的数据通路 Guest-virtio-qemu后端-Host系统调用-Host内核,最后一步总是要切换到内核态。

为了省去这个开销,vhost技术将后端数据面放在Host内核中,由Host的一个内核线程负责处理IO。

#Vhost-user

Vhost需要使用1:1的内核线程,不够灵活。

Vhost-user将后端数据面放在了另一个用户态进程中(i.e., DPDK)。