3. VirtIO协议概览
在虚拟化环境中,Guest 操作系统要访问磁盘、网卡等设备,如果完全模拟真实硬件,每次 I/O 都要陷入 Hypervisor 并模拟寄存器行为,开销极高。VirtIO 的核心思路是:用一套标准化的、半虚拟化的设备接口,让 Guest 驱动与 Host 后端通过共享内存和通知机制直接协作,从而绕开昂贵的硬件模拟路径。理解 VirtIO 协议,是理解 VirtIO Block 与整个存储虚拟化栈的起点。
一、VirtIO 规范的演进脉络
VirtIO 并非一开始就是正式标准,它经历了从“事实约定”到“开放规范”的过程:
- VirtIO 0.9(Legacy):最早由 Rusty Russell 为 lguest、KVM 提出,定义了 PCI 传输、virtqueue 基本布局。它没有正式规范文档,靠 Linux 实现“约定俗成”,存在字节序、特性位等兼容性模糊点。
- VirtIO 1.0(2016,OASIS 标准):正式成为 OASIS 规范,明确了设备状态机、特性协商、virtqueue 内存布局、通知与中断机制,并引入现代 PCI 能力结构(capability)替代固定 BAR 偏移。
- VirtIO 1.1 / 1.2 / 1.3:持续增强,加入 packed virtqueue、更细粒度的特性位、设备管理接口、以及对 MMIO、PCI、Channel I/O 等多种传输层的统一描述。
对工程实践而言,最关键的分水岭是 Legacy 与现代(Modern):现代设备通过 PCI capability 描述寄存器位置,支持 64 位地址和更规范的特性协商;Legacy 设备则依赖固定偏移。驱动必须能识别两者,否则会出现“设备可见但无法初始化”的问题。
二、设备与驱动模型:前后端分离
VirtIO 采用典型的前端/后端(Frontend/Backend)模型:
- 前端(Guest 侧):即 VirtIO 驱动,运行在 Guest 内核中,如
virtio_blk、virtio_net。它负责把上层 I/O 请求翻译成 virtqueue 描述符,并触发通知。 - 后端(Host 侧):即 VirtIO 设备实现,常见于 QEMU(
virtio-blk-pci、vhost-user-blk)、vhost、vhost-user、DPDK 等。它消费 virtqueue,执行真实 I/O,并回写结果。 - 传输层(Transport):连接前后端的“总线”,如 PCI、MMIO、Channel I/O。它定义寄存器、通知方式与中断路径。
三者关系可以概括为:驱动通过传输层发现设备、协商特性、建立 virtqueue;后端通过传输层接收通知并处理请求。下图展示了这一核心结构:
三、传输层抽象:同一协议,多种总线
VirtIO 把“设备如何被发现、寄存器如何访问、通知如何传递”抽象为传输层。这样同一套 virtqueue 语义可以跑在不同总线上:
| 传输层 | 典型场景 | 寄存器访问方式 | 通知/中断 |
|---|---|---|---|
| VirtIO over PCI | QEMU/KVM 虚拟机最常见 | PCI BAR + capability 描述偏移 | PCI MSI-X 中断 + 写寄存器 Kick |
| VirtIO over MMIO | ARM 虚拟机、嵌入式 | 固定 MMIO 地址区间 | MMIO 写触发 + 平台中断 |
| VirtIO over Channel I/O | s390x 大型机 | CCW 命令通道 | 通道中断 |
无论哪种传输层,核心数据结构都是 virtqueue:驱动把请求放入 Avail Ring,后端从 Descriptor Table 读取缓冲区,处理完后写入 Used Ring 并触发中断。这种“共享内存 + 通知”的模型,是 VirtIO 高性能的根本原因。
四、常见 VirtIO 设备类型与场景
VirtIO 规范定义了多种设备类型,每种对应一个设备 ID 和一套特性位:
- virtio-blk:块设备,最常用于虚拟机磁盘。请求以“描述符链”形式提交,包含请求头、数据缓冲区、状态字节。
- virtio-net:网卡设备,支持多队列、校验和卸载、TSO 等特性。
- virtio-scsi:SCSI 控制器,支持更多 SCSI 命令和热插拔,适合需要完整 SCSI 语义的场景。
- virtio-fs:共享文件系统,用于 Host 与 Guest 之间高效共享目录。
- virtio-gpu / virtio-input / virtio-balloon:图形、输入、内存气球等辅助设备。
virtio-blk-pci 与 virtio-scsi-pci 的选择会影响性能与功能。virtio-blk 路径更短、开销更低;virtio-scsi 支持更多命令和热插拔,但多一层 SCSI 解析。存储虚拟化中常根据是否需要 SCSI 语义来决定。
五、案例:一次 virtio-blk 请求的完整路径
假设 Guest 发起一次 4KB 读请求,路径如下:
- Guest 文件系统 → 块层 →
virtio_blk驱动构造请求头、数据缓冲区、状态字节三个描述符。 - 驱动将描述符链放入 Avail Ring,并写 PCI 寄存器触发 Kick。
- QEMU 后端收到通知,从 Avail Ring 取出描述符,解析出读请求。
- 后端执行真实 I/O(如读取 qcow2 镜像或透传到物理盘)。
- 后端把结果写入数据缓冲区,更新 Used Ring,并注入 MSI-X 中断。
- Guest 驱动收到中断,检查 Used Ring,完成 I/O 并唤醒上层。
# 查看 Guest 中 virtio 设备与驱动绑定情况
lspci -nn | grep -i virtio
# 输出示例:
# 00:04.0 SCSI storage controller [0100]: Red Hat, Inc. Virtio block device [1af4:1001]
lsmod | grep virtio
# virtio_blk 20480 3
# virtio_pci 28672 0
# virtio_ring 32768 1 virtio_blk
六、常见误区与复盘
误区二:忽略 Legacy 与现代的差异。现代 PCI 设备用 capability 描述寄存器,Legacy 用固定偏移。驱动若只支持一种,可能在另一类设备上初始化失败。
误区三:认为 virtqueue 是“队列”就一定有锁。多队列场景下每个队列独立,但驱动与后端对 Avail/Used Ring 的访问仍需遵循内存屏障与通知抑制规则,否则会出现丢通知或重复处理。
复盘要点:VirtIO 的协议分层可以概括为“传输层负责发现与通知,virtqueue 负责数据搬运,设备类型负责语义”。掌握这三层,再去看 VirtIO Block 的具体请求格式、特性协商和性能调优,就会清晰很多。