APIC包含有LAPIC(内核CORE里面都有,但不清楚是否是每个线程(超线程CPU)里面都有)
I/O APIC:
- 一般位于南桥中
- 响应外部设备的中断,并将中断发给LAPIC,然后由LAPIC发给对应的CPU
LAPIC:
- 内核CORE里面都有,但不清楚是否是每个线程(超线程CPU)里面都有。
- CPU核间通讯的唯一手段
LAPIC和I/O APIC的连接方式:
- 早期是专用的线连接(3-wire APIC Bus)
- 随着核心数量的增加,改为系统总线的连接方式,每增加一个核心,就是在总线上多挂一个LAPIC,不再受管教数量的约束
专用线连接演进为系统总线连接
当I/O APIC收到设备的中断请求的时候,会通过寄存器决定将中断发送给哪个LAPIC(CORE)。其中I/O APIC寄存器如图所示:
其中0x10~0x3F,有24个64位寄存器,对应着I/O APIC的24个中断引脚,这24个64位寄存器组成了中断重定向表(Redirection Table),重定向表使用管脚号作为索引,比如IR0号管脚对应第一个重定向表项。
- destination表示中断发送目标的CPU(可能是一个,也可能是一组),设置中断负载均衡(中断亲和性),设置的就是destination字段。
- interrupt vector:中断向量,用于在IDT中索引中断服务程序。
OS初始化->I/O APIC写入数据(设置重定向各个表项) OS运行时->I/O APIC写入数据(通过proc文件,设置中断亲和性Affinity),更新重定向表。
中断发生->I/0 APIC查询重定向表,并将中断发往OS预先设置的目标CPU->LAPIC接收到中断,并开始处理。
外设的中断过程
外设调用虚拟I/O APIC接口->虚拟IO APIC接收到中断请求->以中断请求号作为索引,查找中断重定向表->中断重定向表项des值决定将值发送到哪个CPU的LAPIC。
虚拟LAPIC收到请求后,首先设置IRR寄存器,然后唤醒正在睡眠的VCPU,或者触发正在运行的Guest退出到Host模式,下一次VM entry时评估虚拟LAPIC中的中断,执行注入过程。
IRR:Interrupt Request Register,中断请求寄存器;请求中断的外设在IRR对应的位中会置为1.当有多个中断请求,IRR会有多位被置为1,相当于维持了一个中断请求序列。
linux.git/arch/x86/kernel/apic/io_apic.c
static void __ioapic_write_entry(int apic, int pin, struct IO_APIC_route_entry e)
{
union entry_union eu;
eu.entry = e;
io_apic_write(apic, 0x11 + 2*pin, eu.w2);
io_apic_write(apic, 0x10 + 2*pin, eu.w1);
}
static inline void io_apic_write(unsigned int apic,
unsigned int reg, unsigned int value)
{
struct io_apic __iomem *io_apic = io_apic_base(apic);
writel(reg, &io_apic->index);
writel(value, &io_apic->data);
}
io_apic_write的三个参数
- apic:
- reg:是寄存器地址(相对比基址的偏移);0x10,是IO APIC重定向开始的偏移,对于第1个管脚,pin值为0,0x10,0x11恰好是第一个entry;对于第2个管脚,pin值为2,0x12,0x13对应第二个entry。
- value:要写入的内容,eu.w2是entry的低32位;eu.w1是entry的高32位。
对于中断重定向表中的entry(寄存器),只能通过间接访问。CPU地址空间映射了两个寄存器IOREGSEL(I/O Register Select)和IOWIN(I/O Window)。CPU访问entry需要通过他们,具体方式如下: 假设需要写入中断重定向表的第一个entry: CPU写入0x10到IOREGSEL,CPU写入32bit的value1到IOWIN(这就写入了entry1 的低32bit) CPU写入0x11到IOREGSEL,CPU写入32bit的value2到IOWIN(这就写入了entry1 的高32bit)
代码中的:
io_apic->index
是IOREGSELio_apic->data
是IOWIN
回到虚拟化中:虚拟I/O APIC接收到Guest内核I/O APIC填充中断重定向向量表的请求后,将中断向量、目标CPU、触发模式等中断信息记录到中断重定向表中。
file: arch/x86/kvm/ioapic.c
static int ioapic_mmio_write(struct kvm_vcpu *vcpu, struct kvm_io_device *this, gpa_t addr, int len, const void *val)
{
...
addr &= 0xff;
spin_lock(&ioapic->lock);
switch (addr) {
case IOAPIC_REG_SELECT:
ioapic->ioregsel = data & 0xFF; /* 8-bit register */
break;
case IOAPIC_REG_WINDOW:
ioapic_write_indirect(ioapic, data);
break;
default:
break;
}
spin_unlock(&ioapic->lock);
return 0;
}
static void ioapic_write_indirect(struct kvm_ioapic *ioapic, u32 val)
{
unsigned index;
bool mask_before, mask_after;
int old_remote_irr, old_delivery_status;
union kvm_ioapic_redirect_entry *e;
switch (ioapic->ioregsel) {
case IOAPIC_REG_VERSION:
/* Writes are ignored. */
break;
case IOAPIC_REG_APIC_ID:
ioapic->id = (val >> 24) & 0xf;
break;
case IOAPIC_REG_ARB_ID:
break;
default:
index = (ioapic->ioregsel - 0x10) >> 1;
if (index >= IOAPIC_NUM_PINS)
return;
index = array_index_nospec(index, IOAPIC_NUM_PINS);
e = &ioapic->redirtbl[index];
mask_before = e->fields.mask;
/* Preserve read-only fields */
old_remote_irr = e->fields.remote_irr;
old_delivery_status = e->fields.delivery_status;
if (ioapic->ioregsel & 1) {
e->bits &= 0xffffffff;
e->bits |= (u64) val << 32;
} else {
e->bits &= ~0xffffffffULL;
e->bits |= (u32) val;
}
e->fields.remote_irr = old_remote_irr;
e->fields.delivery_status = old_delivery_status;
...
}
}
中断过程
对于在用户空间的虚拟设备,通过ioctl接口向内核的KVM模块发送KVM__IRQ_LINE
请求:
void kvm__irq_line(struct kvm *kvm, int irq, int level)
{
...
if (ioctl(kvm->vm_fd, KVM_IRQ_LINE, &irq_level) < 0)
die_perror("KVM_IRQ_LINE failed");
...
}
虚拟APIC处理中断的代码:
linux\virt\kvm\kvm_main.c
01 static long kvm_vm_ioctl(…)
02 {
03 …
04 case KVM_IRQ_LINE: {
05 …
06 if (irqchip_in_kernel(kvm)) {
07 …
08 kvm_ioapic_set_irq(kvm->vioapic,
09 irq_event.irq, irq_event.level);
10 …
11 }
12 …
13 }
LAPIC向Guest注入中断的过程于PIC本质上完全相同:8259A是收到外设发起的中断后,先在IRR寄存器中记录下来,然后再设置一个变量output表示由pending的中断,在下次VM-entry时,KVM会发起中断评估等一系列过程,最终将中断信息写入到VMCS的VM-entry interruption-information中。
当目标CPU处于Guest模式,或者vcpu正处于睡眠状态,为减少延迟,需要虚拟LAPIC kick(踢)一下目标vCPU,要么将其从Guest模式提出到Host模式,要么唤醒挂起的vCPU线程kvm_vcpu_kick
。
static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode,
int vector, int level, int trig_mode,
struct dest_map *dest_map)
{
int result = 0;
struct kvm_vcpu *vcpu = apic->vcpu;
trace_kvm_apic_accept_irq(vcpu->vcpu_id, delivery_mode,
trig_mode, vector);
switch (delivery_mode) {
case APIC_DM_LOWEST:
vcpu->arch.apic_arb_prio++;
/* fall through */
case APIC_DM_FIXED:
if (unlikely(trig_mode && !level))
break;
/* FIXME add logic for vcpu on reset */
if (unlikely(!apic_enabled(apic)))
break;
result = 1;
if (dest_map) {
__set_bit(vcpu->vcpu_id, dest_map->map);
dest_map->vectors[vcpu->vcpu_id] = vector;
}
if (apic_test_vector(vector, apic->regs + APIC_TMR) != !!trig_mode) {
if (trig_mode)
kvm_lapic_set_vector(vector,apic->regs + APIC_TMR);
else
kvm_lapic_clear_vector(vector,apic->regs + APIC_TMR);
}
if (kvm_x86_ops->deliver_posted_interrupt(vcpu, vector)) {
kvm_lapic_set_irr(vector, apic);
kvm_make_request(KVM_REQ_EVENT, vcpu);
kvm_vcpu_kick(vcpu);
}
break;
}
...
return result;
}
核间中断过程
APIC page(4kb),APIC所有寄存器都存储在这个页面上。linux内核将APIC page映射到内核空间中,通过MMIO访问,当内核访问这些寄存器的时候,将触发Guest退出到Host的KVM模块中的vLAPIC。当Guest发送核间中断时,虚拟LAPIC确定目的CPU,向目的CPU所在的LAPIC发送核间中断,最后由目标vLAPIC完成向Guest的中断注入的过程。
vLAPIC处理函数为apic__mmio_write
linux.git/drivers/kvm/lapic.c
static void apic_mmio_write(…)
{
…
case APIC_ICR:
/* No delay here, so we always clear the pending bit */
apic_set_reg(apic, APIC_ICR, val & ~(1 << 12));
apic_send_ipi(apic);
break;
case APIC_ICR2:
apic_set_reg(apic, APIC_ICR2, val & 0xff000000);
break;
…
}
static void apic_send_ipi(struct kvm_lapic *apic)
{
u32 icr_low = apic_get_reg(apic, APIC_ICR);
u32 icr_high = apic_get_reg(apic, APIC_ICR2);
unsigned int dest = GET_APIC_DEST_FIELD(icr_high);
…
for (i = 0; i < KVM_MAX_VCPUS; i++) {
vcpu = apic->vcpu->kvm->vcpus[i];
…
if (vcpu->apic && apic_match_dest(vcpu, apic, short_hand, dest, dest_mode)) {
…
__apic_accept_irq(vcpu->apic, delivery_mode,
vector, level, trig_mode);
}
}
…
}
当Guest写的是LAPIC的ICR寄存器(64位),表明这个CPU向另外一个CPU(56-63存储目标CPU)发送核间中断,APIC_ICR\APIC_ICR2
分别指ICR低32位/高32位寄存器,写高32位是指定目标vgpu关联的vLAPIC,写低32位时候会触发vLAPIC向vCPU发送核间中断。
当Guest写ICR低32位时,触发vLAPIC向目标CPU发送核间中断,apic_send_ipi
。函数apic_send_ipi
先从寄存器中读取目标CPU,然后遍历虚拟机的所有vCPU,如果有vCPU匹配成功,则向其发送IPI中断,目标vLAPIC调用函数__apic_accept_irq
接收中断。
linux.git/drivers/kvm/lapic.c
static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode, int vector, int level, int trig_mode)
{
…
switch (delivery_mode) {
case APIC_DM_FIXED:
case APIC_DM_LOWEST:
…
if (apic_test_and_set_irr(vector, apic) && trig_mode) {
…
kvm_vcpu_kick(apic->vcpu);
…
}
__apic_accept_irq
与kvm_apic_set_irq
实现几乎完全相同,首先在IRR寄存器记录中断信息,为减少中断延迟,需要kick一下vcpu,唤醒CPU后下次VM entry时,KVM发起中断评估等一系列过程,最终将中断信息写入VMCS的字段VM-entry interruption-information。
IRQ routing
系统中存在虚拟8259A和vAPIC,当中断请求发生时候,系统是通过哪个进行中断注入呢?因为kvm无法检测guest是使用PIC或者APIC,因此guest会忽略没用的那个。
linux.git/drivers/kvm/kvm_main.c
static long kvm_vm_ioctl(…)
{
…
case KVM_IRQ_LINE: {
…
if (irq_event.irq < 16)
kvm_pic_set_irq(pic_irqchip(kvm), …);
kvm_ioapic_set_irq(kvm->vioapic, …);
…
}
当管脚号小于16时,会同时写入kvm_pic_set_irq
和kvm_ioapic_set_irq
,这段代码已经重构了,有IRQ routing方案。
IRQ routing 包含一个表,表格的每一项:
linux.git/include/linux/kvm_host.h
struct kvm_kernel_irq_routing_entry {
u32 gsi;
void (*set)(struct kvm_kernel_irq_routing_entry *e,
struct kvm *kvm, int level);
union {
struct {
unsigned irqchip;
unsigned pin;
} irqchip;
};
struct list_head link;
};
gsi:管脚号如IR0,IR1等 set:函数指针
当一个外设请求到来时,KVM会遍历这个表格,匹配gsi,匹配成功则调用set指向的函数,负责注入中断。
- 对于使用PCI的外设:set指向虚拟PIC对外提供的发射中断接口
- 对于使用APIC的外设:set指向vAPIC对外提供的发送中断的接口
- 对于支持并启用了MSI的外设:set指向MSI实现的发送中断的接口
当KVM收到设备发来的中断时,不再区分
PIC\APIC\MSI
,统一调用kvm_set_irq
,遍历IRQ routing表中的每一个表项,如此就实现了代码统一:
linux.git/arch/x86/kvm/x86.c
long kvm_arch_vm_ioctl(…)
{
…
case KVM_IRQ_LINE: {
…
kvm_set_irq(kvm, KVM_USERSPACE_IRQ_SOURCE_ID,
irq_event.irq, irq_event.level);
…
}
linux.git/virt/kvm/irq_comm.c
void kvm_set_irq(struct kvm *kvm, int irq_source_id, …)
{
…
list_for_each_entry(e, &kvm->irq_routing, link)
if (e->gsi == irq)
e->set(e, kvm, !!(*irq_state));
}
当创建内核中的虚拟中断芯片的时候,虚拟中断芯片会创建一个默认的表格。KVM为用户空调提供API以设置这个表格,用户空间的虚拟设备可以按需增加这个表格的表项。
新的数据结构:
- 小于16的管脚会创建两个entry(set),一个指向8259A提供的中断注入接口,一个指向APIC提供的中断注入接口。
KVM: Userspace controlled irq routing
linux.git/virt/kvm/irq_comm.c
static const struct kvm_irq_routing_entry default_routing[] = {
ROUTING_ENTRY2(0), ROUTING_ENTRY2(1),
…
ROUTING_ENTRY1(16), ROUTING_ENTRY1(17),
…
};
linux.git/virt/kvm/irq_comm.c
#define ROUTING_ENTRY1(irq) I/O APIC_ROUTING_ENTRY(irq)
#define ROUTING_ENTRY2(irq) \
I/O APIC_ROUTING_ENTRY(irq), PIC_ROUTING_ENTRY(irq)
IOAPIC_ROUTING_ENTRY
和PIC_ROUTING_ENTRY
创建结构体kvm_irq_routing
中的entry
,管脚小于16时候,创建了PIC和APIC的表项。管脚大于16时候,只创建APIC的表项。setup_routing_entry
根据PIC/APIC分别指向kvm_pic_set_irq
和kvm_ioapic_set_irq
。
linux.git/virt/kvm/irq_comm.c
int setup_routing_entry(…)
{
…
switch (ue->u.irqchip.irqchip) {
case KVM_IRQCHIP_PIC_MASTER:
e->set = kvm_set_pic_irq;
…
case KVM_IRQCHIP_IOAPIC:
e->set = kvm_set_ioapic_irq;
…
}
…
}
参考文献
- 《深度探索Linux系统虚拟化:原理与实现》 - 王柏生 & 谢广军
- 计算机中断体系一:历史和原理
- 一文讲透计算机的“中断”
- ACPI.sys,从Windows到Bios的桥梁(2):Windows应用程序响应GPIO(SCI)设备中断 Bios篇
评论区