虚拟化中断传递的演进
虚拟化环境下中断传递是影响虚拟机 I/O性能的最核心因素之一,在虚拟化早期,每一次中断的产生和处理都会引发昂贵的上下文切换(VM Exit 和 VM Entry),导致极高的延迟和CPU开销,具体如下:
在宿主机中,设备发出中断信号 -> 中断控制器(如 APIC)接收 -> 提交给 CPU -> CPU 暂停当前任务并执行中断处理程序。但在虚拟化中,Guest OS 并不真正拥有物理硬件(Guest OS 不允许直接访问真实中断控制器)。传统上,当物理设备产生中断,或者 Guest OS 尝试操作虚拟中断控制器(如写入 EOI - 中断结束标志)时,都会触发 VM-Exit,将控制权交还给宿主机(Hypervisor),这会消耗数千个 CPU 周期。演进的核心目标就是:尽可能消除 VM-Exit。
为了达到接近宿主机的性能,虚拟化中断传递技术经历了最早的纯软件模拟到内核级模拟,再到CPU硬件辅助虚拟化,最终到现代硬件支持的直接中断注入(Posted Interrupt)的演进:
- 用户态纯软件中断虚拟化:如早期的 QEMU + KVM 架构
- 内核态中断模拟:kvm irqfd 机制,完全在内核,不经过QEMU,可减少内核用户切换;
- 硬件辅助的APIC虚拟化:APICv/AVIC 机制,极大减少了因为APIC访问引起的VM-Exit,解决了vCPU 处理中断时的开销,但是物理设备产生中断并传递给vCPU的第一步仍需要宿主机干预;
- 透传中断/硬件直通中断:Posted Interrupt 机制,随着IOMMU (Intel VT-d / AMD-Vi)出现,结合 APICv 机制,可实现物理设备直接向 vCPU 发送中断,全程 0 VM Exit,且不需要Host宿主机干预;
1.1 用户态纯软件中断虚拟化:QEMU + KVM 架构
物理设备触发物理中断,Host Kernel 接收并回调中断处理函数,该函数会唤醒QEMU用户态,QEMU ioctl 通知内核态KVM,KVM修改虚拟机状态注入中断,Guest OS开处理中断
该方案中断传递需要经过QEMU中断(即使用QEMU分配的interrupt eventfd):
优点:是实现简单,兼容性极强,可以模拟任何老旧的中断控制器;
缺点:一次中断需要经历内核态 -> 用户态 -> 内核态 的上下文切换,性能最差(2次上下文切换、1次 VM Exit、用户态参与);
// 正常路径(通过QEMU用户态,会触发VM-Exit)Host 物理设备产生 MSI-X中断 ⬇️ Host Linux IRQ(如45) ⬇️vfio_msihandler(irq=45,arg=eventfd_ctx):物理设备中断处理函数 ⬇️eventfd_signal(eventfd_ctx,1)⬇️ QEMU 的 eventfd 被唤醒 ⬇️ QEMU读取 eventfd ,知道有中断了 ⬇️ QEMU 中断处理函数vfio_msi_interrupt:KVM_IOCTL ⬇️ KVM 侧 kvm_set_irq 注入中断到Guest(会造成VM-Exit) ⬇️ Guest CPU 接收虚拟中断 ⬇️ Guest 内核中断子系统处理 ⬇️ 调用 Guest 内核分配中断号对应的中断处理函数vring_interrupt(irq=32,vqs[i])⬇️ Guest 设备驱动处理中断(以网卡virtio-net驱动为例) ⬇️ skb_recv_done/skb_xmit_done(Guest VM 接收数据,从RX队列取出数据包/通知Guest VM发送完成,物理网卡发送完数据包后通知Guest VM释放一发送的数据包缓冲区)1.2 内核态中断模拟:eventfd + kvm irqfd 机制
由于方案1中涉及到QEMU用户态参与,导致多次上下文切换,性能极差,因此该方案主要是优化在避免用户态 QEMU 参与中断路径。
该方案使用QEMU分配的kvm_interrupt eventfd,通过KVMirqfd机制,在Host接收到中断进行中断处理程序时,不会再唤醒QEMU,而是唤醒内核的KVM(因为用的是kvm_interrupt eventfd,而不是 QEMU 的interrupt eventfd),避免了QEMU用户态参与,完整流程如下:
// kvm irqfd机制:无需唤醒QEMUHost 物理设备产生 MSI-X 中断 ⬇️ Host Linux IRQ(如45) ⬇️vfio_msihandler(irq=45,arg=eventfd_ctx):物理设备中断处理函数 ⬇️eventfd_signal(eventfd_ctx,1)⬇️ eventfd 通知 KVM ⬇️ irqfd_wakeup schedule_work irqfd_inject kvm_set_irq 注入中断到Guest(无APICv机制) ⬇️ 设置 vAPIC IRR 设置 VMCS RVI VM Entry Guest 检查 IRR(读取 APIC IRR寄存器,由于没有APICv,这是一个特权指令,会触发VM Exit) VM Exit KVM 模拟APIC(触发 VM Exit, KVM 捕获这个访问,需要模拟APIC寄存器的访问,从内存中的vAPIC结构读取数据,将模拟结果返回给Guest,即VM Entry) VM Entry ⬇️ Guest 内核处理中断 ⬇️ 调用 Guest 内核分配中断号对应的中断处理函数vring_interrupt(irq=32,vqs[i])⬇️ Guest 设备驱动处理中断(以网卡virtio-net驱动为例) ⬇️ skb_recv_done/skb_xmit_done(Guest VM 接收数据,从RX队列取出数据包/通知Guest VM发送完成,物理网卡发送完数据包后通知Guest VM释放一发送的数据包缓冲区) ⬇️ EOI 访问(VM Exit,Guest 写入 EOI 寄存器触发 VM Exit,KVM捕获模拟EOI行为,清除 vAPIC 中的 ISR) VM Entry(返回 Guest)优点:消除了内核态到用户态(QEMU)的切换,性能大幅提升,是目前非直通设备的默认标准配置;
缺点:仍然存在大量的 VM-Exit(总 VM Exit 有3次:1)Guest 读取 IRR寄存器;2)Guest 写入 EOI 寄存器;3)其他可能的APIC访问)。Guest OS 读取中断寄存器、写入 EOI 等 APIC 操作,依然会触发 VM-Exit,导致 CPU 频繁陷入 Hypervisor;
1.3 硬件辅助APIC虚拟化:APICv/AVIC 机制
随着万兆网卡和 NVMe 存储的普及,内核态模拟的 VM-Exit 开销再次成为瓶颈。Intel 和 AMD 在 CPU 硬件层面引入了专门的指令和机制(Intel 称为 APICv,AMD 称为 AVIC),主要是为了减少 VM-Exit,提高性能。
工作原理:
CPU 硬件直接理解并接管部分虚拟 APIC 的操作,KVM 在内存中为 vCPU 维护一个“虚拟 APIC 页面(Virtual APIC Page)”,CPU 硬件会自动将其映射给虚拟机。
核心特性(以 Intel APICv 为例):
- 寄存器访问虚拟化 (APIC Register Virtualization):Guest OS 读取或写入大多数 APIC 寄存器时,直接操作物理内存中的 Virtual APIC Page,不触发 VM-Exit
- 虚拟中断传递 (Virtual Interrupt Delivery, VID):当 KVM 需要注入中断时,只需更新内存中的数据结构,如果 vCPU 正在运行,硬件会自动评估优先级并注入中断,无需 KVM 干预
- EOI 虚拟化:Guest 写入 EOI 结束中断时,硬件自动清除相关的虚拟状态位,不触发 VM-Exit
完整流程:
// kvm irqfd机制:无需唤醒QEMUHost 物理设备产生 MSI-X 中断 ⬇️ Host CPU ⬇️ Host Linux IRQ(如45) ⬇️vfio_msihandler(irq=45,arg=eventfd_ctx):物理设备中断处理函数 ⬇️eventfd_signal(eventfd_ctx,1)⬇️ eventfd 通知 KVM ⬇️ irqfd_wakeup schedule_work irqfd_inject kvm_set_irq 注入中断到Guest(有APICv) ⬇️ 设置 vAPIC IRR 设置 VMCS RVI VM Entry 硬件自动投递中断:1)检查 VMCS 的 RVI 字段;2)如果 RVI 不为0,获取中断向量;3)检查 Guest 的 IF(Interrupt Flag);4)如果 IF=1,投递中断到Guest;5)更新Guest 的虚拟APIC(APICv)状态 ⬇️ Guest 处理中断 ⬇️ 调用 Guest 内核分配中断号对应的中断处理函数vring_interrupt(irq=32,vqs[i])⬇️ Guest 设备驱动处理中断(以网卡virtio-net驱动为例) ⬇️ skb_recv_done/skb_xmit_done(Guest VM 接收数据,从RX队列取出数据包/通知Guest VM发送完成,物理网卡发送完数据包后通知Guest VM释放一发送的数据包缓冲区) ⬇️ 硬件自动处理EOI(由于APICv不会触发VM Exit):1)Guest 写入 E OI 寄存器;2)硬件捕捉这个写入;3)硬件自动清除 vAPIC 的 ISR;4)硬件自动检查是否有其他待处理的中断;5)如果有,自动投递下一个中断优缺点:
- 优点:极大减少了因为 APIC 访问和 EOI 写入引起的 VM-Exit(可减少约 90% 以上与中断相关的 VM-Exit, 总VM Exit 次数 0 次,硬件自动处理所有 APIC访问),I/O 吞吐量和延迟得到质的飞跃;
- 缺点:它解决了 vCPU 处理中断时的开销,但对于物理设备产生中断并传递给 vCPU 的第一步,仍然需要宿主机干预
1.2 和 1.3 方案主要是APICv,相关差异点:
中断投递方式差异点:
无 APICv: - KVM 在内存中设置 vAPIC IRR - Guest 读取 IRR 触发 VM Exit - KVM 模拟 IRR 读取 - Guest 获得中断信息 有APICv: - KVM 在内存中设置 vAPIC IRR - KVM 设置 VMCS RVI - 硬件在 VM Entry 时自动投递中断 - Guest 直接收到中断EOI 处理方式:
无 APICv: - Guest 写入 EOI 寄存器 - 触发 VM Exit - KVM 模拟 EOI 写入 - kVM 清楚 vAPIC ISR 有 APICv - Guest 写入 EOI 寄存器 - 硬件自动捕获 - 硬件自动清除 vAPIC ISR - 硬件自动检查下一个中断
PS:Virtual APIC Page 是一块内存区域,存储虚拟 APIC 的状态: ISR、IRR、TMR等APIC寄存器。
1.4 硬件直通中断:Posted Interrupt 机制
这是目前虚拟化中断传递的终极方案,通常与 SR-IOV(设备直通)和 VT-d(IOMMU 中断重映射)结合使用。它实现了物理设备直接向 vCPU 发送中断,全程 Zero VM-Exit。
1.3 方案得知当前方案已经减少了 VM Exit 的次数,但是仍需要宿主机干预(Host CPU还需要参与),本质是则当物理中断到来时,VFIO 调用通用的、稍微臃肿的eventfd_signal 走通知 KVM 进行中断注入流程,该过程需要操作自旋锁(spinlock)、遍历等待队列(waitqueue),并且往往依赖于调度器去唤醒 KVM 的处理逻辑。
因此需要进一步优化,避免使用eventfd_signal通知机制,而是可以通过通过一个事先注册好的函数指针,直接调用 KVM 的中断注入函数,完整流程图:
Posted Interrupt 机制中断传递流程: Host 物理设备产生 MSI-X 中断 ➡️ IOMMU ➡️ IRTE ➡️ PID ➡️ Guest CPU ➡️ Guest APIC ➡️ Guest 处理中断 ➡️ Guest 调用对应中断处理函数 vring_interrupte(irq=32, vqs[i]) ➡️ 硬件自动处理EOI 不启用 Posted Interrupt 机制中断传递流程(需要 Host CPU参与通过eventfd通知机制通知KVM): Host 物理设备产生 MSI-X 中断 ➡️ IOMMU ➡️ IRTE ➡️ Host CPU ➡️ Host IRQ ➡️ vfio_msihandler ➡️ eventfd_signal ➡️ KVM ➡️ ➡️ Guest APIC ➡️ Guest 处理中断 ➡️ Guest 调用对应中断处理函数 vring_interrupte(irq=32, vqs[i]) ➡️ 硬件自动处理EOIPosted Interrupt机制中断传递流程每步骤拆解分析:
物理设备产生 MSI-X 中断
/* * MSI- X 消息格式: * Address:0xFEExxxx(IOMMU的地址) * Data:中断向量号 */structmsi_msg{u32 address_lo;// 低32位地址u32 address_hi;// 高32位地址u32 data;// 中断数据(向量号)}IOMMU 接收 MSI-X 消息
- 检查MSI-X消息的地址是否在 IOMMU的地址范围内
- 查找对应的 IRTE (Interrupt Remapping Table Entry)
- 根据 IRTE 的配置决定如何投递传递
IOMMU 查找 IRTE:IOMMU 根据 MSI-X 消息查找 IRTE
/* * 关键字段: * dest_id:Guest vCPU 的 APIC ID * vector:Guest 中断向量号 */structirte{u64 present:1;// IRTE 是否有效u64 dest_mode:1;// 目标模式(0=物理,1=虚拟)u64 delivery_mode:1;// 投递模式(0=Fixed)u64 tigger_mode:1;// 触发模式u64 dest_id:8;// 目标 ID (Guest vCPU 的 APIC ID)u64 vector:8;// 中断向量(Guest 中断向量)...u64 pda_l:26;// Posted Interrupt Decriptor Lowu64 pda_h:32;// Posted Interrupt Decriptor High}IOMMU 检查 IRTE 配置:检查该IRTE表项是否配置了 Posted Interrupt
IOMMU 访问 PID(Posted Interrupt Descriptor)
// PID 物理地址u64 pid_addr=((u64)irte->pda_h<<32)|irte->pda_l;structposted_interrupt_desc{u64 pd[4];}// PD格式:Bit0-255:中断向量位图-Bit0:向量0-Bit1:向量1-.....-Bit255:向量255示例:如果Guest 中断向量是32:PD[0]=0x0000000100000000,Bit32被设置IOMMU 更新 PID:IOMMU 在 PID 中设置对应的中断向量位
IOMMU 向 Guest GPU 发送通知
- IOMMU 向 Guest CPU 的 Local APIC 发送一个特殊的 IPI
- 这个 IPI 的向量是 Posted Interrupt Notification Vector(通常是0xF0)
- Guest CPU 收到这个通知后,会检查 PID
Guest CPU 处理 Posted Interrupt Notification:
- Guest CPU 收到该通知后,读取PID,检查PID中的中断向量位图
- Guest CPU 将设置的中断向量注入到自己的虚拟APIC
Guest APIC 接收中断
Guest CPU 处理中断:执行中断处理程序
总结:整个过程中没有 VM Exit,且不需要 Host CPU 参与,而1.3章节仍然需要 Host CPU 处理物理中断号,以达到性能最优。
