3. VirtIO 概述
VirtIO 是 Linux 虚拟化体系中最重要的设备 I/O 标准之一。它并不试图让虚拟机完全“假装”成物理硬件,而是让 Guest 操作系统明确知道自己运行在虚拟化环境中,并与 Hypervisor 协作完成 I/O。这种设计思想称为半虚拟化。理解 VirtIO,需要同时抓住三条主线:半虚拟化思想、前后端驱动模型、设备类型与协议族。
一、核心原理:半虚拟化思想
传统全虚拟化设备(如 e1000 网卡、IDE 磁盘)对 Guest 暴露的是真实硬件寄存器模型。Guest 使用原生驱动访问这些寄存器时,会频繁触发 VM Exit,由 Hypervisor 模拟硬件行为。这种“陷入—模拟—返回”的路径开销很大,尤其在高吞吐场景下会成为瓶颈。
VirtIO 的思路是:既然 Guest 已经知道自己是虚拟机,就不必再模拟真实硬件。双方约定一套共享内存中的virtqueue(虚拟队列)作为数据通道,Guest 把请求描述符写入队列并通知后端,后端处理后回写完成状态并中断 Guest。这样,数据面大部分工作通过共享内存完成,只有“通知”和“中断”需要陷入,显著减少 VM Exit。
二、前后端驱动模型
VirtIO 采用典型的前后端分离架构:
- 前端驱动(Frontend / Guest Driver):运行在 Guest 内核中,如 virtio-net、virtio-blk、virtio-scsi。它负责把上层 I/O 请求封装成 virtqueue 描述符,并触发 kick。
- 后端设备(Backend / Device):运行在 Host 用户态或内核态,如 QEMU 的 virtio-net 设备、vhost-net、vhost-user、DPDK vhost。它消费 virtqueue,完成实际 I/O,并回写 used ring。
- 传输层(Transport):负责前后端如何被发现与访问,常见有 virtio-pci、virtio-mmio、virtio-ccw。现代系统还引入 virtio-modern 与 legacy 兼容模式。
前后端通过三类关键结构协作:Descriptor Table 描述缓冲区地址与长度;Available Ring 由前端填写,表示“我已准备好”;Used Ring 由后端填写,表示“我已处理完”。前端 kick 后端,后端 interrupt 前端,形成闭环。
三、设备类型与协议族
VirtIO 不是单一设备,而是一个协议族。每种设备类型都有独立的规范编号与配置结构,但共享 virtqueue 与 feature 协商机制。常见设备类型如下:
| 设备类型 | 典型前端驱动 | 主要用途 | 关键特性 |
|---|---|---|---|
| virtio-net | virtio_net | 虚拟网卡 | 多队列、校验和卸载、TSO、RSS |
| virtio-blk | virtio_blk | 块设备 | 简单高效、单队列或 MQ、支持 flush |
| virtio-scsi | virtio_scsi | SCSI 控制器 | 多设备、热插拔、SCSI 命令透传 |
| virtio-console | virtio_console | 串口/控制台 | 多端口、Host-Guest 通信 |
| virtio-balloon | virtio_balloon | 内存气球 | 动态调整 Guest 可用内存 |
| virtio-vsock | virtio_vsock | Host-Guest 套接字 | 无网络栈的进程间通信 |
| virtio-gpu | virtio_gpu | 图形显示 | 2D/3D 命令、显示输出 |
协议族层面,VirtIO 规范分为多个部分:通用部分定义 virtqueue、feature 协商、设备状态机;传输部分定义 PCI、MMIO、CCW 等总线绑定;设备部分定义每种设备的具体配置与命令。开发时通常先看通用规范,再对照具体设备规范。
四、案例与场景:virtio-net 收发路径
以 virtio-net 为例,Guest 发送一个网络包时,前端驱动会:
- 从上层协议栈拿到 skb,填充到 virtqueue 的发送队列描述符中;
- 更新 avail ring,并通过 PCI 配置空间或 MMIO 写寄存器执行 kick;
- Host 后端(如 vhost-net)收到通知,从 avail ring 取出描述符;
- 后端将数据交给 tap 设备或物理网卡发送;
- 发送完成后,后端把描述符索引写入 used ring,并注入中断;
- Guest 前端在中断处理中回收缓冲区,释放 skb。
接收路径方向相反:前端预先填充空缓冲区到 receive queue,后端收到包后填入数据并写 used ring,前端中断后交给协议栈。整个过程体现了“前端准备、后端消费、共享内存传递、通知与中断同步”的固定模式。
五、常见误区与复盘
误区二:把 virtqueue 当成普通队列。virtqueue 是共享内存中的环形结构,涉及内存屏障、通知抑制、描述符链,必须按规范顺序访问。
误区三:忽略 feature 协商。前后端必须通过 feature 位协商能力,例如是否支持多队列、是否支持 mergeable buffer,否则会出现兼容性问题。
复盘时建议抓住三个检查点:设备是否被正确发现(PCI/MMIO 枚举与驱动绑定)、virtqueue 是否正常初始化(队列大小、描述符表、ring 地址)、通知与中断是否匹配(kick 是否被后端看到、used ring 是否触发中断)。掌握这三步,大多数 VirtIO 问题都能快速定位。
# 查看 Guest 中的 VirtIO 设备
lspci -nn | grep -i virtio
ls /sys/bus/virtio/devices/
# 查看 virtio-net 队列与特性
ethtool -k eth0 | grep -i virtio
cat /sys/class/net/eth0/queues/rx-0/rps_cpus
# Host 侧查看 QEMU 设备模型
qemu-system-x86_64 -device virtio-net-pci,help
总结来说,VirtIO 以半虚拟化思想为基础,通过前后端驱动模型和 virtqueue 共享内存机制,在控制路径与数据路径之间取得平衡;再通过设备类型与协议族覆盖网络、存储、控制台、内存等场景。理解这三层关系,是后续开发 VirtIO 虚拟设备、调试性能瓶颈和实现自定义后端的关键起点。