3. VirtIO架构总览
VirtIO 是 Linux 虚拟化中最重要的半虚拟化 I/O 框架之一。它并不直接模拟真实硬件寄存器,而是通过一套“前端驱动 + 后端设备 + 共享内存队列”的协作模型,让 Guest 与 Host 之间以标准化方式交换 I/O 请求。理解 VirtIO 架构,是后续掌握共享内存通信、零拷贝、通知合并与性能调优的基础。
一、四层角色:前端、后端、virtqueue 与通知
VirtIO 系统可以拆成四个关键角色。第一是前端驱动,运行在 Guest 内核中,例如 virtio-net、virtio-blk、virtio-console。它负责把上层协议栈的请求转换成 VirtIO 描述符,并放入 virtqueue。第二是后端设备,运行在 Host 用户态或内核态,例如 QEMU 的 virtio-net 设备、vhost-net、vhost-user 后端。它负责消费请求、访问 Guest 内存并写回结果。第三是virtqueue,即共享内存中的环形队列,是前后端通信的核心数据结构。第四是通知机制,包括 Guest 到 Host 的 kick 和 Host 到 Guest 的 interrupt,用于在“有活干”时唤醒对方。
这四者不是简单的调用关系,而是通过共享内存和事件通知解耦。前端驱动不需要知道后端是 QEMU 还是 vhost-user;后端也不需要理解 Guest 文件系统或网络协议,只处理 VirtIO 标准请求。
二、virtqueue:共享内存中的环形队列
virtqueue 是 VirtIO 的“心脏”。它由三部分组成:描述符表、available 环和 used 环。描述符表保存每个缓冲区的地址、长度、标志和下一个描述符索引;available 环由前端驱动写入,表示“我已经准备好这些请求”;used 环由后端设备写入,表示“这些请求已经处理完成”。
一个请求可以由多个描述符组成链,例如网络包发送时,头部、数据、状态可以分散在不同缓冲区,通过 next 字段串联。这样既支持零拷贝,也支持分散聚合 I/O。前端驱动通常按“填充描述符 → 更新 avail 环 → kick 后端”的顺序工作;后端则按“读取 avail → 处理描述符 → 写 used → 中断前端”的顺序工作。
virtqueue 关键结构(简化):
struct virtqueue {
struct vring vring; // 描述符表、avail、used
void *priv; // 后端私有数据
bool broken; // 队列是否损坏
};
struct vring_desc {
__virtio64 addr; // Guest 物理地址
__virtio32 len; // 缓冲区长度
__virtio16 flags; // NEXT / WRITE / INDIRECT
__virtio16 next; // 下一个描述符索引
};
三、通知机制:kick 与 interrupt 的协作
如果前端每次放入请求后都轮询后端,CPU 会大量空转;如果后端每次处理完都轮询前端,也会浪费资源。VirtIO 使用事件通知解决这个问题:前端在 avail 环更新后,通过写 MMIO 或 PCI 配置空间触发 kick,通知后端“有新请求”;后端处理完成后,通过注入中断触发 interrupt,通知前端“请求已完成”。
通知机制还支持“抑制”优化。前端可以设置“不通知后端”的标志,批量提交请求后再统一 kick;后端也可以设置“不中断前端”的标志,批量完成后再统一中断。这种机制在高吞吐场景下能显著降低 VM Exit 和中断开销。
四、特性协商与设备初始化流程
VirtIO 设备初始化不是“直接可用”,而是先经过特性协商。前端驱动读取设备特性位,选择自己支持且设备也支持的特性,写回驱动特性位。只有双方达成一致,后续功能才能启用。例如 VIRTIO_NET_F_MRG_RXBUF 影响接收缓冲区合并,VIRTIO_F_RING_EVENT_IDX 影响通知抑制,VIRTIO_F_ACCESS_PLATFORM 影响 DMA 访问方式。
初始化流程通常包括:复位设备、设置 ACKNOWLEDGE 状态、设置 DRIVER 状态、读取设备特性、写入驱动特性、设置 FEATURES_OK、读取并配置 virtqueue、设置 DRIVER_OK。每一步都通过状态位推进,任何一步失败都可能导致设备不可用。
| 阶段 | 关键动作 | 状态位 | 常见问题 |
|---|---|---|---|
| 复位 | 写 0 到 status,清空设备状态 | — | 设备未响应,后端未启动 |
| 确认 | 设置 ACKNOWLEDGE,表示驱动已发现设备 | ACKNOWLEDGE | PCI 枚举失败或设备 ID 不匹配 |
| 驱动加载 | 设置 DRIVER,表示驱动知道如何操作设备 | DRIVER | 驱动未加载或版本不兼容 |
| 特性协商 | 读取 device features,写入 driver features | FEATURES_OK | 特性位不匹配,FEATURES_OK 未置位 |
| 队列配置 | 设置 virtqueue 数量、大小、地址 | — | 队列大小超限或地址未对齐 |
| 就绪 | 设置 DRIVER_OK,开始收发请求 | DRIVER_OK | 队列未初始化完成,I/O 超时 |
五、案例场景:virtio-net 收包路径
以 virtio-net 为例,Guest 内核网络栈要发送一个包时,前端驱动从发送队列取空闲描述符,填入包数据地址和长度,更新 avail 环,然后 kick 后端。Host 侧 vhost-net 读取 avail 环,解析描述符,直接把数据写入 tap 设备或物理网卡。发送完成后,后端把描述符索引写入 used 环,并注入中断。前端收到中断后回收缓冲区,释放描述符,供下一次发送使用。
接收方向则相反:后端先把空缓冲区放入 avail 环并 kick 前端,前端填充空缓冲区后通知后端;后端收到数据后写入缓冲区,再把 used 环更新并中断前端。前端从 used 环取出数据,交给协议栈处理。整个过程围绕 virtqueue 和通知机制循环推进。
六、复盘:架构视角下的关键检查点
- 前端驱动:是否正确填充描述符链,是否按顺序更新 avail 环,是否在合适时机 kick。
- 后端设备:是否正确解析描述符,是否按规范写 used 环,是否支持通知抑制。
- virtqueue:队列大小、对齐、地址映射是否正确,是否存在越界或竞争。
- 通知机制:kick 与 interrupt 是否匹配,是否出现中断丢失或通知风暴。
- 特性协商:双方特性位是否一致,FEATURES_OK 是否成功置位。
- 初始化流程:状态位推进是否完整,DRIVER_OK 之前是否已完成队列配置。