virtio 设备初始化与特性协商
一、virtio 设备初始化的阶段与约束
客户机驱动与 virtio 设备通过共享内存交接请求。第一次 I/O 前,驱动先找到设备的控制区域,与设备选定共同支持的功能,再分配队列并公布地址。设备确认这些约定后,才能按相同格式读取描述符、访问缓冲区并归还结果。
本文跟踪现代 virtio-pci 接口上的一个 virtio-blk 队列。例子采用 4 个描述符、split 格式,普通 RAM,无客户机 IOMMU 地址转换;队列地址以 GPA 表示。开启平台访问约束时,需要使用相应 DMA 映射,不能沿用这里“地址就是 GPA”的假设。协议依据 VIRTIO 1.2,实例由 QEMU 与 Linux 驱动实现。
这次初始化依次经过:发现设备 → 复位并选择特性 → 确认共同格式 → 配置队列内存 → 允许处理 → 发布读取请求。特性确认决定队列的解释方式;队列地址和设备状态决定何时可以开始访问。
源码版本:QEMU 10.2.0、Linux 6.18。
Mermaid 源码
sequenceDiagram
accTitle: 现代 virtio 设备初始化握手
accDescr: 驱动复位设备,依次设置 ACKNOWLEDGE 和 DRIVER,选择特性后设置 FEATURES_OK 并读回确认。接受后配置并启用队列,最后设置 DRIVER_OK 才可通知设备处理请求;拒绝特性时停止初始化。
participant D as 客户机驱动
participant V as QEMU virtio 设备
D->>V: device_status = 0
V-->>D: 读回 0:复位完成
D->>V: ACKNOWLEDGE:0x01
D->>V: 加 DRIVER:0x03
D->>V: 读取设备特性,写回有效子集
D->>V: 加 FEATURES_OK:尝试 0x0b
V-->>D: 读回 device_status
alt FEATURES_OK 被接受
D->>D: 初始化描述符和环内存
D->>V: 配置地址、大小及必要的中断字段
D->>V: queue_enable = 1
Note over D,V: 单队列已配置,尚未放行设备
D->>V: 加 DRIVER_OK:0x0f
D->>V: 按通知条件 kick
else FEATURES_OK 未置位
D->>D: 停止初始化,处理失败
Note over D,V: 重试从设备复位重新开始
end
二、通过 PCI 能力找到控制与通知区域
PCI 枚举先识别设备,再遍历能力链。现代 virtio 使用 vendor-specific capability 描述控制区域所在的 BAR、偏移和长度,驱动据此映射寄存器。virtio_pci_cap 的 cfg_type 标识区域用途,bar / offset / length 给出位置与范围。这些控制区域由设备接口呈现,日常请求使用的 virtqueue 则分配在客户机 RAM 中。
| 区域 | 本次初始化中的作用 | 与队列内存的区别 |
|---|---|---|
| Common configuration | 读写特性、device_status、队列大小和地址 | 保存控制字段,不存每次请求的数据 |
| Notify | 驱动写门铃,提示某队列有新工作 | 通知不携带整条描述符或数据缓冲区 |
| ISR | 传统引脚中断下读取队列/配置事件原因 | 不代替 used 中的完成记录;MSI-X 有独立向量路由 |
| Device configuration | 读取块设备容量等设备特有配置 | 布局由设备类型和已协商特性决定 |
Common 中的 config_generation 用于检查设备配置读取的一致性。例如读取跨多个访问的容量值时,前后读取 generation,发生变化就重试,避免拼出一次更新前后的混合值。设备配置空间、PCI 配置空间和客户机 RAM 中的 virtqueue,是三类不同位置。能力结构与公共配置字段
三、先选择特性,再让设备确认
1. 特性选择包含依赖,不能只取交集
设备公布它支持的 feature bits,驱动选择自身支持并准备启用的特性子集。公共配置用 device_feature_select 选择每组 32 位,再从 device_feature 读取;驱动以对应的 driver_feature_select / driver_feature 写回选择。QEMU 头文件把后两者称为 guest_feature,表达的是同一组寄存器。
本例选择 VIRTIO_F_VERSION_1,使用现代接口;不选择 RING_PACKED,所以采用 split 队列,也不选择 EVENT_IDX、RING_RESET、通知数据扩展。VERSION_1 位说明采用现代协议,不能据此断言设备实现了某个文档版本的所有可选功能。
选择还要满足依赖与环境要求。比如 virtio-net 的 HOST_TSO4 要求 CSUM,不能只打开分段能力而忽略其校验和前提。平台要求设备使用受控 DMA 地址时,ACCESS_PLATFORM 也可能是运行条件。Linux 因而在初步交集后继续进行传输层和设备驱动验证。现代 PCI 路径还明确检查 VERSION_1。驱动特性筛选、现代 PCI 验证与写回
2. 状态位表示已经完成的步骤
| 操作 | 本例状态值 | 此时能确认什么 |
|---|---|---|
| 写 0,等待读回 0 | 0x00 | 设备复位完成 |
| 加入 ACKNOWLEDGE | 0x01 | 客户机已识别设备 |
| 加入 DRIVER | 0x03 | 已找到适用驱动,开始协商 |
| 写回特性,加入 FEATURES_OK,再读回 | 0x0b,仅限设备接受时 | 双方确认本轮选择,仍未开始消费请求 |
| 完成队列与设备配置,加入 DRIVER_OK | 0x0f | 驱动已准备好运行 |
状态按位累积:本例在 0x0b 上加入 DRIVER_OK 的 0x04,得到 0x0f。FEATURES_OK 的读回是确认点,QEMU 会在这一步验证特性组合;读回仍带该位,驱动才继续配置队列。已确认的特性保持到设备复位,下一轮协商从复位后重新开始。QEMU 特性验证、Linux 读回检查
四、把 4 个描述符的队列放入客户机内存
驱动选择 queue_select = 0,确认该队列存在且尚未启用,读取支持的大小。设其上限至少为 4,本例写 queue_size = 4。split 队列大小为 2 的幂;它表示描述符表和环的容量,不表示始终可以同时提交 4 个任意大小的请求。
| 公共配置字段 | 本例 GPA | 本例内存结构与最小占用 |
|---|---|---|
queue_desc | 0x100000 | 4 × 16 B 描述符,共 64 B;16 B 对齐 |
queue_driver | 0x101000 | available 的 flags、idx、4 个链头,共 12 B;2 B 对齐 |
queue_device | 0x102000 | used 的 flags、idx、4 个 id/len 项,共 36 B;4 B 对齐 |
本例没有 EVENT_IDX,所以没有额外的 used_event、avail_event 字段。分开保留三页是为了让地址关系清楚,实际分配可以更紧凑。每个描述符由 addr:64 / len:32 / flags:16 / next:16 构成;现代接口的这些多字节字段采用小端。驱动初始化环和索引,设备就能按相同布局访问它们。
Mermaid 源码
flowchart TB
accTitle: N 等于 4 的 split 队列地址与结构
accDescr: 公共配置的三个地址指向客户机 RAM 中的描述符表、available 和 used。描述符零到二组成读取请求,第四项空闲;available 发布链头零,used 归还 id 零及写入长度。门铃位于 PCI BAR,和这些 RAM 区域分开。
C["Common configuration:队列 0<br/>queue_size = 4"]
C -->|queue_desc = 0x100000| D["描述符表:64 B<br/>desc 0 → desc 1 → desc 2<br/>desc 3 空闲"]
C -->|queue_driver = 0x101000| A["available:12 B<br/>flags、idx、ring 0..3<br/>ring 0 发布链头 0"]
C -->|queue_device = 0x102000| U["used:36 B<br/>flags、idx、4 个 id / len<br/>成功完成:id 0、len 4097"]
D --> B["缓冲区位于其他 RAM 地址<br/>0x200000:16 B 请求头<br/>0x201000:4096 B 数据<br/>0x202000:1 B 状态"]
A -. "设备按链头查表" .-> D
U -. "驱动按 id 回收原链" .-> D
N["PCI BAR 中的 Notify 区域<br/>写入队列号 0,提示有新工作"] -. "不携带请求载荷" .-> A
classDef config fill:#fff4db,stroke:#b89a55,color:#59491e;
classDef queue fill:#eaf0f6,stroke:#8198ac,color:#263c4c;
classDef buffer fill:#e4f1ec,stroke:#438575,color:#173e34;
class C,N config;
class D,A,U queue;
class B buffer;
选用 packed 时,queue_desc 指向 packed 描述符环,queue_driver 和 queue_device 指向双方的事件抑制结构;它们不再是上表的 available/used 环。双方必须先对 RING_PACKED 达成一致,再按对应格式分配与访问,不能在运行中只切换一个标志来改造已有队列。
五、启用队列,再发出第一个读取请求
完成地址、大小、中断等必要配置后,驱动写 queue_enable = 1,再完成设备级准备并设置 DRIVER_OK。前者说明这个队列已配置,后者说明驱动能够接收结果;仅有 queue_enable 不能越过 DRIVER_OK 提前通知或消费请求。
例子从磁盘偏移 1 MiB 读取 4 KiB,采用直接链 0 → 1 → 2。描述符 0 指向 GPA 0x200000 的 16 B 请求头,类型 IN、sector 2048;描述符 1 指向 0x201000 的 4096 B 可写数据区;描述符 2 指向 0x202000 的 1 B 可写状态区。第 4 个描述符空闲。WRITE 表示设备写入客户机内存,所以磁盘读取的数据区需要这个标志。
desc[0]: addr=0x200000, len=16, flags=NEXT, next=1
desc[1]: addr=0x201000, len=4096, flags=NEXT|WRITE, next=2
desc[2]: addr=0x202000, len=1, flags=WRITE
avail.ring[0] = 0
按发布顺序完成内存写入,再令 avail.idx: 0 → 1
成功完成后:used.ring[0] = {id: 0, len: 4097}, used.idx: 0 → 1
通知地址由 BAR 基址 + notify capability.offset + queue_notify_off × notify_off_multiplier 得到,queue_notify_off 不是字节偏移。本例未启用通知扩展,驱动向该地址写 16 位队列号 0;数据仍留在描述符指向的 RAM。发布所需的内存屏障不能被门铃写入替代,是否需要通知还应遵守队列抑制规则。通知区域计算
设备检查 available,读取请求并完成块操作,再写数据、状态和 used。初始化至此才接入日常数据路径。具体处理见virtio-blk 请求与完成,环槽回绕可在virtqueue 交互图中逐步观察。
六、协商失败处理与设备复位
1. 设备复位重新开始整轮协商
驱动遇到无法恢复的初始化错误时,标记 FAILED 并停止继续初始化。FAILED 本身不是“设备已停止访问内存”的证明;重试需要复位。设备报告 DEVICE_NEEDS_RESET 时,在途请求可能已经完成,也可能尚未完成,驱动不能据此推断结果或盲目重放。
现代 PCI 的设备复位通过向 device_status 写 0 发起,读取到 0 才确认完成;此时设备不再访问旧队列或发送相应通知。驱动还需停止自己的提交与回调,之后才能安全拆除队列、释放缓冲区,并从 ACKNOWLEDGE 重新协商。复位不会替应用证明一次磁盘写入是否持久化,设备特有状态仍遵循对应协议。PCI 复位等待
2. 队列复位是独立的可选功能
只有先协商 RING_RESET,驱动才可使用 queue_reset。选中队列后写 1,等待读回 queue_reset=0 且 queue_enable=0,再回收该队列资源、重新配置并启用。其他队列和设备级特性协商不随之重置;队列复位不能用来更换全设备的 split/packed 格式。本例没有选择 RING_RESET,不能走这条路径。
现代 PCI 传输通过已协商的队列复位或整设备复位停止队列。其 queue_enable 用于启用,QEMU 的公共配置写处理只接受 queue_enable=1;写 0 没有关闭队列的效果。MMIO 传输的寄存器操作另有约定。队列启用与复位、Linux 队列复位等待
七、初始化路径的源码调用链
| 阶段 | 驱动与设备两侧的入口 |
|---|---|
| 发现后复位并识别 | Linux register_virtio_device:reset、ACKNOWLEDGE,再匹配驱动 |
| 协商与确认 | Linux virtio_dev_probe:筛选、finalize、FEATURES_OK 读回,再调用设备驱动 probe |
| 分配并配置队列 | Linux setup_vq / vp_modern_find_vqs:建立 vring、写配置,最后启用 |
| 接收寄存器操作 | QEMU virtio_pci_common_write:特性、status、queue 地址与 enable/reset |
| 保存设备级约定 | QEMU 特性修改限制与复位:确认后不再直接接受新特性,复位清理旧运行状态 |
Linux 中设备驱动的 probe 可以自行调用 virtio_device_ready,核心层也会在未设置时补上 DRIVER_OK。阅读时应按状态顺序追踪,不能假定所有设备都在同一个函数内完成初始化。
八、从固定布局到明确协商与局部恢复
早期 legacy 接口缺少 FEATURES_OK 的确认边界,特性选择与首次使用常交织在一起;现代接口用能力区域描述位置,用状态握手确认布局,再放行数据处理。代价是驱动要增加校验与失败清理,但双方可以安全拒绝不兼容的组合。
split 队列将描述符、可用项和完成项分开,生产者与消费者的角色清楚;packed 将可用和完成状态放回描述符环,减少部分元数据访问,需额外处理 wrap 状态和事件结构。RING_RESET 又把恢复范围缩小到单队列,但要求双方同步停止旧访问。新增能力带来选择空间,也增加配置组合和状态机的验证成本。
排查“设备可见却没有 I/O”,可依次核对特性选择及依赖、FEATURES_OK 读回、三个队列地址与大小、queue_enable、DRIVER_OK,最后检查 available 与门铃。初始化成功只表明接口约定成立,吞吐和延迟仍取决于队列深度、通知策略与实际后端。
九、参考资料
- VIRTIO 1.2:§2.1–2.8 状态、特性、复位与队列;§3.1 初始化;§4.1.4–4.1.5 PCI 配置与操作。
- QEMU:现代 PCI 字段、split / packed 结构。
- Linux:virtio 核心状态与特性确认、现代 PCI 驱动。
- QEMU:设备特性与状态机、PCI 控制区域与通知。