设备与 I/O 虚拟化:从网卡模拟到共享队列与卸载
一、设备接口与 I/O 处理路径
设备虚拟化向客户机提供磁盘、网卡等设备接口,并将客户机请求连接到宿主资源。以网络为例,一台物理服务器可能运行几十台虚拟机,却只有几块网卡;每台客户机仍需独立初始化设备、提交报文并接收完成通知。虚拟化层在连接真实网络的同时,需要隔离不同客户机的内存和请求。
这些名称涉及三个层次:e1000 与 virtio 规定客户机使用的设备接口;QEMU、vhost-net、vhost-user 安排请求的处理位置;VFIO 与 IOMMU 管理设备分配及内存访问隔离。例如客户机使用 virtio-net 驱动时,宿主可以选择 QEMU 或 vhost 后端,驱动提交请求的接口保持不变。
源码版本:QEMU 10.2.0、Linux 6.18。
二、一个报文的四步交接
设客户机要发送一个以太网帧 P1。各种后端都要完成以下交接,区别在于接口、执行者和每次交接的成本:
- 准备。驱动把 P1 放入内存,记录地址和长度。在请求完成前,这块内存不能被随意改写或释放。
- 提交。驱动发布请求,并按需要通知设备。请求描述处理什么,通知提示有工作可以处理。
- 处理。设备模型、宿主内核、外部进程或硬件读取 P1,交给网络出口。
- 归还。处理方确认不再引用原缓冲区后发布完成,驱动据此回收内存。远端是否收到报文,由网络协议另行确认。
例如先复制 P1 到宿主缓冲区,可以较早归还客户机内存;直接使用原缓冲区,则必须让它存活到最后一次访问结束。后面的方案比较都围绕这条路径展开。
三、模拟已有网卡,先解决驱动兼容
以 e1000 为例,客户机驱动配置网卡寄存器,准备硬件规定格式的发送描述符,再更新尾指针。QEMU 实现这些寄存器及状态变化,从客户机内存读取描述符和报文,然后交给 TAP 等网络后端。客户机因而可以使用已有驱动,无需知道网卡由软件实现。
代价来自兼容整套硬件协议:寄存器访问可能触发退出,描述符解析、状态更新和中断行为都需要实现。批处理能让多个报文共享一次通知,软件仍需维护硬件接口的语义。允许客户机使用 virtio 驱动后,双方可以采用面向虚拟化的通用交接格式。
四、virtio 用共享队列表达请求和完成
virtio 规定设备与驱动怎样协商能力、建立队列和归还缓冲区。客户机驱动是请求提交者,QEMU 或其他后端扮演设备。双方共享客户机内存中的结构,通过队列批量交接缓冲区,再用独立通知提示进度。传统 e1000 同样使用描述符和设备直接访问内存的 DMA 接口;virtio 的区别在于针对虚拟化统一这些约定,减少兼容具体硬件行为的负担。
| Split virtqueue 的组成 | 在 P1 中的作用 | 更新者 |
|---|---|---|
| 描述符表 desc | 记录网络头与 P1 的地址、长度和访问方向 | 驱动准备,设备读取 |
| 可用环 avail | 发布已经准备好的描述符链头 | 驱动发布,设备消费 |
| 已用环 used | 记录已经可以归还的链 | 设备发布,驱动回收 |
两个环循环使用数组槽位,报文本身保存在描述符指向的缓冲区。写好内容后才能发布进度,看到完成后才能复用缓冲区,这两条规则决定了队列的正确性。发送链让设备读取数据;接收链则预先提供可写空间。kick 与中断用于提示另一方检查队列,多项请求可以共享一次通知。
双方必须先确认共同支持的特性和队列格式,再配置共享区域并开始处理。字段、索引回绕与链 0 → 2 的变化见发送队列交互图,协商和启用顺序见virtio 设备初始化。客户机接口确定后,可以单独改变处理 P1 的后端。
五、保留 virtio 接口,改变队列处理位置
| 后端 | P1 的主要处理路径 | 配置责任 |
|---|---|---|
| QEMU 用户态设备模型 | 客户机队列 → QEMU 进程 → TAP | QEMU 配置并消费队列 |
| 宿主内核 vhost-net | 客户机队列 → 内核 vhost → TAP 对应网络路径 | QEMU 配置,内核处理报文 |
| 独立进程中的 vhost-user 后端 | 客户机队列 → 后端进程 → 后端选择的出口 | QEMU 与后端进程协商资源 |
QEMU 可以自己消费队列,但高吞吐网络还可能受到用户态与内核交接、线程调度和缓存移动的影响。vhost-net 把 virtio-net 队列处理移入宿主内核,连接内核网络路径;QEMU 仍负责设备配置与控制,客户机无需更换 virtio 驱动。
vhost-user 让独立用户态进程消费队列。QEMU 通过 UNIX socket 协商特性、共享内存和通知文件描述符;报文通常从共享内存读取,不逐包走这条控制 socket。DPDK 后端可以接用户态交换或网卡驱动,SPDK 后端可以接存储栈。收益来自专门的数据处理实现,代价是跨进程内存、重连、故障和升级的协调。vhost-user 控制协议
设备可以通过中断通知完成,驱动或后端也可以轮询。轮询减少短请求的唤醒等待,却在空闲时继续消耗 CPU;批处理摊薄每次交接成本,等待凑批又可能增加低负载延迟。相同后端在高吞吐和低负载下,会有不同的 CPU 与延迟表现。
六、硬件分配、数据路径卸载与队列演进
如果希望减少软件处理,可以经 VFIO 把一个可隔离的设备功能交给客户机。客户机使用相应硬件驱动,设备通过 DMA 访问获准映射的内存;IOMMU 约束其访问范围。CPU 的 EPT/NPT 不能自动替代 DMA 隔离,设备重置和中断也仍需宿主协调。
SR-IOV 允许物理功能(PF)提供多个虚拟功能(VF),解决硬件资源如何细分;VFIO 可以负责把 VF 分配给客户机,两者经常组合使用。mdev 则由设备相关驱动提供中介设备实例,其共享与隔离能力取决于实现。vDPA 保留 virtio 兼容的数据接口,让适配后的硬件或软件处理队列,仍需控制侧配置。
Mermaid 源码
flowchart TB
accTitle: 设备接口与数据后端的选择
accDescr: 客户机设备可以使用模拟硬件接口、virtio 共享队列或分配的设备功能。virtio 队列有 QEMU、内核 vhost、用户态 vhost-user 和 vDPA 等不同后端,VFIO 直通则使用所分配设备的接口。
G["客户机驱动"] --> E["模拟 e1000 接口"]
G --> V["virtio 接口<br/>virtqueue"]
G --> P["分配的 PCI 功能<br/>PF / VF 等"]
E --> Q["QEMU 设备模型<br/>TAP 等后端"]
V --> Q
V --> K["vhost-net<br/>宿主内核"]
V --> U["vhost-user<br/>DPDK / SPDK 等独立进程"]
V --> D["vDPA 后端<br/>virtio 兼容数据面"]
P --> H["设备 DMA<br/>VFIO / IOMMU 管理隔离"]
virtio 自身也在演进。Split 队列用三组结构分开描述请求与完成;packed 队列把可用、已用状态放入同一描述符环,并结合回绕标记识别所有权,以调整缓存访问成本。两种格式都必须遵守发布顺序和缓冲区生命周期,也都可连接不同后端。
七、按需求比较,沿数据路径定位成本
| 优先解决的问题 | 常见选择 | 还需确认的代价 |
|---|---|---|
| 沿用客户机已有驱动 | 传统设备模拟 | 兼容语义与设备模拟开销 |
| 通用队列接口与灵活管理 | virtio+QEMU 或 vhost | 队列交接、通知与后端 CPU 消耗 |
| 独立网络或存储数据处理 | vhost-user | 进程间共享、断连与恢复 |
| 减少软件数据处理 | VFIO 分配或 vDPA 卸载 | 硬件能力、隔离粒度与状态迁移支持 |
先固定报文大小、队列数和 CPU 预算,再比较吞吐、尾延迟、丢包与 CPU 占用。随后核对启动、热插拔、迁移和故障恢复:硬件能发送报文,不代表它能导出完整设备状态。把请求、缓冲区和状态所有者逐项画出来,才容易发现省下了哪次交接,又新增了哪项协调。
八、沿队列与后端继续阅读
- virtio 设备初始化与特性协商:从设备发现、格式协商到队列启用,区分失败与复位。
- e1000 与 virtio-net 发包路径:交互查看描述符、环索引与缓冲区。
- virtio-net 接收与 NAPI:可写缓冲区、完成归还与预算轮询。
- Packed virtqueue:操作四槽队列,观察发布、完成和回绕。
- vhost-vDPA:硬件数据路径、DMA 映射与迁移时的影子队列。
- vhost-net 与 vhost-user:队列消费者、控制通道与通知绑定。
- DPDK vhost-user 报文收发:virtqueue、mbuf 与物理网卡发送环。
- SPDK vhost-user-blk、QEMU virtio-blk:存储请求与完成语义。
- VFIO 与 DMA 地址转换:直通设备访问内存及隔离边界;继续阅读 IOMMU 的转换与缓存失效、IOMMUFD 的设备与地址空间管理。
- vIOMMU:客户机 DMA 映射与失效同步:客户机管理 DMA 地址后,页表、宿主映射与转换缓存的同步。
- virtio-gpu 的二维资源与画面更新:从客户机像素缓冲区到宿主图像、scanout 和显示更新,再区分三维加速接口。