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;后端通过传输层接收通知并处理请求。下图展示了这一核心结构:

Guest 侧(前端) VirtIO 驱动 virtio_blk / virtio_net / virtio_scsi Virtqueue(虚拟队列) Descriptor Table + Avail Ring + Used Ring 特性协商 / 状态机 ACKNOWLEDGE → DRIVER → FEATURES → DRIVER_OK Host 侧(后端) VirtIO 设备实现 QEMU virtio-blk / vhost / vhost-user 后端处理引擎 消费描述符 → 执行真实 I/O → 回写 Used Ring 通知与中断 Kick(驱动→后端) / IRQ(后端→驱动) 传输层抽象 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:图形、输入、内存气球等辅助设备。
工程提示:在 QEMU 中,virtio-blk-pci 与 virtio-scsi-pci 的选择会影响性能与功能。virtio-blk 路径更短、开销更低;virtio-scsi 支持更多命令和热插拔,但多一层 SCSI 解析。存储虚拟化中常根据是否需要 SCSI 语义来决定。

五、案例:一次 virtio-blk 请求的完整路径

假设 Guest 发起一次 4KB 读请求,路径如下:

  1. Guest 文件系统 → 块层 → virtio_blk 驱动构造请求头、数据缓冲区、状态字节三个描述符。
  2. 驱动将描述符链放入 Avail Ring,并写 PCI 寄存器触发 Kick。
  3. QEMU 后端收到通知,从 Avail Ring 取出描述符,解析出读请求。
  4. 后端执行真实 I/O(如读取 qcow2 镜像或透传到物理盘)。
  5. 后端把结果写入数据缓冲区,更新 Used Ring,并注入 MSI-X 中断。
  6. 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

六、常见误区与复盘

误区一:把 VirtIO 当成“纯软件模拟”。VirtIO 是半虚拟化接口,Guest 需要安装对应驱动;没有驱动的 Guest 无法识别设备,会表现为“设备存在但无盘符”。
误区二:忽略 Legacy 与现代的差异。现代 PCI 设备用 capability 描述寄存器,Legacy 用固定偏移。驱动若只支持一种,可能在另一类设备上初始化失败。
误区三:认为 virtqueue 是“队列”就一定有锁。多队列场景下每个队列独立,但驱动与后端对 Avail/Used Ring 的访问仍需遵循内存屏障与通知抑制规则,否则会出现丢通知或重复处理。

复盘要点:VirtIO 的协议分层可以概括为“传输层负责发现与通知,virtqueue 负责数据搬运,设备类型负责语义”。掌握这三层,再去看 VirtIO Block 的具体请求格式、特性协商和性能调优,就会清晰很多。