中断注入:是指 虚拟中断控制器采集到的中断请求,将按照VMM排定的优先级,被逐一注入到对应的虚拟CPU中。
VM exit:最常用的办法就是往虚拟CPU对应的物理CPU发送一个IPI核间中断。
为什么中断注入一定要让CPU退出,这是因为intel硬件设计使然。只有在VM-Entry这个时间硬件才检查VMCS域中的VM-entry interruption-information的字段,所以要让虚机先退出,然后复现一个VM-Entry的时机。
为了让Guest对其(虚拟)APIC的访问不必引起VM Exit,引入了Virtual-APIC Page的概念。它相当于是一个Shadow APIC,Guest对其APIC的部分甚至全部访问都可以被硬件翻译成对Virtual-APIC Page的访问,这样就不必频繁引起VM Exit了。Virtual-APIC Page受VMCS管理,这是一个可以由虚拟机直接访问的内存页面。
VT-d是X86架构里面可以对中断进行重新映射或者直接投递中断的一个硬件,它根据中断映射表将外部中断投递到目标Guest。VMM拥有一个Interrupt Remap Table Address Register指向中断映射表的入口,每passthrough一个设备,VMM就在这个表中分配一个entry,打上相应的属性,由于外设已经pasthrough给Guest了,Guest里驱动写外设的PCI config space,此时VMM要拦截对PCI config space的写,配置外设MSI产生的中断是remappable格式。Guest里的driver给这个passthroug的device分配了一个vector X,然后在IDT中添加了vector X的处理函数。由于device的interrupt是external interrupt,不能直接给了Guest,host也给device分配一个vector Y,host接收到了interrupt Y转换成interrupt X,再投递给Guest,Guest用自己的函数处理,投递时用post interrupt就不会导致Guest exit出来。
中断评估:待处理的中断有没有被屏蔽?待处理的中断优先级是否比CPU正在处理的中断优先级高?等等的过程就是中断评估。
正篇开始
virtual-APIC page(虚拟中断寄存器页面)
物理LAPIC有一个APIC-access page,物理采用mmap的方式访问这些寄存器。一开的设计是vCPU准备访问这些寄存器的时候会触发从guest VM-exit到Host中,由KVM负责模拟,将Guest写给LAPIC的值写入vLAPIC,或者从vLAPIC的APIC page读入值给Guest。这其中充斥了大量的VM exit,为了优化整个过程,intel设计了virtual APIC page,CPU在Guest模式下将使用这个virtual-APIC page来维护寄存器的状态;
- 读取vAPIC page:不需要VM-exit到host
- 写入vAPIC page:需要VM-exit到host
Guest模式下CPU需要从VMCS中找vAPIC page,VMX在VMCS中设计了VIRTUAL_APIC_PAGE_ADDR
字段,在CPU切入Guest模式前,KVM需要将vAPIC page的地址记录到VMCS这个字段中。
linux.git/arch/x86/kvm/vmx.c
static int vmx_vcpu_reset(struct kvm_vcpu *vcpu)
{
…
vmcs_write64(VIRTUAL_APIC_PAGE_ADDR,
__pa(vmx->vcpu.arch.apic->regs));
…
}
这个功能有一个VMCS开关配置,叫Secondary Processor-Based VM-Execution Controls
,打开这个配置的代码如下:
linux.git/arch/x86/kvm/vmx.c
static __init int setup_vmcs_config(struct vmcs_config *vmcs_conf)
{
…
opt2 = SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES |
…
SECONDARY_EXEC_APIC_REGISTER_VIRT;
…
}
linux.git/arch/x86/include/asm/vmx.h
#define SECONDARY_EXEC_APIC_REGISTER_VIRT 0x00000100
该功能打开后,只有写vAPIC page会触发VM-exit,这个处理函数由handle_apic_write
负责。
linux.git/arch/x86/kvm/vmx.c
static int (*const kvm_vmx_exit_handlers[])
(struct kvm_vcpu *vcpu) = {
…
[EXIT_REASON_APIC_ACCESS] = handle_apic_access,
[EXIT_REASON_APIC_WRITE] = handle_apic_write,
…
};
未开启该功能,访问APIC page都会触发VM-exit,读和写都由handle_apic_access
进行处理。
写寄存器可能会伴随其他动作,这里这里的APIC_ICR会发送核间中断的操作,这也是为啥intel的硬件实现中写寄存器还是要触发VM-exit。
linux.git/arch/x86/kvm/lapic.c
int kvm_lapic_reg_write(struct kvm_lapic *apic, u32 reg, u32 val)
{
...
case APIC_ICR:
/* No delay here, so we always clear the pending bit */
val &= ~(1 << 12);
apic_send_ipi(apic, val, kvm_lapic_get_reg(apic, APIC_ICR2));
kvm_lapic_set_reg(apic, APIC_ICR, val);
break;
...
}
Guest模式下中断评估
没有intel硬件层面支持的话,中断评估和中断注入都应该在VM entry时。有两种情况无法处理中断:
- VM entry那一刻Guest是关闭中断的
- Guest正在执行不能被中断的指令 这种情况会导致中断等很久,为了不让中断延时过大,当CPU打开中断且未执行无法中断的指令时,CPU应该立马VM exit,以便注入中断。
VMX提供了一种特性:Interrupt-window exiting,RFLAGS寄存器中的IF置位了,就和CPU检查中断类似,会在Guest能够处理中断,且无不可打断指令执行,会触发VM exit从而进行中断注入。
Primary Processor-Based VM-Execution Controlscommit 85f455f7ddbed403b34b4d54b1eaf0e14126a126 (历史代码)
KVM: Add support for in-kernel PIC emulation
static void vmx_intr_assist(struct kvm_vcpu *vcpu)
{
…
interrupt_window_open =
((vmcs_readl(GUEST_RFLAGS) & X86_EFLAGS_IF) &&
(vmcs_read32(GUEST_INTERRUPTIBILITY_INFO) & 3) == 0);
if (interrupt_window_open)
vmx_inject_irq(vcpu, kvm_cpu_get_interrupt(vcpu));
else
enable_irq_window(vcpu);
}
static void enable_irq_window(struct kvm_vcpu *vcpu)
{
u32 cpu_based_vm_exec_control;
cpu_based_vm_exec_control = vmcs_read32(CPU_BASED_VM_EXEC_CONTROL);
cpu_based_vm_exec_control |= CPU_BASED_VIRTUAL_INTR_PENDING;
vmcs_write32(CPU_BASED_VM_EXEC_CONTROL,
cpu_based_vm_exec_control);
}
当guest模式下支持中断评估后,Guest模式的CPU就不仅仅在VM entry时才能进行中断评估了,其重大的不同在于运行于Guest模式的CPU也能评估中断,一旦识别出中断,在Guest模式即可自动完成中断注入,无须再触发VM exit。
Guest模式的CPU评估中断借助VMCS中的字段guest interrupt status。字段guest interrupt status长度为16位,存储在VMCS中的Guest Non-Register State区域。低8位称作Requesting virtual interrupt(RVI),这个字段用来保存中断评估后待处理的中断向量;高8位称作Servicing virtual interrupt(SVI),这个字段表示Guest正在处理的中断。
当Guest打开中断或者执行完不能被中断的指令后,CPU会检查VMCS中的字段guest interrupt status是否有中断需要处理,如果有中断pending在这,则调用Guest的内核中断handler处理中断. 中断注入则有两种方式了,
- VM-entry的时候处理。
- 记录到guest interrupt status中,进入guest模式自己处理
x86, apicv: add virtual interrupt delivery support
linux.git/arch/x86/kvm/x86.c
static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
{
…
if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win) {
…
if (kvm_x86_ops->hwapic_irr_update)
kvm_x86_ops->hwapic_irr_update(vcpu,
kvm_lapic_find_highest_irr(vcpu));
…
}
}
…
}
linux.git/arch/x86/kvm/vmx.c
static void vmx_hwapic_irr_update(struct kvm_vcpu *vcpu, …)
{
…
vmx_set_rvi(max_irr);
}
static void vmx_set_rvi(int vector)
{
…
if ((u8)vector != old) {
status &= ~0xff;
status |= (u8)vector;
vmcs_write16(GUEST_INTR_STATUS, status);
}
}
Guest模式的CPU中断评估支持默认关闭(需手动开启),但是KVM默认开启了这个支持。
linux.git/arch/x86/kvm/vmx.c
static __init int setup_vmcs_config(struct vmcs_config *vmcs_conf)
{
…
opt2 = SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES |
…
SECONDARY_EXEC_VIRTUAL_INTR_DELIVERY; //
…
}
posted-interrupt processing
当虚拟中断芯片需要注入中断时,其将中断的信息更新到posted-interrupt descriptor中。然后虚拟中断芯片向CPU发送一个通知posted-interrupt notification(常规IPI,但核间中断向量专有,不会触发VM exit),处于Guest模式的CPU收到这个中断后,将在Guest模式直接响应中断。
当来自虚拟设备的中断到达虚拟LAPIC后,虚拟LAPIC将更新目标Guest的posted-interrupt descriptor,然后通知目的CPU评估并处理中断,目的CPU无须进行一次VM exit和VM entry。
处理模拟设备的中断过程外部中断在一个处于Guest模式的CPU,但是目标Guest是运行于另外一个CPU上的情况。来自外设的中断落在CPU0上,而此时CPU0处于Guest模式,将导致CPU0发生VM exit,陷入KVM。KVM中的虚拟LAPIC将更新目标Guest的posted-interrupt descriptor,然后通知目的CPU1评估并处理中断,目的CPU1无须进行一次VM exit和VM entry。
处理外设中断过程设备透传结合posted-interrupt processing机制后,中断重映射硬件单元负责更新目标Guest的posted-interrupt descriptor,将不再导致任何VM exit,外部透传设备的中断可直达目标CPU。
透传设备中断处理过程posted-interrupt descriptor的长度为64Bytes,其格式
posted-interrupt descriptor格式0~255位用来表示中断向量,256位用来指示是否有中断。其地址记录在VMCS中,对应的字段是posted-interrupt descriptor address。 CPU判断哪个中断是posted-interrupt notification?其中断向量记录在VMCS中。
linux.git/arch/x86/kvm/vmx.c
static int vmx_vcpu_setup(struct vcpu_vmx *vmx)
{
…
if (vmx_vm_has_apicv(vmx->vcpu.kvm)) {
…
vmcs_write64(POSTED_INTR_NV, POSTED_INTR_VECTOR);
vmcs_write64(POSTED_INTR_DESC_ADDR, __pa((&vmx->pi_desc)));
}
…
}
函数pi_test_and_set_pir和pi_test_and_set_on分别设置posted-interrupt descriptor中的pir和notification,设置完posted-interrupt descriptor后,如果此时 CPU处于Guest模式,那么发送专为posted-interrupt processing定义的核间 中断POSTED_INTR_VECTOR;如果CPU不是处于Guest模式,那么就 发送一个重新调度的核间中断,促使目标CPU尽快得到调度,在VM entry后马上处理posted-interrupt descriptor中的中断。
linux.git/arch/x86/kvm/lapic.c
static int __apic_accept_irq(…)
{
…
kvm_x86_ops->deliver_posted_interrupt(vcpu, vector);
…
}
linux.git/arch/x86/kvm/vmx.c
static void vmx_deliver_posted_interrupt(…)
{
…
if (pi_test_and_set_pir(vector, &vmx->pi_desc))
…
r = pi_test_and_set_on(&vmx->pi_desc);
…
if (!r && (vcpu->mode == IN_GUEST_MODE))
apic->send_IPI_mask(get_cpu_mask(vcpu->cpu),
POSTED_INTR_VECTOR);
else
kvm_vcpu_kick(vcpu);
}
参考文献
- 《深度探索Linux系统虚拟化:原理与实现》 - 王柏生 & 谢广军
- X86架构中断虚拟化技术解析
评论区