VirtIO 规范

VirtIO 是一种通用的半虚拟化的解决方案,它定义了一组规范:只要 Guest 和 Host 按照此规范进行数据操作,就可以使虚拟机绕过内核空间而直接和用户空间的进程间通信,以此达到提高 IO 性能的目的。

众所周知,真实硬件具有复杂的细节,模拟起来不仅复杂且效率低,当一个 Guest 访问到完全虚拟设备的寄存器 or 内存时,会 trap 到 Hypervisor 的设备模拟代码中,这一过程需要虚拟机内、外的多次切换过程,效率很低。 VirtIO 极大地简化了设备的实现,当我们只需要最基本的内外传输的功能时,VirtIO 是很好的选择。

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

VirtIO 规范目前由 OASIS 维护,目前已经更新到 virtio v1.3 版本。 VirtIO 规范中定义了多种实现方式,最常见的是 VirtIO over PCI,也有 VirtIO over MMIO 和 VirtIO over Channel I/O。

#VirtIO 架构

virtio-arch

VirtIO 支持虚拟多种类型的设备,例如块设备(virtio_blk),网卡设备(virtio_net),SCSI 总线设备(virtio_scsi)等。

位于 Guest 中的部分(即虚拟出来的设备)称为前端。对于 VirtIO 系列设备,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(VQ)

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

根据实际设备收发速率,可能存在一个或者多个 VirtQueue。比如 virtio-net 默认创建了两条 VirtQueue。

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

#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 – Hypervisor 后端 – Host 系统调用 – Host 内核,最后一步总是要切换到内核态。

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

#Vhost-user

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

与 Vhost 相反,Vhost-user 将后端数据面放在了用户态进程(e.g., DPDK,SPDK 的 BlobFS)中。