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 对,减少锁竞争。

一个典型的块请求包含三段描述符:

  1. 请求头 virtio_blk_req,包含类型(读/写/flush)、扇区号等。
  2. 数据缓冲区,读操作时由设备填充,写操作时由驱动填充。
  3. 状态字节,设备写回 VIRTIO_BLK_S_OK 或错误码。

驱动将描述符链写入 Available Ring 并通知设备;设备处理后把已完成的描述符索引写入 Used Ring,并触发中断。这个“请求队列 → 完成队列”的闭环,就是 VirtIO Block 的 I/O 生命周期。

VirtIO Block 设备架构与请求/完成队列闭环 Guest 驱动 virtio_blk 请求头 virtio_blk_req 数据缓冲区 (SG) 状态字节 status virtqueue 共享内存 Descriptor Table Available Ring (驱动→设备) Used Ring (设备→驱动) Host 后端 vhost/QEMU 请求解析与调度 块设备/文件后端 完成状态回写 写入 通知 完成 中断 特性位协商流程 (Feature Negotiation) 读 device features 驱动选择 子集 写 guest features 置位 FEATURES_OK 校验并进入 DRIVER_OK 关键特性位:VIRTIO_BLK_F_SEG_MAX、VIRTIO_BLK_F_MQ、VIRTIO_BLK_F_FLUSH、VIRTIO_F_VERSION_1 协商失败 → 设备不可用;协商成功 → 队列使能,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 会被清除,驱动必须重新协商或放弃。

提示:在 QEMU 中可通过 -device virtio-blk-pci,drive=disk0,scsi=off 指定设备类型,并用 disable-legacy=on 强制现代模式,便于验证特性协商路径。

五、案例:一次 4K 随机读的完整路径

假设 Guest 发起一次 4K 随机读,扇区号 0x800,流程如下:

  1. 文件系统将请求下发到块层,virtio_blk 驱动分配请求头,类型为 VIRTIO_BLK_T_IN。
  2. 驱动将请求头、4K 数据缓冲区、状态字节组成描述符链,写入 Available Ring。
  3. 驱动写 MMIO/PCI 通知寄存器,触发 Host 侧 vhost 或 QEMU 后端。
  4. 后端从 Used Ring 读取描述符,解析扇区号,从后端存储读取数据。
  5. 后端将数据写入 Guest 缓冲区,状态字节写 VIRTIO_BLK_S_OK,更新 Used Ring。
  6. 后端注入中断,驱动回收描述符,完成 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

六、常见误区与复盘

误区一:认为 virtqueue 是“一个队列”。实际上每个 virtqueue 包含三环结构,且多队列场景下存在多个独立 virtqueue 对,驱动与设备各持一端。
误区二:忽略特性协商顺序。若在 FEATURES_OK 之前就配置队列,设备可能拒绝或行为未定义。
误区三:把完成队列当成独立硬件队列。完成通知通过 Used Ring 和中断实现,并非独立 DMA 通道。