KVM 中断芯片是如何工作的

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 拥有三个芯片 masterslaveelcr,这个函数还将每个芯片赋予了 readwrite 方法。
  • 2: 初始化 struct kvm_ioapic,给 dev 赋予 readwrite 方法。
  • 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_entrykvm_irq_routing_irqchipkvm_irq_routing_msi,与 KVM 相关的都在下方,包括 kvm_irq_routing_enablekvm_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

mapkvm_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_entryset() 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: 根据 outputlevel 判断是否需要唤醒 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_picwakeup_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 产生额外开销。最后的最后再来张完整的大图:

References

  1. https://terenceli.github.io/%E6%8A%80%E6%9C%AF/2018/08/27/kvm-interrupt-emulation
All rights reserved
Except where otherwise noted, content on this page is copyrighted.