virtio 完成通知与 KVM 中断注入
一、virtio 请求完成与客户机中断通知
virtio 后端完成请求后,把结果写入客户机内存中的 used ring,再根据通知条件向客户机发出中断。中间经过队列通知策略、PCI MSI-X 消息和虚拟 APIC;客户机驱动最终仍要读取队列,才能知道哪些请求已经完成。描述符提交与后端处理见virtqueue 的数据结构与操作。
本文跟踪现代 virtio-pci 设备的一个 split virtqueue,采用 x86、内核 irqchip、QEMU 用户态后端的普通通知路径。示例在非 IOThread 的设备处理上下文中直接发送 MSI-X;guest notifier 与 irqfd 是后文单独分析的替代路径。APIC 状态转换先按软件注入路径展开,再讨论硬件辅助。
一次完成经过四个检查点:used 已发布、通知条件满足、MSI-X 消息到达目标 LAPIC、客户机驱动取走 used 项。任一检查点之后都可能继续等待,因此“后端完成”与“客户机发现完成”之间仍有延迟。以下用队列 0 接收一个 1500 字节以太网帧,采用 12 字节 virtio 网络头,按 MSI-X 表项 2 向 APIC ID 1 投递向量 0x51。先追踪数据完成和通知,再对照编号、字段与源码。
源码版本:QEMU 10.2.0、Linux 6.18。
二、发布 used 与判定通知
设队列大小为 8,先前 used.idx = 7,本次完成的描述符链头为 3。QEMU 先写完成项,再推进索引。设备写入 1500 字节帧与 12 字节网络头,共 1512 字节,本次记录如下:
used.ring[7 % 8] = { id: 3, len: 1512 }
写屏障:先让完成数据与完成项可见
used.idx = 8
驱动读取:last_used_idx = 7 → 消费 ring[7] → last_used_idx = 8
len 表示设备写入缓冲区的字节数;本例接收记录为 1512,纯发送请求则可以为 0。virtqueue_split_flush() 在更新 used.idx 前执行写屏障;virtio_split_should_notify() 在读取通知条件前执行内存屏障。发布完成和决定通知是两个动作。完成发布、通知判定
未协商 EVENT_IDX 时,设备查看驱动设置的 VRING_AVAIL_F_NO_INTERRUPT。协商后,驱动用 used_event 表示希望被通知的完成位置,设备按 16 位环绕运算判断本次推进是否越过阈值:
need_event = (uint16_t)(new - event - 1) < (uint16_t)(new - old)
old = 7, new = 8, event = 7 → 0 < 1,发出通知
old = 7, new = 8, event = 8 → 65535 < 1 为假,暂不通知
这里假定 QEMU 已有有效的先前通知判定位置;首次尚未建立该记录时,代码会允许通知。通知抑制保留全部 used 记录,供驱动批量消费或轮询。驱动准备停止轮询时,需要重新启用通知并再次检查队列,处理“设备恰好在重新启用期间完成请求”的竞争。单纯打开中断后立即睡眠,可能错过已经发布的工作。
调大批处理范围可以减少通知与唤醒,但请求可能等到下一次轮询或通知才被消费。完成延迟因此同时受后端处理、通知策略和客户机调度影响。只统计中断次数,不能判断 I/O 是否更快。
三、MSI-X 消息与屏蔽状态
通知被允许后,virtio_pci_notify() 用队列配置的表项索引调用 msix_notify()。假设表项 2 已分配、MSI-X 已启用、功能级和表项级屏蔽均关闭,使用传统 xAPIC 消息格式、物理目的地、fixed delivery、edge trigger,向 APIC ID 1 发送向量 0x51:
MSI-X table[2]:
address_lo = 0xfee01000 // 0xfee00000 | (APIC ID 1 << 12)
address_hi = 0x00000000
data = 0x00000051 // vector = 0x51,fixed、edge
vector_control = 0x00000000 // bit 0:表项屏蔽位,当前为 0
地址选择中断目的地,data 编码向量和投递属性。这次写入针对客户机的中断消息地址空间,不会把 0x51 写入一页普通客户机 RAM。x2APIC 扩展目的地、逻辑目的地或中断重映射使用不同的编码与转换,不能直接套用这个数值示例。
QEMU 从表项组装 MSIMessage { address, data },经 PCI 地址空间写入进入 KVM APIC 的 MSI 区域,最终用 KVM_SIGNAL_MSI 提交 struct kvm_msi。这条直接路径把地址和 data 一起交给 KVM,由消息本身指定目标 APIC 与向量。MSI-X 发送、KVM APIC 消息入口
若表项被屏蔽,QEMU 设置 Pending Bit Array 中该表项的 pending 位,暂不发送。取消屏蔽时发现 pending,会清除该位并补发通知。一个 pending 位可以合并多个完成事件;驱动仍通过 used.idx 与 used ring 取得完整的完成记录。
这个屏蔽与 virtqueue 的通知抑制处于不同层。前者已有一次待发送的 MSI-X 通知,后者可能根本没有产生该次通知;LAPIC 接收后暂不能投递,又是第三种等待状态。排查“完成了但客户机没处理”时,需要分别查看。
四、从 LAPIC 待处理中断到驱动回调
KVM 把 MSI 地址和 data 解码为目的 APIC、向量、投递方式等字段,再送到目标虚拟 LAPIC。以未启用 APICv 的 Intel VMX 路径为例,vmx_deliver_interrupt() 设置 IRR[0x51],请求事件处理并 kick 目标 vCPU。kick 用于促使 vCPU 及时检查状态,不保证客户机立即执行中断处理函数。
| 状态 | 关键变化 | 仍需满足的条件 |
|---|---|---|
| 收到中断 | IRR[0x51] = 1,记录待处理向量 | vCPU 获得执行机会,APIC 启用,优先级允许 |
| 可以接受 | 软件注入路径清 IRR 对应位,设置 ISR[0x51],安排 VM Entry 中断注入 | 客户机 IF 与 interrupt shadow 等状态允许接受可屏蔽中断 |
| 客户机处理中断 | 通过客户机 IDT 和操作系统 IRQ 分派找到回调;驱动检查 used | 具体设备可以在硬中断中消费,也可以调度后续处理 |
| APIC EOI | 清除最高优先级的 in-service 位,重新计算优先级 | 只完成 APIC 层面的应答;不修改 used.idx 或回收描述符 |
此处的 ISR 是 LAPIC 的 In-Service Register,记录正在服务的中断向量。virtio-pci 的 ISR 则是设备状态字节,用于表达设备中断原因。MSI-X 模式下,驱动可依据已分配的中断关联找到队列,直接进入相应回调。
Mermaid 源码
sequenceDiagram
accTitle: virtio 完成发布、MSI-X 通知与客户机处理
accDescr: QEMU 先写 used 项并发布索引,通知条件满足后发送 MSI。KVM 记录 LAPIC 待处理状态,客户机能够接受中断时进入 IRQ 流程。APIC EOI 与驱动读取完成队列分属两个层次,EOI 不回收描述符。
participant D as QEMU 设备后端
participant M as 客户机内存中的队列
participant P as PCI MSI-X / KVM
participant A as 虚拟 LAPIC / vCPU
participant G as 客户机 IRQ 与驱动
D->>M: 写 used.ring[7] = id 3, len 512
D->>M: 写屏障后发布 used.idx = 8
D->>M: 屏障后读取 used_event = 7
Note over D,M: old 7 → new 8,跨过通知位置
D->>P: 表项 2 未屏蔽,发送 MSI address + data
P->>A: APIC ID 1,向量 0x51
A->>A: 软件路径设置 IRR,促使 vCPU 检查
Note over A,G: 等待 IF、优先级、执行机会等条件
A->>G: 接受中断:IRR → ISR,进入客户机 IRQ
Note over A,G: Linux 边沿 IRQ 可在通用应答阶段提前 EOI
G->>A: EOI,结束 APIC in-service 状态
G->>M: 回调或后续处理读取 used.idx 与 ring[7]
G->>G: 找回请求、处理结果、回收描述符链
Note over M,G: EOI 不改变 used;消费队列不等于 APIC 应答
Linux 的边沿中断处理可以在通用 IRQ 流程的应答阶段提前 EOI,随后执行设备回调。handle_edge_irq() 先调用 irq_ack,再执行 handle_irq_event();x86 LAPIC 的 apic_ack_edge() 经 apic_ack_irq() 发出 EOI。网络设备还可能把队列处理交给 NAPI。因而“进入中断”“发出 EOI”“消费 used”需要分别观察。通用边沿处理、APIC 应答
virtio 的 vring_interrupt() 检查队列是否有新完成,再调用该队列的回调。实际消费发生在 virtqueue_get_buf_ctx_split() 等路径:读取 used.idx、执行读屏障、获取 id / len、解除描述符链的占用,再推进驱动私有的 last_used_idx。used 项与描述符不会因 EOI 自动清零,后续环绕复用时才被覆盖。完成消费、中断回调
五、对照队列、MSI-X 表项与路由编号
这次收包已经走完从 used 发布到驱动回收的路径。沿途的队列 0、链头 3、MSI-X 表项 2、APIC ID 1 和向量 0x51 分别定位不同对象,由设备和中断配置串起来,数值无需相等。下表先对照这些状态,再加入下一节 irqfd 使用的路由编号。
| 对象 | 关键字段与示例 | 保存位置与访问方 |
|---|---|---|
| virtqueue 完成记录 | used.ring[i].id、len 各 32 位;used.idx 为 16 位 | 客户机 RAM,由 QEMU 映射访问;后端发布,客户机驱动按私有 last_used_idx 消费 |
| 完成通知条件 | avail.flags 或 avail 区末尾的 used_event,均为 16 位 | 客户机 RAM;驱动填写,后端读取并判定通知 |
| 队列 → MSI-X | 队列 0 的 queue_msix_vector = 2 | QEMU 设备对象的 VirtQueue.vector;客户机驱动通过设备配置接口选择表项 |
| MSI-X 表项 2 | 地址低 32 位、高 32 位、data 32 位、vector control 32 位,共 16 字节 | QEMU 的 MSI-X 模拟状态;客户机 PCI 子系统配置,QEMU 发送消息时读取 |
| 目标 LAPIC | APIC ID 1,向量 0x51;IRR、ISR 各按向量记录状态 | KVM 的每 vCPU 状态,可由 APIC 虚拟化硬件辅助维护;客户机按中断架构处理 |
| irqfd 替代路径的 KVM MSI 路由 | gsi = 37,type = KVM_IRQ_ROUTING_MSI,u.msi.address_lo / address_hi / data | 宿主内核;QEMU 经 ioctl 配置,irqfd 触发时 KVM 查找或使用缓存路由 |
客户机 Linux 还会为中断分配 Linux IRQ 号,用来查找处理函数;它同样不要求等于 0x51。MSI-X 表项索引可以被多个队列共用,驱动需要检查这些队列。消息本身不携带描述符 ID、完成字节数或报文内容。
前面的直接路径用 KVM_SIGNAL_MSI 每次传入消息。irqfd 改为预先把这条消息登记为一项 KVM 路由,再用 eventfd 触发它;这里选 GSI 37 作为路由标识。目标仍是同一个 APIC ID 1 和向量 0x51,改变的是后端把通知交给 KVM 的方式。
Mermaid 源码
flowchart TB
accTitle: virtqueue 完成通知的编号与路由
accDescr: 队列 0 选择 MSI-X 表项 2,表项保存目的 APIC ID 1 与中断向量 0x51。直接通知提交 MSI 消息;irqfd 替代路径使用 GSI 37 关联同一消息,各种编号互不等同。
Q["客户机 virtqueue 0<br/>used.ring 与 used.idx"]
V["通知条件成立<br/>queue_msix_vector = 2"]
T["MSI-X table[2]<br/>address = 0xfee01000<br/>data = 0x51 · 未屏蔽"]
D["直接路径<br/>KVM_SIGNAL_MSI<br/>提交 address + data"]
E["替代路径:guest notifier<br/>eventfd → irqfd<br/>KVM GSI = 37"]
R["预先配置的 MSI 路由<br/>GSI 37 → address + data"]
K["KVM 解码并投递<br/>物理目的地 APIC ID 1<br/>fixed · edge · vector 0x51"]
L["目标虚拟 LAPIC<br/>IRR / ISR 对应向量 0x51"]
Q --> V --> T --> D --> K --> L
T -. 配置消息与屏蔽关系 .-> R
V -. 已绑定 notifier 的分支 .-> E
E -. 查路由 .-> R
R -. 注入 .-> K
classDef queue fill:#e4f1ec,stroke:#438575,color:#173e34
classDef routing fill:#eaf0f6,stroke:#8198ac,color:#263c4c
classDef alternate fill:#f5f4f1,stroke:#a6aaa7,color:#3c4542
class Q,V,L queue
class T,D,K routing
class E,R alternate
六、irqfd 的内核中断注入路径
QEMU 为 guest notifier 配置 irqfd 时,先通过 KVM_SET_GSI_ROUTING 建立 GSI 37 到 MSI 地址及 data 的路由,再通过 KVM_IRQFD 绑定 eventfd 与 GSI 37。struct kvm_irqfd 的 fd、gsi、flags 和 resamplefd 均为 32 位字段;本例边沿 MSI 路径不使用电平中断的 resamplefd 应答协议。
后端发出 eventfd 事件后,KVM 的 irqfd_wakeup() 读取事件并尝试用缓存路由直接投递;当前上下文无法完成时,再安排工作队列。原先需要由用户态监听完成事件并调用 MSI 注入接口的场景,可以省去这次中转。进入目标 LAPIC 后,仍按前文的待处理、交付和驱动回收流程继续。eventfd 计数不等于客户机应收到同样次数的中断,请求完成仍以队列为准。
QEMU 10.2 的 virtio_irq() 在 IOThread 中使用 guest notifier;非 IOThread 的普通路径调用 virtio_notify_vector()。vhost 的完成通知也可以接到 eventfd,但其后端执行者已经不同。irqfd 路径必须随 MSI-X 路由修改、屏蔽、解除绑定同步更新,不能绕开这些设备语义。
irqfd 描述的是 eventfd 到虚拟中断的内核接口。硬件 posted interrupt 则属于后续的中断投递优化;是否启用还受处理器能力、虚拟机配置及 KVM 当前状态限制。使用 irqfd 并不自动意味着完全绕过宿主软件,更不保证没有 VM Exit。
七、源码:发布完成、路由与中断注入
| 阶段 | 函数与检查重点 |
|---|---|
| 完成与抑制 | virtqueue_split_flush() 先屏障后推进索引;virtio_split_should_notify() 判定 flags 或 used_event。 |
| 选择通知路径 | virtio_irq() 区分 IOThread notifier 与直接 vector 通知;virtio_pci_notify() 分派 MSI-X 或 INTx。 |
| 消息与 pending | msix_notify() 先检查分配与屏蔽;屏蔽状态更新 在解除屏蔽后补发 pending。 |
| 直接注入 | kvm_irqchip_send_msi() 提交 KVM_SIGNAL_MSI;kvm_msi_to_lapic_irq() 解码消息并选择 LAPIC。 |
| LAPIC 状态 | vmx_deliver_interrupt() 选择 posted interrupt 或 IRR 路径;kvm_apic_ack_interrupt() 更新 IRR / ISR;apic_set_eoi() 清 ISR 并更新优先级。 |
| irqfd | 路由与 notifier 绑定 区分表项索引和 GSI;irqfd_wakeup() 消费 eventfd 并发起内核投递。 |
八、批处理、irqfd 与 APICv 的优化边界
本例的检查顺序是:used.idx 是否推进到 8,通知阈值是否允许发送,MSI-X 表项 2 是否屏蔽,目标 LAPIC 是否记录 0x51,驱动的 last_used_idx 是否推进。只有先确认停在哪一步,才能判断优化对象是批处理、路由还是 vCPU 调度。控制器的历史与基础模型见中断虚拟化总览。
virtqueue 的批量完成与 EVENT_IDX 减少通知频率,irqfd 缩短宿主侧通知交接,APICv / posted interrupt 减少部分 APIC 模拟及投递开销。它们作用于不同阶段,可以组合,但不能相互替代。
| 机制 | 改善的位置 | 需要处理的约束 |
|---|---|---|
| MSI-X | 设备与队列的中断分配,减少共享源检查 | 向量资源、CPU 亲和性、表项屏蔽与 pending 一致性 |
| 通知抑制与批处理 | 单位请求的通知、唤醒和中断次数 | 批量与尾延迟权衡,重新启用时检查竞争 |
| irqfd | 完成事件到 KVM 中断注入的用户态中转 | eventfd 生命周期、路由更新及屏蔽处理 |
| APICv / posted interrupt | APIC 操作模拟及部分中断交付 | 硬件支持,vCPU 运行、阻塞与迁移状态的同步;不可用时回退 |
Linux 的 irqfd 引入提交记录了用 eventfd 连接中断注入的设计。当前 VMX 源码则同时保留 posted interrupt 和软件 IRR 两条路径:目标 vCPU 正在运行时可以借硬件同步待处理中断,未运行时仍需要唤醒或等待下一次进入。客户机的中断屏蔽、优先级与队列消费规则保持有效。