Table of Contents
CREDIT:
这篇文章基本上是根据 kvm interrupt emulation 完成的,这其中有一些细节在原文中没有详细讲到,因此就有了本篇文章。
名词解释
Interrupt Service Routing (ISR)
a special function or block of code that executes in response to an interrupt. When an interrupt occurs, the processor temporarily stops normal execution, jumps to the ISR (using the interrupt vector), executes it.
Interrupt line
A physical signal (hardware) used to notify the CPU of an interrupt.
Interrupt vector
A memory address (software) used to locate the ISR for a given interrupt.
Interrupt Request Register (IRR)
Holds the pending interrupts that have been requested but not yet acknowledged by the CPU.
In-service Register (ISR)
Tracks which interrupts are currently being serviced by the CPU.
Interrupt Mask Register (IMR)
Controls which interrupts are allowed to be processed by the CPU.
中断传递模式
XT-PIC
每个 PIC 芯片有 8 个 pin,一共有 master 和 slave 两个芯片,即 16 pin,设备通过触发 PIC 针脚(pin),PIC 根据针脚向 CPU 传递在 data bus 上传递中断向量(即 IRQ index),然后根据 base + IRQ index 就能找到对应的 ISR。
给定一个 irq(需要小于 16),irq >> 3
或者 irq / 8
选择第几个 PIC 芯片,irq & 7
选择 PIC 芯片的针脚。
Slave 芯片会连接到 master 芯片的 PIN 2(参见 [1] 中的图片)
I/O APIC
设备触发 I/O APIC,然后 I/O APIC 将中断向量(interrupt vector)传递给 LAPIC(即每个 CPU 核心的 APIC),然后找到对应的 ISR 调用。
MSI
设备直接将中断注入到 LAPIC。
其中 I/O APIC 有 8 个 中断可用于共享。为啥要共享?因为 I/O APIC 支持 interrupt line 的数量是有限的,没办法每个设备独占一个。那么如何共享?这就需要 ISR 需要依次遍历并找到需要服务的设备,所以效率一定就变低了。
KVM
中断芯片(irqchip)初始化
KVM 提供了一个 ioctl KVM_CREATE_IRQCHIP
,在 x86 架构下,会创建 PIC 和 I/O APIC 两个芯片。

IRQCHIP 创建的流程如下
- 1: 初始化
struct kvm_pic
,根据结构体层次图,PIC 拥有三个芯片master
、slave
和elcr
,这个函数还将每个芯片赋予了read
和write
方法。 - 2: 初始化
struct kvm_ioapic
,给dev
赋予read
和write
方法。 - 3: 设置默认中断路由表。
kvm_arch_vm_ioctl()
-> case KVM_CREATE_IRQCHIP
-> 1: kvm_pic_init()
-> 2: kvm_ioapic_init()
-> 3: kvm_setup_default_irq_routing()
路由表相关的结构体关系如图所示,默认路由表的定义都在上方,包括 kvm_irq_routing_entry
、kvm_irq_routing_irqchip
和 kvm_irq_routing_msi
,与 KVM 相关的都在下方,包括 kvm_irq_routing_enable
和 kvm_kernel_irq_routing_entry
。

