VirtIO 规范
VirtIO 驱动定义了一组规范,只要 guest 和 host 按照此规范进行数据操作,就可以使虚拟机 IO 绕过内核空间而直接再用户空间的两个进程间传输数据,以此达到提高 IO 性能的目的。
真实硬件具有复杂的细节,模拟复杂且效率低,让 Guest 访问虚拟设备的寄存器 or 内存时,会 trap 到 QEMU 的设备模拟代码。当我们只需要最基本的内外传输的功能时,VirtIO 是很好的选择。
VirtIO 属于半虚拟化的一种,即 Guest 知道自己是虚拟出来的。
具体实现有多种,最常见的是 VirtIO over PCI,也有 VirtIO over MMIO 和 VirtIO over Channel IO。
#VirtIO 架构
位于 Guest 中的部分称为前端。对于不同类型的设备有不同类型的驱动。源码位于 Linux 内核中。
不管是哪种类型的设备,底层的数据通道都是一样的。
位于 Host 上的部分为后端。源码位于 hypervisor 中。
#前端 VirtIO 设备
1 | struct virtio_device { |
features
是 virtio_driver & virtio_device 同时支持的通信特性,也就是前后端最终协商的通信特性。
1 | struct virtio_driver { |
设备加载和注销等
#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_queue
和receive_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)。