4. VirtIO 传输层
VirtIO 规范把“设备长什么样、怎么被发现、怎么配置、怎么通知”抽象成传输层(Transport)。真正承载队列语义的 virtqueue 是传输无关的,而传输层负责把 virtqueue 映射到具体总线:PCI 设备用 BAR 与 capability 描述,MMIO 设备用固定偏移的寄存器窗口描述,Channel I/O 则用 ccw 命令字描述。理解传输层,才能解释为什么同一份 virtio-net 驱动可以跑在 PCI、MMIO、s390 通道设备上。
一、三种传输方式的定位与差异
传输层的核心职责只有四件事:设备发现、能力协商、virtqueue 定位、中断与通知。三种传输方式在这四件事上的实现手段不同,但暴露给上层 virtio 核心的接口是一致的。
| 维度 | VirtIO over PCI | VirtIO over MMIO | VirtIO over Channel I/O |
|---|---|---|---|
| 典型平台 | x86/ARM 服务器、QEMU virtio-pci | ARM/RISC-V 嵌入式、微虚拟机 | IBM Z / s390x |
| 发现方式 | PCI 枚举 + Vendor/Device ID | 设备树 / ACPI 固定地址 | 子通道 Subchannel 枚举 |
| 配置入口 | PCI Capability 指向的 BAR 区域 | MMIO 寄存器窗口固定偏移 | ccw 命令字 + 控制块 |
| 队列定位 | common cfg 中 queue_notify_off | QueueNotify 寄存器 | ccw 指定队列索引 |
| 中断 | MSI-X 向量(首选)或 INTx | GIC/PLIC 中断线 | I/O 中断 + 子通道 |
二、设备发现与配置空间
PCI 传输把配置信息拆成多个 capability:VIRTIO_PCI_CAP_COMMON_CFG 提供 feature 协商与队列选择,VIRTIO_PCI_CAP_NOTIFY_CFG 提供 doorbell 地址,VIRTIO_PCI_CAP_DEVICE_CFG 暴露 MAC、链路状态等设备专属字段。驱动先读 PCI 配置空间找到这些 capability,再通过 BAR 映射访问。
# 查看 virtio-net PCI 设备与 capability
lspci -nn | grep -i virtio
# 01:00.0 Ethernet controller [0200]: Red Hat, Inc. Virtio 1.0 network device [1af4:1041]
lspci -vv -s 01:00.0 | grep -A3 -i "capabilities"
# Capabilities: [98] MSI-X: Enable+ Count=4 Masked-
# Capabilities: [84] Vendor Specific Information: VirtIO: CommonCfg
# Capabilities: [70] Vendor Specific Information: VirtIO: Notify
# Capabilities: [60] Vendor Specific Information: VirtIO: DeviceCfg
MMIO 传输没有枚举过程,设备地址由设备树或 ACPI 直接给出,寄存器按固定偏移排列:MagicValue、Version、DeviceID、VendorID、DeviceFeatures、QueueSel、QueueNum、QueueReady、QueueNotify、InterruptStatus 等。驱动只需按偏移读写即可完成协商与队列配置。
Channel I/O 走的是 s390 的 ccw 机制:驱动通过子通道发送 CCW_CMD_READ_VQ_CONF、CCW_CMD_SET_VQ 等命令字,由通道子系统完成队列注册与状态查询。它没有 BAR 概念,配置信息封装在控制块里。
三、中断机制与通知路径
传输层的中断设计要解决两个方向:设备完成 I/O 后通知驱动(中断),驱动填充队列后通知设备(doorbell)。PCI 传输优先使用 MSI-X,每个 virtqueue 可绑定独立向量,避免共享中断带来的额外判断;MMIO 传输使用平台中断控制器;Channel I/O 使用 I/O 中断并在中断处理中读取子通道状态。
queue_notify_off:驱动把队列索引写入 QueueNotify 寄存器时,设备据此定位到具体 virtqueue。若该值配置错误,会出现“队列已就绪但设备收不到 kick”的静默故障。
四、实操:从 sysfs 观察传输层状态
# 查看 virtio 设备与驱动绑定
ls -l /sys/bus/virtio/devices/
# virtio0 -> ../../../devices/pci0000:00/0000:00:04.0/virtio0
# 查看队列数量与中断向量
cat /sys/bus/virtio/devices/virtio0/status
cat /sys/bus/virtio/devices/virtio0/features
# 查看 MSI-X 向量分配
cat /proc/interrupts | grep virtio
# 24: 0 0 0 0 PCI-MSI 524288-edge virtio0-input.0
# 25: 0 0 0 0 PCI-MSI 524289-edge virtio0-output.0
在 QEMU 中可通过 -device virtio-net-pci,disable-modern=off,msix=on 控制传输层行为;若改为 disable-modern=on,则回退到 legacy 接口,配置空间布局与中断方式都会变化,这是排障时常见的兼容性分界点。
五、误区与复盘
- 把传输层当队列层:virtqueue 的 split/packed 格式与传输方式无关,PCI 与 MMIO 都可以使用 packed 队列。
- 忽略 feature 协商顺序:必须先读 DeviceFeatures,再写 DriverFeatures,最后置 FEATURES_OK 并回读确认,顺序错误会导致设备拒绝进入 DRIVER_OK。
- 中断与轮询混用不当:高吞吐场景可关闭中断改用轮询,但需确认传输层支持 event suppression,否则会丢失完成通知。
- MMIO 地址写错:MMIO 寄存器偏移固定,任何偏移错位都会表现为“设备无响应”,应优先核对设备树 reg 与规范偏移表。
总结来看,传输层是 VirtIO 的“接入适配层”:PCI 用 capability 与 BAR 表达,MMIO 用固定偏移寄存器表达,Channel I/O 用 ccw 表达。三者共享同一套 feature 协商与 virtqueue 语义,差异集中在发现方式、配置入口与中断投递上。掌握这三条路径,就能在跨平台部署与性能调优时快速定位问题所在。