4. VirtIO Block设备架构
VirtIO Block 是半虚拟化存储栈的核心设备模型。它并不模拟真实磁盘控制器寄存器,而是通过一套标准化的“设备 ID + 配置空间 + virtqueue + 特性协商”机制,让 Guest 驱动与 Host 后端以共享内存方式高效交换 I/O 请求。理解这套架构,是后续分析性能瓶颈、排查队列卡死、实现自定义后端的基础。
一、设备 ID 与设备发现
VirtIO 设备通过 PCI 或 MMIO 总线暴露身份。对于现代 QEMU/KVM 环境,块设备通常以 PCI 设备形式出现,其 Vendor ID 为 0x1AF4,Device ID 随 VirtIO 版本不同而变化:传统 transitional 设备使用 0x1001,现代 modern 设备使用 0x1042。驱动加载时先读取这些 ID,确认这是一个块设备而非网卡或控制台,再进入配置空间读取具体参数。
# 在 Guest 中查看 VirtIO Block PCI 设备
lspci -nn | grep -i virtio
# 01:00.0 SCSI storage controller [0100]: Red Hat, Inc. Virtio block device [1af4:1042]
# 查看 sysfs 中的 virtio 设备
ls /sys/bus/virtio/devices/
# virtio0 virtio1 ...
设备 ID 决定了驱动匹配路径。若 ID 不匹配,即使后端已经就绪,Guest 也不会加载 virtio_blk 驱动,表现为“磁盘不可见”。这是排障时第一个要确认的点。
二、配置空间:容量、队列与状态
配置空间是驱动了解设备能力的窗口。VirtIO Block 的配置空间包含以下关键字段:
- capacity:以 512 字节扇区为单位的设备容量,驱动据此计算块设备大小。
- size_max:单个请求可传输的最大字节数,现代设备通常为 4 MiB 或更大。
- seg_max:一个请求中允许的最大段数,影响 SG 列表长度。
- blk_size:逻辑块大小,常见为 512 或 4096 字节。
- num_queues:多队列数量,用于多核并行 I/O。
- status:设备状态位,驱动通过写该字段完成初始化握手。
驱动初始化时按固定顺序写 status:ACKNOWLEDGE → DRIVER → 协商特性 → FEATURES_OK → 设置队列 → DRIVER_OK。任何一步失败,设备都会拒绝进入下一状态。
三、virtqueue 结构与请求/完成队列
virtqueue 是 VirtIO 的数据平面核心。每个 virtqueue 由三部分组成:Descriptor Table(描述符表)、Available Ring(驱动→设备)、Used Ring(设备→驱动)。对于块设备,通常使用一个请求队列(request queue)承载读写请求,设备处理完成后通过完成队列(completion queue)回写结果。在多队列场景下,每个 CPU 可绑定一个独立的 virtqueue 对,减少锁竞争。
一个典型的块请求包含三段描述符:
- 请求头
virtio_blk_req,包含类型(读/写/flush)、扇区号等。 - 数据缓冲区,读操作时由设备填充,写操作时由驱动填充。
- 状态字节,设备写回
VIRTIO_BLK_S_OK或错误码。
驱动将描述符链写入 Available Ring 并通知设备;设备处理后把已完成的描述符索引写入 Used Ring,并触发中断。这个“请求队列 → 完成队列”的闭环,就是 VirtIO Block 的 I/O 生命周期。
四、特性位与协商流程
特性位是驱动与设备之间的“能力契约”。驱动读取设备支持的特性位图,选择自己支持且需要的子集写回。常见块设备特性包括:
| 特性位 | 含义 | 工程影响 |
|---|---|---|
| VIRTIO_BLK_F_SEG_MAX | 支持最大段数配置 | 决定 SG 列表长度与合并能力 |
| VIRTIO_BLK_F_MQ | 多队列支持 | 多核并行 I/O,降低锁竞争 |
| VIRTIO_BLK_F_FLUSH | 支持 flush 命令 | 保证持久化语义,影响 fsync 性能 |
| VIRTIO_F_VERSION_1 | 现代 VirtIO 1.0+ 规范 | 使用现代 PCI 能力结构,非 legacy |
| VIRTIO_BLK_F_BLK_SIZE | 可配置逻辑块大小 | 支持 4K 原生扇区,影响对齐 |
协商流程必须严格按序:驱动先写 ACKNOWLEDGE,再写 DRIVER,然后读取并写回特性位,最后置 FEATURES_OK 并回读确认。如果设备不支持驱动选择的某个特性,FEATURES_OK 会被清除,驱动必须重新协商或放弃。
-device virtio-blk-pci,drive=disk0,scsi=off 指定设备类型,并用 disable-legacy=on 强制现代模式,便于验证特性协商路径。
五、案例:一次 4K 随机读的完整路径
假设 Guest 发起一次 4K 随机读,扇区号 0x800,流程如下:
- 文件系统将请求下发到块层,
virtio_blk驱动分配请求头,类型为VIRTIO_BLK_T_IN。 - 驱动将请求头、4K 数据缓冲区、状态字节组成描述符链,写入 Available Ring。
- 驱动写 MMIO/PCI 通知寄存器,触发 Host 侧 vhost 或 QEMU 后端。
- 后端从 Used Ring 读取描述符,解析扇区号,从后端存储读取数据。
- 后端将数据写入 Guest 缓冲区,状态字节写
VIRTIO_BLK_S_OK,更新 Used Ring。 - 后端注入中断,驱动回收描述符,完成 I/O。
# 观察 virtio_blk 队列与中断统计
cat /sys/block/vda/stat
cat /proc/interrupts | grep virtio
# 查看队列深度与调度器
cat /sys/block/vda/queue/nr_requests
cat /sys/block/vda/queue/scheduler
六、常见误区与复盘
误区二:忽略特性协商顺序。若在
FEATURES_OK 之前就配置队列,设备可能拒绝或行为未定义。误区三:把完成队列当成独立硬件队列。完成通知通过 Used Ring 和中断实现,并非独立 DMA 通道。