4. VirtIO 规范解读
VirtIO 之所以能让不同厂商、不同操作系统的前端驱动与后端设备实现互操作,核心在于它把“设备如何被识别、如何被配置、如何被驱动、如何被通知”抽象成了一套统一的规范。本节围绕设备状态、特性位、配置空间、通知机制与通用操作五个关键点展开,帮助你在开发 VirtIO 虚拟设备时建立完整的规范视角。
一、设备状态:驱动与设备之间的握手协议
VirtIO 设备状态(Device Status)是驱动与设备之间最基础的同步机制。它用一个 8 位字段记录设备当前所处的初始化阶段,驱动通过写状态位推进流程,设备通过读状态位判断自己是否已被正确配置。规范定义了以下状态位:
| 状态位 | 名称 | 含义 |
|---|---|---|
| 0 | ACKNOWLEDGE | 驱动已发现设备,开始接管 |
| 1 | DRIVER | 驱动知道如何驱动该设备 |
| 2 | DRIVER_OK | 驱动已完成配置,设备可以开始工作 |
| 3 | FEATURES_OK | 驱动已接受设备特性位,特性协商完成 |
| 4 | NEEDS_RESET | 设备遇到错误,需要驱动复位 |
| 5 | FAILED | 驱动或设备发生不可恢复错误 |
状态迁移必须按顺序推进:驱动先写 ACKNOWLEDGE,再写 DRIVER,然后读取特性位并写 FEATURES_OK,最后写 DRIVER_OK。设备在收到 DRIVER_OK 之前不得访问 virtqueue。如果设备在 FEATURES_OK 之后发现驱动选择了它不支持的特性组合,可以清除 FEATURES_OK 表示拒绝。NEEDS_RESET 和 FAILED 是错误路径,驱动看到后应执行复位或报告错误。
二、特性位:能力协商的契约
特性位(Feature Bits)是 VirtIO 设备与驱动之间的能力契约。设备通过特性位告诉驱动“我支持什么”,驱动通过特性位告诉设备“我选择使用什么”。规范把特性位分为两部分:
- 设备特性位:设备支持的能力,驱动读取后决定是否接受。
- 驱动特性位:驱动选择启用的能力,设备读取后据此调整行为。
特性位按 32 位一组组织,通过 Feature Select 寄存器选择要访问的组。常见特性包括 VIRTIO_F_VERSION_1、VIRTIO_F_ACCESS_PLATFORM、VIRTIO_F_RING_PACKED 等。以 VIRTIO_F_VERSION_1 为例,它表示设备遵循现代 VirtIO 1.0+ 规范,驱动必须接受该位才能继续。如果设备不支持该位,驱动应放弃使用现代接口。
/* 驱动侧特性协商伪代码 */
u64 features = read_device_features();
features &= driver_supported_features;
write_driver_features(features);
if (!(features & VIRTIO_F_VERSION_1)) {
return -EINVAL; /* 设备不支持现代 VirtIO */
}
write_status(status | FEATURES_OK);
if (!(read_status() & FEATURES_OK)) {
return -EIO; /* 设备拒绝特性组合 */
}
特性位协商的关键原则是:驱动只能启用设备声明支持的位;设备必须接受驱动写入的位,除非该位组合非法。任何一方违反契约,都会导致设备无法正常工作。
三、配置空间:设备参数的读取窗口
配置空间(Configuration Space)是驱动读取设备参数的区域,通常包含设备类型、队列数量、队列大小、设备特定配置等。现代 VirtIO 使用 PCI 能力结构或 MMIO 区域暴露配置空间,驱动通过标准寄存器访问。
配置空间一般分为通用配置和设备特定配置。通用配置包括 Device ID、Vendor ID、Queue Num、Queue Address 等;设备特定配置则因设备类型而异,例如网络设备的 MAC 地址、块设备的容量等。驱动在读取配置空间前,必须先完成特性协商和状态推进,否则读到的值可能无效。
四、通知机制:virtqueue 的“门铃”
VirtIO 使用 virtqueue 在驱动与设备之间传递缓冲区。驱动把请求放入 avail ring,设备从 avail ring 取走请求,处理完成后把结果放入 used ring。双方如何知道对方有新动作?这就是通知机制。
通知机制包含两个方向:
- 驱动通知设备:驱动写完 avail ring 后,通过写 Queue Notify 寄存器“敲门”,告诉设备有新请求。
- 设备通知驱动:设备写完 used ring 后,通过中断或写 Queue Interrupt 寄存器通知驱动。
为了减少不必要的通知,VirtIO 支持通知抑制(Notification Suppression)。驱动可以在 avail ring 的 flags 中设置 NO_INTERRUPT,设备可以在 used ring 的 flags 中设置 NO_NOTIFY。双方根据负载情况动态决定是否通知,从而降低中断开销。
/* 驱动侧提交通知示例 */
virtq_add_buf(vq, buf);
virtq_kick(vq); /* 写 Queue Notify 寄存器 */
/* 设备侧完成通知示例 */
virtq_add_used(vq, head, len);
if (!(vq->used_flags & NO_NOTIFY))
virtq_interrupt(vq); /* 触发中断 */
五、通用操作:从复位到运行的完整流程
把上述机制串起来,VirtIO 设备的通用操作流程可以概括为:复位设备 → 设置 ACKNOWLEDGE → 设置 DRIVER → 读取并协商特性位 → 设置 FEATURES_OK → 配置 virtqueue → 设置 DRIVER_OK → 运行 → 错误处理 → 复位。
下面这张图展示了设备状态、特性位、配置空间与通知机制之间的整体关系,帮助你从系统视角理解 VirtIO 设备的生命周期。
六、常见误区与复盘
在实际开发中,以下误区最容易导致 VirtIO 设备无法正常工作:
- 状态位顺序错误:跳过 FEATURES_OK 直接写 DRIVER_OK,设备可能拒绝服务或行为异常。
- 特性位协商不完整:驱动未检查设备是否支持 VIRTIO_F_VERSION_1,导致使用了设备不支持的现代接口。
- 配置空间读取时机错误:在 DRIVER_OK 之后才读取设备特定配置,此时配置可能已被设备修改。
- 通知抑制使用不当:驱动设置了 NO_INTERRUPT 但设备仍频繁中断,或设备设置了 NO_NOTIFY 但驱动仍在等待通知,导致性能下降或死锁。
- 复位不彻底:设备出错后驱动只写 FAILED 而不执行复位,导致 virtqueue 状态残留,后续初始化失败。
总结来说,设备状态是握手协议,特性位是能力契约,配置空间是参数窗口,通知机制是数据流动的“门铃”,通用操作则把它们串成一条完整的生命周期。理解这五者的关系,是开发 VirtIO 虚拟设备的基础,也是后续实现具体设备类型(网络、块、控制台等)的前提。