中断路由表在 x86 使用的是默认路由表,定义在 arch/x86/kvm
中
#define SELECT_PIC(irq) \
((irq) < 8 ? KVM_IRQCHIP_PIC_MASTER : KVM_IRQCHIP_PIC_SLAVE)
#define IOAPIC_ROUTING_ENTRY(irq) \
{ .gsi = irq, .type = KVM_IRQ_ROUTING_IRQCHIP,\
.u.irqchip = { .irqchip = KVM_IRQCHIP_IOAPIC, .pin = (irq) } }
#define ROUTING_ENTRY1(irq) IOAPIC_ROUTING_ENTRY(irq)
#define PIC_ROUTING_ENTRY(irq) \
{ .gsi = irq, .type = KVM_IRQ_ROUTING_IRQCHIP,\
.u.irqchip = { .irqchip = SELECT_PIC(irq), .pin = (irq) % 8 } }
#define ROUTING_ENTRY2(irq) \
IOAPIC_ROUTING_ENTRY(irq), PIC_ROUTING_ENTRY(irq)
static const struct kvm_irq_routing_entry default_routing[] = {
ROUTING_ENTRY2(0), ROUTING_ENTRY2(1),
ROUTING_ENTRY2(2), ROUTING_ENTRY2(3),
ROUTING_ENTRY2(4), ROUTING_ENTRY2(5),
ROUTING_ENTRY2(6), ROUTING_ENTRY2(7),
ROUTING_ENTRY2(8), ROUTING_ENTRY2(9),
ROUTING_ENTRY2(10), ROUTING_ENTRY2(11),
ROUTING_ENTRY2(12), ROUTING_ENTRY2(13),
ROUTING_ENTRY2(14), ROUTING_ENTRY2(15),
ROUTING_ENTRY1(16), ROUTING_ENTRY1(17),
ROUTING_ENTRY1(18), ROUTING_ENTRY1(19),
ROUTING_ENTRY1(20), ROUTING_ENTRY1(21),
ROUTING_ENTRY1(22), ROUTING_ENTRY1(23),
};
前 16 个的后缀是 ENTRY2
,从上面的定义可以看出 ENTRY2 = IOAPIC + PIC
,因为 PIC 只有 16 个 pin,后 8 个则是独属于 APIC 的,因此是 ENTRY1
。
括号里的数字表示的是 irq,也称针脚(PIC 需要做余 8 操作),在 KVM 里这个概念叫 global system interrupt (GSI)。
kvm_setup_default_irq_routing()
最终的工作是根据 defeault_routing
初始化 kvm_irq_routing_table
,更具体来说是初始化 chip
字段和 map
字段。
原始定义 int chip[KVM_NR_IRQCHIPS][KVM_IRQCHIP_NUM_PINS]
,在 x86 下,irqchip 有两个 PIC 芯片和一个 I/O APIC 芯片,因此 KVM_NR_IRQCHIPS
的值是 3,KVM_IRQCHIP_NUM_PINS
的值是 24,因为 I/O APIC 芯片最大支持 24 pin。chip[i][j]
表示第 i 个芯片的第 j 个针脚被触发时对应的中断号(GSI),如果没有则设置为 -1。比如说 ROUTING_ENTRY2(0)
会被解析为 IOAPIC_ROUTING_ENTRY(0), PIC_ROUTING_ENTRY(0)
,那么 chip[0][0] = 0
以及 chip[2][0] = 0
。
map
是 kvm_kernel_irq_routing_entry
的哈希列表,长度是 24,值是 GSI,最终它应该的样子长得像是这样。

