中断,这个概念相信大家已经不陌生了,我也没什么资格来介绍中断,就简单的说一下。我认为它从宏观上看可以分为软件部分和硬件部分。
软件部分:
软件部分在操作系统中实现,如Linux中断的x86,每一个中断对应一个中断门,中断门中包含中断处理函数(ISR或者别的)地址,优先级等等。CPU可以通过LIDT加载这个描述符表,跳转到指定的中断门。
硬件部分:
中断硬件部分就是产生中断脉冲,传给中断控制器,然后通知CPU,CPU在执行下调指令前会去查询中断情况,如果有中断信号,就执行中断。我们在这里模拟的就是硬件部分内容。
因此中断的模拟按照我的理解可以分为两个主要部分,一个是中断源的模拟,一个是给虚拟机的VCPU响应中断。
中断源模拟:
中断源模拟我也不是很清楚,方法很多,可以直接响应Linux的驱动,也可以别的,比如时钟中断可以设置一个定时器,定时器到了就触发中断,但是键盘,鼠标,硬盘呢?这个等待高手回?欢迎大家来讨论。
虚拟机响应虚拟中断:
KVM中断虚拟化主要依赖于VT-x技术,VT-x主要提供了两种中断事件机制,分别是中断退出和中断注入。
中断退出
是指虚拟机发生中断时,主动使得客户机发生VM-exit,这样能够在主机中实现对客户机中断的注入。
中断注入
它是指将中断写入VMCS对应的中断信息位,来实现中断的注入,当中断完成后通过读取中断的返回信息来分析中断是否正确。这个也是这里要详细将的地方。
首先中断注入有个一个标志性的函数 kvm_set_irq,这个是中断注入的最开始。
中断退出和注入是个关系紧密的过程,一先一后,我们放在一起来讲解,下面来分析一下KVM-KMOD-2.6.36相关实现代码,首先从kvm_set_irq开始。
函数位置:
函数参数:
int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, intlevel)
{
struct kvm_kernel_irq_routing_entry *e,irq_set[KVM_NR_IRQCHIPS];
struct kvm_irq_routing_table *irq_rt;
......
if (irq < irq_rt->nr_rt_entries)
r = irq_set[i].set(&irq_set[i], kvm, irq_source_id, level);
if (r < 0)
continue;
ret = r + ((ret < 0) ? 0 : ret);
}
return ret;
}
安装中断路由函数主要在setup_routing_entry中。
int setup_routing_entry(struct kvm_irq_routing_table *rt, structkvm_kernel_irq_routing_entry *e, const struct kvm_irq_routing_entry*ue)
{
struct kvm_kernel_irq_routing_entry *ei;
struct hlist_node *n
… …
switch (ue->type) {
case KVM_IRQ_ROUTING_IRQCHIP:
delta = 0;
switch (ue->u.irqchip.irqchip) {
case KVM_IRQCHIP_PIC_MASTER:
e->set = kvm_set_pic_irq;
max_pin = 16;
......
case KVM_IRQCHIP_IOAPIC:
max_pin = KVM_IOAPIC_NUM_PINS;
e->set = kvm_set_ioapic_irq;
... ...
}
case KVM_IRQ_ROUTING_MSI:
e->set = kvm_set_msi;
......
}
pent = &ioapic->redirtbl[idx];
if (!pent->fields.mask) {
injected = ioapic_deliver(ioapic, idx);
if (injected && pent->fields.trig_mode ==IOAPIC_LEVEL_TRIG)
pent->fields.remote_irr = 1;
}
return injected;
}
然后来看下LAPIC如何接收中断,主要是在函数__apic_accept_irq中,这里就是将中断写入当前触发VCPU的kvm_lapic结构体中的相应位置。
位置:x86/lapic.c
static int __apic_accept_irq(struct kvm_lapic *apic, intdelivery_mode,
int vector, int level, int trig_mode)
{
int result = 0;
struct kvm_vcpu *vcpu = apic->vcpu;
switch (delivery_mode) {
case APIC_DM_LOWEST:
vcpu->arch.apic_arb_prio++;
case APIC_DM_FIXED:
......
if (trig_mode) {
apic_set_vector(vector, apic->regs + APIC_TMR);
} else
apic_clear_vector(vector, apic->regs + APIC_TMR);
result = !apic_test_and_set_irr(vector, apic);
......
kvm_vcpu_kick(vcpu);
break;
… …
}
return result;
}
对于MSI来说
就是将irq消息解析,然后构造发送给VCPU的LAPIC,后面和IOAPIC的相同。
kvm_set_msi -> kvm_irq_delivery_to_apic -> kvm_apic_set_irq-> __apic_accept_irq
这样我们就大概讲解了三种中断触发方式的实现,具体的可以参见详细代码。这里要注意,CPU主循环和中断注入是两个并行的过程,所以CPU处于任何状态都能进行设置中断,设置中断以后,就会引起中断退出(最后一点是个人意见,可能不正确,应该是要写到vmcs位)。另外来自QEMU的中断注入也是调用这个循环,所以在QEMU中的中断和CPU循环也是并行执行。
当我们设置好虚拟中断控制器以后,接着在KVM_RUN退出以后,就开始遍历这些虚拟中断控制器,如果发现中断,就将中断写入中断信息位,实现如下:
inject_pending_event在进入guest之前被调用。
位置:86/x86.c
参数:发生退出的虚拟cpu结构体
注意这里将NMI,exception的注入过程都注释掉了,同理。
static void inject_pending_event(struct kvm_vcpu *vcpu)
{
… …
if (vcpu->arch.interrupt.pending) {
kvm_x86_ops->set_irq(vcpu);
return;
}
… …
if (vcpu->arch.nmi_pending) {
… …
} else if (kvm_cpu_has_interrupt(vcpu)) {
if (kvm_x86_ops->interrupt_allowed(vcpu)) {
kvm_queue_interrupt(vcpu, kvm_cpu_get_interrupt(vcpu), false);
kvm_x86_ops->set_irq(vcpu);
}
}
}
set_irq(),实现写入VMCS,代码如下:
位置:
static void vmx_inject_irq(struct kvm_vcpu *vcpu)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
uint32_t intr;
int irq = vcpu->arch.interrupt.nr;
… …
intr = irq | INTR_INFO_VALID_MASK;
if (vcpu->arch.interrupt.soft) {
intr |= INTR_TYPE_SOFT_INTR;
vmcs_write32(VM_ENTRY_INSTRUCTION_LEN,
vmx->vcpu.arch.event_exit_inst_len);
} else
intr |= INTR_TYPE_EXT_INTR;
vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr);
}
这样KVM就完成了虚拟中断的注入,从中断源触发到写入虚拟中断控制器,再到VMCS的过程。
最后我在回过头来讲讲是什么时候触发这个kvm_set_irq的。当然中断需要模拟的时候就调用。这里调用分为两种。
1.可以直接在KVM中调用这个函数,如虚拟I8254,我在其他文章中分析过i8254的中断模拟过程,这里有这种类型设备的中断源的模拟,顺被贴一张,一般中断源的逻辑流程图:
2.可以从QEMU中通过调用QEMU中的函数中断注入函数kvm_set_irq.
在QEMU中,如果有中断触发,会触发到相应中断控制器,中断方式也有8259(hw/i8259.c), IOAPIC(hw/ipf.cx86下没用这个,pic和apic相同处理,将pic扩展到24个),MSI(hw/msix.c),在这里中断控制器里面都会触发这个QEMU的kvm_set_irq函数。
至于这个kvm_set_irq函数位置在qemu-kvm.c,在这个函数中进而调用kvm_set_irq_level,最后通过一个KVM_IRQ_LINE的IOCTL调用KVM模块里面的kvm_set_irq函数。
int kvm_set_irq_level(kvm_context_t kvm, int irq, int level, int*status)
{
... ...
event.level = level;
event.irq = irq;
r = kvm_vm_ioctl(kvm_state, kvm->irqchip_inject_ioctl,&event);
... ...
}
这里我就不分析QEMU中的中断源了,毕竟主要将的是KVM,也希望大家针对QEMU中断的中断源进行讨论。发表自己的观点。