QEMU/KVM 中断虚拟化:基本原理与演进
一、中断的作用与虚拟化边界
中断用于将设备完成、定时器到期等事件通知处理器,使操作系统及时执行相应的处理程序。中断虚拟化为每台客户机维护独立的中断控制器状态、路由和处理入口,并在客户机允许接收时交付事件。以网卡收包为例,报文先进入接收缓冲区,通知路径再促使客户机驱动检查队列;数据传输与中断交付是相互关联的两个过程。
1. 轮询与中断:主动检查,或等待通知
一种办法是让 CPU 反复检查接收队列,这叫轮询。检查足够频繁时,响应可以很快;没有包到达时,也要消耗 CPU 时间。另一种办法由设备在有工作时发出中断,CPU 平时继续计算或进入空闲,收到通知后转入操作系统的处理入口。代价是保存与恢复执行现场,以及处理通知本身的开销。高流量网络常把两者结合:中断启动处理,再通过轮询批量收包。
2. 一次中断需要完成三件事
设备先保存数据和完成状态,再发出通知;中断系统决定通知哪个 CPU、使用哪个入口;CPU 在允许接受中断时保存必要的执行现场,经中断描述符表(IDT)进入内核,驱动再读取队列或设备状态。处理结束后,系统恢复先前的执行。网络包保存在缓冲区中,中断携带的是处理入口等控制信息。
本文围绕 x86 的一次 virtio-net 收包展开:后端填好缓冲区,通知指定 vCPU,客户机驱动读取结果。先建立这条完整路径,再解释中断控制器、内核注入与硬件辅助分别改善了哪一段。
源码版本:QEMU 10.2.0、Linux 6.18。
二、一次收包的数据路径与通知路径
设客户机已给接收队列 0 提供空缓冲区,后端收到一个 1500 字节以太网帧。客户机把这个队列的通知配置到 vCPU 1,对应中断向量 0x51;向量是客户机查找中断处理入口的编号。处理分为四步:
- 保存结果。后端把网络头和帧写入客户机缓冲区,再发布 used 完成记录。驱动今后据此找到数据与实际长度。
- 提交通知。通知条件满足时,设备按配置发出中断消息。多个完成可以合并为一次通知,帧内容仍保存在内存里。
- 等待并交付。虚拟中断控制器记录待处理向量。目标 vCPU 获得执行机会、客户机允许接收时,处理器才转入客户机的中断入口。
- 消费数据。客户机驱动检查队列,安排 NAPI 批量处理,把帧交给网络栈,并补充新的接收缓冲区。
Mermaid 源码
flowchart TB
accTitle: 从设备完成到客户机驱动处理
accDescr: 完成记录先于通知发布,通知按路由进入虚拟 LAPIC 并等待客户机接受。vCPU 阻塞时需要先唤醒宿主线程;客户机驱动最终读取 used 队列确定完成项。
C["后端收到 1500 字节报文"] --> B["客户机接收缓冲区<br/>先写网络头与报文"]
B --> U["发布 used 记录<br/>写屏障 → 更新 used.idx"]
U --> N{"需要通知?"}
N -->|是| R["按已配置路由提交通知<br/>目标 vCPU 1 · 向量 0x51"]
N -->|否| P["客户机轮询或后续通知<br/>used 记录仍保留"]
R --> L["虚拟 LAPIC pending<br/>等待可接受条件"]
L -. "目标未运行" .-> W["阻塞时唤醒线程<br/>等待宿主调度"]
W --> A["客户机接受中断<br/>IDT → IRQ 分派"]
L -->|目标已运行且允许接受| A
A --> D["驱动检查 used<br/>回收完成项"]
P --> D
B -. 实际报文数据 .-> D
从后端写完数据到客户机消费数据,还可能经历通知合并、客户机屏蔽、宿主调度和驱动轮询等等待。下面沿同一个帧,分别说明路由、等待状态与减少交接成本的方案。
三、设备通知与中断路由
接收队列 0 与 vCPU 1、向量 0x51 的关联由客户机驱动和中断子系统配置。硬件需要表达并执行这组配置。PIC、APIC 和 MSI-X 分别体现了设备请求汇集、多处理器路由和消息化通知的发展,现代虚拟机仍要提供客户机能够识别的这些接口。
1. PIC:集中处理设备的中断请求
早期 PC 使用 8259 PIC(可编程中断控制器)汇集设备的中断线,记录请求、处理屏蔽和优先级,再通知 CPU。单片有 8 个输入,传统 PC 用两片级联得到 15 个可供外部使用的输入。设备增加、多个设备共享中断线时,CPU 还要询问相关设备,才能确定谁需要处理;固定的输入与目标也难以满足多处理器分工。
2. APIC:把路由与每个 CPU 的接收状态分开
APIC 把工作拆开:I/O APIC 汇集外部中断线,并按路由表选择目标;每个逻辑 CPU 的 Local APIC(LAPIC)负责本地的待处理中断、优先级、定时器和处理器间中断。这样,操作系统可以把设备工作分配到不同 CPU。
在物理机器上,I/O APIC 属于平台的中断路由设施,LAPIC 则与逻辑 CPU 关联。例如启用全部线程的 16 核、32 线程 x86 服务器有 32 个逻辑 CPU,各自具有 LAPIC 状态。虚拟机看到的是自己的拓扑:一台 4 vCPU 的客户机有 4 份虚拟 LAPIC 状态,宿主调度这些 vCPU 时维护其归属。
其中两个编号尤其重要:APIC ID 选择目标 CPU,中断向量选择该 CPU 的中断入口。例如“发给 APIC ID 1,向量 0x51”,表示让目标 CPU 按 IDT 中的 0x51 号入口处理。操作系统的 IRQ 号、设备队列号和向量属于不同编号空间,由配置关联起来。
3. MSI/MSI-X:设备用消息表达通知
传统 PCI INTx 中断线可能被多个设备共用。MSI 改用设备向特定地址发出一次写事务,地址与数据编码中断目的地和向量。MSI-X 进一步提供可独立配置地址、数据和屏蔽状态的表项,方便把不同队列分配给不同 CPU。消息最终仍交给目标 LAPIC 接收和排队;这条路径不需要经过 I/O APIC 的引脚路由。Linux MSI 文档
Mermaid 源码
flowchart LR
accTitle: 设备、平台与 CPU 中的中断路径
accDescr: 按物理组件分组。设备的 INTx 引脚经平台 I/O APIC 路由,MSI-X 消息直接指向目标 LAPIC;CPU 接受中断后按 IDT 进入操作系统处理入口。图中省略总线和可能存在的中断重映射。
subgraph DEVICE["设备"]
PIN["INTx 中断线"]
MSG["MSI-X 表项<br/>地址 · 数据 · 屏蔽"]
end
subgraph PLATFORM["平台中断路由"]
IO["I/O APIC<br/>引脚 → CPU 与向量"]
end
subgraph PROCESSOR["目标逻辑 CPU"]
LAPIC["Local APIC<br/>待处理 · 优先级 · 服务状态"]
CPU["接受中断<br/>保存现场 · 按向量查 IDT"]
end
DRIVER["操作系统中断入口<br/>驱动读取队列或设备状态"]
PIN --> IO --> LAPIC
MSG -->|消息写事务| LAPIC
LAPIC -->|条件允许| CPU --> DRIVER
回到本例:客户机把接收队列 0 关联到一个 MSI-X 表项,表项中的地址与数据指定 APIC ID 1 和向量 0x51。设备发出消息后,目标 LAPIC 记录待处理向量。虚拟机也沿用这套接口;接下来要落实的是,QEMU 保存的设备配置怎样转为 KVM 中的待处理中断,以及 vCPU 怎样进入客户机处理程序。
四、QEMU/KVM 的中断模拟与交付
先采用一条便于跟踪的路径:QEMU 在用户态处理 virtio-net 收包,客户机启用 MSI-X,KVM 维护虚拟 LAPIC;Intel VMX 的中断交付使用软件注入路径。后面的 irqfd 和 APICv 优化都以这条路径为参照。
1. 每台虚拟机独立的控制器状态
客户机写入的 MSI-X 表项由 QEMU 的设备模型保存,每个 vCPU 的 LAPIC 待处理位、优先级和服务状态由 KVM 维护,IDT 则保存在客户机内存中。QEMU 向本虚拟机的 KVM 接口提交消息,KVM 在这台虚拟机内选择目标 vCPU。客户机的 APIC ID 1 不代表宿主的物理 CPU 1:vCPU 线程会由宿主调度到可用的物理 CPU。
2. 从 QEMU 的完成记录到 KVM 的待处理向量
- QEMU 完成接收。把这个 1500 字节帧写入缓冲区,发布 used 记录,并在通知条件满足时读取队列关联的 MSI-X 表项。
- QEMU 提交消息。设备模型组装 MSI 地址和数据,经 KVM APIC 的消息入口,以
KVM_SIGNAL_MSI提交给宿主内核。 - KVM 选择目标。解码出 APIC ID 1 和向量 0x51,找到对应的虚拟 LAPIC。软件注入路径设置
IRR[0x51],标记有事件需要处理,并促使目标 vCPU 检查事件。 - vCPU 接受中断。满足客户机屏蔽、优先级等条件后,KVM 安排 VM Entry 中断注入。处理器进入客户机的 0x51 号 IDT 入口,客户机内核再分派到驱动回调。
Mermaid 源码
sequenceDiagram
accTitle: QEMU virtio-net 经 KVM 软件注入向量 0x51
accDescr: QEMU 先写报文和 used 记录,再提交 MSI 消息。KVM 按 APIC ID 找到 vCPU 1,设置 IRR 并请求事件处理;条件满足后经 VM Entry 注入,客户机按 IDT 进入处理程序。驱动消费和 EOI 分别推进队列与 APIC 状态。
participant Q as QEMU 网卡模型
participant K as KVM 虚拟 LAPIC
participant V as vCPU 1 / 客户机
Note over Q: 发布 1500 字节帧<br/>更新 used
Q->>K: KVM_SIGNAL_MSI:目标 1,向量 0x51
Note over K: IRR[0x51] = 1<br/>请求事件处理<br/>促使 vCPU 检查
Note over K,V: 等待 vCPU 运行及客户机允许接受
Note over K: 清 IRR[0x51],置 ISR[0x51]
K->>V: VM Entry 注入向量 0x51
Note over V: IDT → IRQ 入口<br/>Linux 边沿应答
V->>K: EOI:结束 APIC 服务状态
Note over V: 驱动回调 → NAPI<br/>读取 used,处理报文
这几步分别可以在 msix_notify()、KVM APIC 消息入口和 vmx_deliver_interrupt() 中对应起来。消息提交成功表示 KVM 已处理这次投递请求,客户机驱动何时执行,还取决于下一段的接受条件。
3. 待处理、正在服务与驱动消费
IRR[0x51] 记录等待中的请求;向量被取出用于注入时,软件路径清除相应 IRR 位并设置 ISR[0x51],记录正在服务的中断。vCPU 若处于阻塞状态,需要先唤醒宿主线程;若因 CPU 竞争被抢占,需要等待重新调度。即使已经运行,客户机的 IF、interrupt shadow 和 APIC 优先级也可能暂时阻止接受。
客户机发出的 EOI(中断结束应答)结束 APIC 层面的服务状态。驱动消费报文是另一项工作,Linux 还可以把它交给 NAPI 后续轮询。因此 EOI 不代表这个 1500 字节帧已经交给网络栈,used 也不会因 EOI 被自动回收。中断注入专篇继续展开这些状态与字段。
4. 控制器状态的放置与内核 irqchip
上面的流程已经把 LAPIC 放在 KVM 中。早期完整的软件模拟可在 QEMU 保存 PIC、I/O APIC 和 LAPIC 的寄存器及待处理状态,但客户机访问控制器和接受事件会增加用户态与内核的往返。in-kernel irqchip 将高频控制器处理移入 KVM,使许多访问和状态检查可以留在内核完成。
QEMU 在宿主用户态模拟设备,KVM 在宿主内核维护虚拟 LAPIC。x86 的两种内核 irqchip 配置进一步区分传统引脚控制器的归属:
| kernel-irqchip 配置 | PIC 与 I/O APIC | 每 vCPU 的 LAPIC |
|---|---|---|
| on:完整内核 irqchip | KVM,宿主内核 | KVM,宿主内核;可结合硬件辅助 |
| split:split irqchip | QEMU,宿主用户态 | KVM,宿主内核;可结合硬件辅助 |
split 把传统引脚控制器留在用户态,缩小内核负责的控制器范围,代价是相应引脚路径需要用户态参与。两种配置都保留内核 LAPIC。QEMU 8.0 已移除带 LAPIC 的 KVM 客户机使用用户态 LAPIC 的模式,前文的完整软件控制器路径用于说明模拟原理与历史演进。KVM irqchip 接口、QEMU 的接口演变
两种配置下,本例的 MSI-X 消息都要交给内核中的目标 LAPIC。接下来可以分别优化两段开销:后端完成事件进入 KVM 的交接,以及 KVM 将待处理向量交付给 vCPU 的过程。
五、中断路径的开销与加速
先把刚才的流程按开销位置分开。设备完成后,消息要从设备后端进入 KVM;KVM 随后检查虚拟控制器状态并安排客户机接受中断。irqfd 主要改变前一段的通知入口,APICv / AVIC 主要加速后一段的控制器访问与交付。MSI-X 表项决定的目标和向量仍然有效。
1. irqfd:减少独立后端的用户态通知中转
现在把收包处理从 QEMU 切换到 vhost-net:报文和 used 记录由内核后端填写。如果完成通知还要唤醒 QEMU,再由 QEMU 调用注入接口,就会为通知重新走一趟用户态。irqfd 允许 QEMU 事先把后端的 eventfd 与 KVM 中断路由绑定,使这次交接直接在内核完成。独立的 vhost-user 后端也能使用这一机制,设置和更新路由仍由管理路径负责。
本例仍向 vCPU 1 交付 0x51:后端触发已绑定的 eventfd,KVM 按预先配置的路由处理。报文与完成记录保存在共享内存中,eventfd 只传递通知。若设备后端本身仍在 QEMU 中,收包处理也仍由 QEMU 执行,不能由使用 irqfd 推导出整个 I/O 路径都绕过 QEMU。
与完成方向对应,ioeventfd 用于请求方向,连接客户机的设备通知写入与后端唤醒:
| 接口 | 通知方向 | 减少的中转 |
|---|---|---|
| ioeventfd | 客户机写设备通知地址 → KVM → 后端 eventfd | 这次通知写入可由 KVM 处理,无需返回 QEMU 的 vCPU 线程做设备访问分派 |
| irqfd | 后端 eventfd → KVM 路由 → 客户机中断 | 完成事件无需先唤醒用户态监听者,再调用中断注入接口 |
ioeventfd 路径仍可能产生硬件 VM Exit,只是 KVM 可以在内核完成匹配和通知;irqfd 的交付同样受客户机中断状态与 vCPU 调度限制。两者在同一设备上分别服务于请求与完成方向。KVM eventfd 实现
2. APICv / AVIC:减少控制器访问与虚拟中断交付的退出
事件进入 KVM 后,前一节的软件路径还要记录 IRR、促使 vCPU 检查事件,并安排 VM Entry 注入;部分 APIC 寄存器访问也会使客户机退出。Intel APICv 用虚拟 APIC 状态与执行控制,让硬件接管部分访问、优先级判断和中断交付;AMD AVIC 也沿硬件辅助方向降低开销,但使用不同的数据结构和通知机制。
以 Intel posted interrupt 为例,KVM 可以先把向量记入 Posted Interrupt Descriptor 的 PIR 位图,再按需发送通知。目标 vCPU 正在合适的运行状态时,处理器可同步待处理向量并向客户机交付,避免为这段交付专门退出。客户机的屏蔽和优先级规则仍然有效。irqfd 的投递也可以进入这条路径,因此 irqfd 与 APICv 可以配合使用。KVM 的 posted 与软件回退路径
3. 直通设备分支:用 interrupt posting 减少宿主转发
前面讨论的是 virtio 软件后端。换成 VFIO 分配的物理网卡后,数据与通知由实际设备产生,驱动和设备路径也随之改变。如果物理中断先进入宿主处理程序,再由 VFIO 等路径转交 KVM,即使 CPU 支持虚拟中断加速,仍然保留这段转发。支持 Intel VT-d interrupt posting 的硬件可把设备中断经中断重映射单元直接写入目标 vCPU 的 posted 描述符,在符合条件时省去宿主软件转发。
这需要设备分配、IOMMU、CPU 和内核配置共同支持。vCPU 阻塞时仍需宿主唤醒线程;vCPU 被抢占时要保存待处理状态,等待重新调度。硬件能够减少投递开销,不能替宿主调度器分配 CPU 时间。AMD 的 AVIC/中断重映射有自己的实现,不能套用 Intel 描述符字段。阻塞与调度处理
Mermaid 源码
flowchart TB
accTitle: irqfd、APICv 与直通设备 interrupt posting 的不同位置
accDescr: 软件后端可以通过 irqfd 进入 KVM 路由,再使用软件注入或 APICv posted 路径。满足条件的 Intel VT-d 直通设备可由 IOMMU 写入 posted 描述符。阻塞或抢占状态仍需宿主调度配合。
BACK["独立软件后端<br/>如 vhost-net / vhost-user"] --> EVENT["事件接入:eventfd → KVM irqfd<br/>按已配置路由投递"]
EVENT --> SW["KVM 虚拟 LAPIC:软件注入<br/>IRR → 事件检查 → VM Entry 注入"]
EVENT -->|可用时| PI["Intel Posted Interrupt Descriptor<br/>PIR 待处理位图 + 通知状态"]
DEV["另一条路径:VFIO 物理设备<br/>设备发出 MSI/MSI-X"] -->|已配置且平台支持| IOMMU["VT-d 中断重映射<br/>interrupt posting"]
IOMMU --> PI
PI --> HW["目标 vCPU 运行时<br/>APICv 辅助接收与交付"]
SW --> GUEST["客户机中断入口<br/>驱动处理队列"]
HW --> GUEST
PI -.->|目标未运行| SCHED["宿主唤醒或后续调度<br/>同步待处理状态再执行"]
SCHED -.-> HW
六、中断机制的演进与实现取舍
沿这次收包过程,可以把演进归为三条线:设备接口从中断线走向消息;虚拟控制器处理从用户态移入内核;后端通知与 vCPU 交付逐步减少软件往返。下表汇总每项改进消除的成本,以及仍需承担的工作。
| 阶段 | 原有问题与改进 | 仍需承担的工作 |
|---|---|---|
| PIC → APIC / I/O APIC | 从集中、有限的中断输入,发展到多 CPU 路由与每 CPU 状态管理 | 引脚分配、路由配置、优先级与应答 |
| PCI INTx → MSI / MSI-X | 消息与独立表项改善共享中断辨源和多队列分工 | 向量资源、亲和性与通知频率 |
| QEMU 软件控制器 → KVM 内核 irqchip | 减少控制器处理时的用户态往返 | 内核中的模拟、事件检查及可能的 VM Exit |
| irqfd(2009 年提出) | 把 eventfd 后端通知连接到 KVM 路由,省去用户态注入中转 | eventfd 与路由生命周期、屏蔽同步 |
| APICv / AVIC | 硬件辅助 APIC 操作与虚拟中断交付,减少部分退出 | 硬件能力、不能加速的操作与回退处理 |
| IOMMU interrupt posting | 把设备中断直接送到虚拟中断状态,减少直通设备的宿主软件转发 | 平台能力、vCPU 调度迁移、阻塞唤醒与隔离 |
irqfd 的原始提案说明了 eventfd 连接中断的设计。后来增加的 APICv/AVIC 并没有使 irqfd 失去作用:前者着重减少控制器与 CPU 交付的开销,后者把后端事件接入 KVM。
一台虚拟机可以同时使用 MSI-X、内核 LAPIC、irqfd 和 APICv:它们分别承担消息表达、状态维护、事件接入和交付加速。端到端延迟还包含通知合并、宿主调度与驱动批处理,判断配置的效果需要把这些阶段分别观察。
七、中断配置、计数与延迟观察
可以先在客户机检查网卡采用的中断方式,再观察相应处理次数。下面的 0000:00:03.0 是示例 PCI 地址,应替换为 lspci -nn 中实际网卡的地址;读取完整配置空间可能需要管理员权限。
# 在客户机执行,只查询状态
lspci -nn
lspci -vv -s 0000:00:03.0
cat /proc/interrupts
MSI-X: Enable+ 表示客户机已启用该设备的 MSI-X。收包前后对照 /proc/interrupts 中网卡相关行,可以看到客户机各 CPU 的中断处理计数变化。多个包可以共用一次通知,后续还可能由 NAPI 轮询处理,因此这些计数既不等于报文数量,也不能证明宿主启用了 irqfd 或 APICv。宿主的 /proc/interrupts 则反映宿主自身的中断处理,须与客户机视图分开。
报文到达后仍长时间未被客户机处理,可以按顺序检查以下阶段:
- 结果发布。查看缓冲区、used 索引和驱动消费位置。后端尚未完成时,优化中断注入没有帮助。
- 通知产生与路由。检查通知抑制、MSI-X 屏蔽与 pending、目标 CPU 和向量,区分没有发出与已经等待。
- 客户机接受与执行。区分中断屏蔽、优先级等待、vCPU 阻塞和宿主 CPU 竞争。投递变快不会自动消除调度延迟。
- 驱动消费。中断处理可能只启动 NAPI,后续批量大小、网络栈与应用调度继续影响尾延迟。
中断优化的目标是以合理的 CPU 成本及时处理工作。降低中断次数可能提高吞吐,也可能让低负载报文等得更久;增加轮询可缩短等待,却占用 CPU。比较时同时记录吞吐、尾延迟、每次通知的处理量和 CPU 占用,才能判断优化是否对实际负载有效。
八、进一步阅读
- virtio 完成通知与 KVM 中断注入:继续展开 MSI-X 字段、IRR/ISR、屏蔽与源码调用。
- KVM 执行与退出:对比设备访问、ioeventfd 和 irqfd 两个通知方向。
- vCPU 阻塞、轮询与唤醒:跟踪中断到达后的调度与运行时序。
- KVM 虚拟时钟与定时器:时钟读取与定时中断的分工,以及到期后的唤醒和处理。
- virtqueue 数据结构与交互图:观察描述符、队列与缓冲区的读写关系。
- VFIO 设备直通:理解直通设备的数据访问与隔离,区分 DMA 重映射和中断重映射。
九、参考资料
- Intel SDM:中断与异常、Local APIC、APIC virtualization 与 posted-interrupt processing。
- Linux MSI Driver Guide:引脚中断、MSI 与 MSI-X 的差别。
- QEMU PIC、I/O APIC、软件 LAPIC、MSI-X 通知。
- KVM API:CREATE_IRQCHIP、SPLIT_IRQCHIP、GSI routing 与 IRQFD;PV EOI、PV IPI。
- KVM LAPIC 待处理与服务状态、VMX 中断交付、AMD AVIC。
- Intel VT-d 规范第 5 章:Interrupt Remapping 与 Interrupt Posting;KVM 对应实现。