那么结合一下,已知 I/O APIC 的针脚 8 有中断产生,那么通过 chip[2][8] = 8
,然后通过 map[8]
找到 ioapic pin 8 对应的 kvm_kernel_irq_routing_entry
,最终通过各自的 set
callback 设置中断。
PIC 的 callback 是 kvm_set_pic_irq
,I/O APIC 的 callback 是 kvm_set_ioapic_irq
。
注入中断
如果你理解上面的结构的话,那么中断注入过程就非常简单了,主要的函数是 kvm_set_irq()
。
该函数提供了 4 个参数,struct kvm *kvm, int irq_source_id, u32 irq, int level, bool line_status
,其中:
irq
表示中断号,它负责选中map[irq]
,然后执行每个kvm_kernel_irq_routing_entry
的set()
callback。irq_source_id
表示 irq 来源,可选值有KVM_USERSPACE_IRQ_SOURCE_ID (0x0)
和KVM_IRQFD_RESAMPLE_IRQ_SOURCE_ID (0x1)
。KVM_USERSPACE_IRQ_SOURCE_ID
表示 irq 来源是用户空间,比如 QEMU 通过 KVM 的 ioctl 发送了一个中断,那么就会用0x0
。level
表示期望的状态,0
表示 inactive(清除中断标识位),1
表示 active。
先看 PIC 是如何工作的,调用栈如下所示
- 1: 负责设置 irq line state 的位图(bitmap),用来标注中断来源。
- 2: 根据
level
设置 IRR,用来告诉 vCPU 有中断注入。 - 3: 根据
output
和level
判断是否需要唤醒 vCPU,output
是 1 表示当前 master 芯片有中断信号,仅在是output == 0 && level == 1
的时候唤醒。 - 4: 唤醒 vCPU。
kvm_set_pic_irq()
-> kvm_pic_set_irq()
-> pic_lock()
-> 1: __kvm_irq_line_state()
-> 2: pic_set_irq1()
-> 3: pic_update_irq()
-> 4: pic_unlock()
回看 struct kvm_pic
的结构,有一个字段是 unsigned long irq_states[PIC_NUM_PINS]
,其中 PIC_NUM_PINS
的值是 16(因为两个 PIC 芯片),每个都是一个 64 为的位图,每个位表示一种 source id。所以 __kvm_irq_line_state()
的工作模式就是,如果 level
为 1 则表示设置 irq_states[irq] | source
,反之则清除该位。
中断触发方式分为水平触发(level-triggered)和边缘触发(edge-triggered),每个 source 都有一种触发方式,受到 elcr
(edge/level control register)的控制。elcr
也是个位图,比如 irq 对应的触发方式是 elcr & (1 >> irq)
,如果是 1 则是水平触发,反之是边缘触发。
水平触发是指电信号维持高位就表示有中断,反之无中断。这种方案比较适合共享 IRQ,比如设备 1 发了中断请求,此时信号维持高位,唤醒 guest 处理中断,这时候又来一个中断请求,发现水平信号是高,则会再发一次中断请求。
边缘触发则是需要一次电信号转换(transition)触发中断,一般是信号从低到高时触发中断。按照上面的情况,第一次会发请求,因为电平从低到高,如果没人复位电平,第二次则不会再发中断请求,因为电平持续是高位,没有产生转换。
pic_set_irq1()
的作用就好理解了,根据触发方式和 level
来决定是否设置 IRR。
pic_update_irq()
做了两件事情,如果是 slave 芯片产生的中断,触发 master 芯片 PIN 2 中断信号(因为 slave 芯片通过 master PIN 2 连接的),然后将 struct kvm_pic
的 wakeup_needed
设置为 true,这将会实际唤醒 vCPU。
PIC 信号在两种情况下会被真正 vCPU 接受,(1)APIC 硬件被禁用;(2)LVT0 没有被屏蔽且 delivery 模式是 ExtInt
。
LVT0 是本地向量表(local vector table)的第一个项目,一般是关联 local APIC timer 中断或者外部中断(这个例子里)。
在 pic_unlock()
中会遍历当前 VM 的全部 vCPUs,然后调用 __kvm_vcpu_kick()
来实际唤醒 vCPU。唤醒分为三个层级:
- 简单唤醒:当 vCPU 不在运行,处于 sleeping 状态,那么直接唤醒线程即可。
- vCPU 正在运行且是当前 CPU:让 guest 退出。
- vCPU 正在运行且不是当前 CPU:发送 IPI 中断,让其他 CPU 来处理使 guest 退出。
vGPU 随后会通过调用 vcpu_enter_guest()
方法重新返回 guest
- 1: 检查 pic 芯片的
output
是否为 1,如果是 1 则说明当前有外部中断 - 2: 根据 irq 设置 ISR 并清除 IRR。
- 3: 将 irq 写入到 VMCS 的
VM_ENTRY_INTR_INFO_FIELD
字段,向 guest 注入中断。
vcpu_enter_guest()
-> kvm_check_and_inject_events()
-> kvm_cpu_get_interrupt()
-> 1: kvm_cpu_get_extint()
-> kvm_pic_read_irq()
-> 2: pic_intack()
-> kvm_queue_interrupt()
-> kvm_x86_call(inject_irq)()
-> 3: vmx_inject_irq()
TODO: I/O APIC
总结每次中断产生时,如果 guest 正在运行则必须进行一次 VM Exit,导致 VM 产生额外开销。最后的最后再来张完整的大图:
