KVM虚拟机代码揭秘—中断虚拟化(基于kenel 3.10.0)

 

中断从宏观上看可以分为软件部分和硬件部分。

软件部分:

软件部分在操作系统中实现,如Linux中断的x86,每一个中断对应一个中断门,中断门中包含中断处理函数(ISR或者别的)地址,优先级等等。CPU可以通过LIDT加载这个描述符表,跳转到指定的中断门。

硬件部分:

中断硬件部分就是产生中断脉冲,传给中断控制器,然后通知CPUCPU在执行下条指令前会去查询中断情况,如果有中断信号,就执行中断。我们在这里模拟的就是硬件部分内容。

因此中断的模拟按照我的理解可以分为两个主要部分,一个是中断源的模拟,一个是给虚拟机的VCPU响应中断。

中断源模拟:

中断源模拟我也不是很清楚,方法很多,可以直接响应Linux的驱动,也可以别的,比如时钟中断可以设置一个定时器,定时器到了就触发中断,但是键盘,鼠标,硬盘呢?这个等待高手回?欢迎大家来讨论。

虚拟机响应虚拟中断:

KVM中断虚拟化主要依赖于VT-x技术,VT-x主要提供了两种中断事件机制,分别是中断退出和中断注入

中断退出

是指虚拟机发生中断时,主动使得客户机发生VM-exit,这样能够在主机中实现对客户机中断的注入。

中断注入

它是指将中断写入VMCS对应的中断信息位,来实现中断的注入,当中断完成后通过读取中断的返回信息来分析中断是否正确。这个也是这里要详细讲的地方。

首先中断注入有个一个标志性的函数 kvm_set_irq(),这个是中断注入的最开始。

中断退出和注入是个关系紧密的过程,一先一后,我们放在一起来讲解,下面来分析一下相关实现代码,首先从 kvm_set_irq()开始。

函数位置: virt/kvm/irqchip.c

函数参数:struct kvm *kvm,发生中断的客户机的结构体指针,我们知道在KVM模块中有个vm_list,链接了所有注册的虚拟机,当多次使用QEMU命令以后会产生多个虚拟机,不同虚拟机中公用一个KVM模块,通过这个kvm结构体来辨识是哪个虚拟机,当然每个虚拟机里面可以对应多个VCPU,别把这个概念弄混淆了;irq_source_id中断资源ID,对于KVM设备我们都会申请一个中断资源ID,注册KVM IO设备时申请的;irq中断请求号,这个是转化GSI之前的,比如时钟是0号,这里就是0,而不是32level表示中断的高低电平;line_status暂不清楚含义。

 
   

int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level,
bool line_status)
{
struct kvm_kernel_irq_routing_entry *e, irq_set[KVM_NR_IRQCHIPS];
int ret = -1, i = 0;
struct kvm_irq_routing_table *irq_rt;
trace_kvm_set_irq(irq, level, irq_source_id);
/* Not possible to detect if the guest uses the PIC or the
* IOAPIC. So set the bit in both. The guest will ignore
* writes to the unused one.
*/
rcu_read_lock();
irq_rt = rcu_dereference(kvm->irq_routing);
if (irq < irq_rt->nr_rt_entries) /*中断请求号范围检查*/
hlist_for_each_entry(e, &irq_rt->map[irq], link)/*提取中断路由表中对应的中断路由实体,map[irq]是一个对应中断的路由实体表头结点,这里遍历它能够得到所有对应的路由实体。*/
irq_set[i++] = *e;
rcu_read_unlock();
while(i--) {
int r;
r = irq_set[i].set(&irq_set[i], kvm, irq_source_id, level,
line_status);/*触发对应路由实体的钩子函数,setup_routing_entry中注册*/
if (r < 0)
continue;
ret = r + ((ret < 0) ? 0 : ret);
}

return ret;
}



安装中断路由主要在setup_routing_entry中,执行路径为setup_routing_entry->kvm_set_routing_entry,下面先讲解kvm_set_routing_entry

 

 
   

int kvm_set_routing_entry(struct kvm_irq_routing_table *rt,
struct kvm_kernel_irq_routing_entry *e,
const struct kvm_irq_routing_entry *ue)
{
int r = -EINVAL;
int delta;
unsigned max_pin;

switch (ue->type) {
case KVM_IRQ_ROUTING_IRQCHIP: /*普通的IRQCHIP,分为PIC和IOAPIC,路由函数分别是kvm_set_pic_irq和kvm_set_ioapic_irq,PIC分为主从两块芯片。*/
delta = 0;
switch (ue->u.irqchip.irqchip) {
case KVM_IRQCHIP_PIC_MASTER:
e->set = kvm_set_pic_irq;
max_pin = PIC_NUM_PINS;
break;
case KVM_IRQCHIP_PIC_SLAVE:
e->set = kvm_set_pic_irq;
max_pin = PIC_NUM_PINS;
delta = 8;
break;
case KVM_IRQCHIP_IOAPIC:
max_pin = KVM_IOAPIC_NUM_PINS;
e->set = kvm_set_ioapic_irq;
break;
default:
goto out;
}
e->irqchip.irqchip = ue->u.irqchip.irqchip;
e->irqchip.pin = ue->u.irqchip.pin + delta;
if (e->irqchip.pin >= max_pin)
goto out;
rt->chip[ue->u.irqchip.irqchip][e->irqchip.pin] = ue->gsi;
break;
case KVM_IRQ_ROUTING_MSI: /*MSI中断消息系统的中断触发函数kvm_set_msi*/
e->set = kvm_set_msi;
e->msi.address_lo = ue->u.msi.address_lo;
e->msi.address_hi = ue->u.msi.address_hi;
e->msi.data = ue->u.msi.data;
break;
default:
goto out;
}

r = 0;
out:
return r;
}



然后在setup_routing_entry添加路由实体到中断。

 

 
   

static int setup_routing_entry(struct kvm_irq_routing_table *rt,
struct kvm_kernel_irq_routing_entry *e,
const struct kvm_irq_routing_entry *ue)
{
int r = -EINVAL;
struct kvm_kernel_irq_routing_entry *ei;
/*
* Do not allow GSI to be mapped to the same irqchip more than once.
* Allow only one to one mapping between GSI and MSI.
*/
hlist_for_each_entry(ei, &rt->map[ue->gsi], link) // 只能映射1次
if (ei->type == KVM_IRQ_ROUTING_MSI ||
ue->type == KVM_IRQ_ROUTING_MSI ||
ue->u.irqchip.irqchip == ei->irqchip.irqchip)
return r;
e->gsi = ue->gsi;
e->type = ue->type;
r = kvm_set_routing_entry(rt, e, ue);
if (r)
goto out;
hlist_add_head(&e->link, &rt->map[e->gsi]); /*中断路由实体添加到中断*/
r = 0;
out:
return r;
}



这样就会通过kvm_set_irq()调转到对应的中断注入函数。这个触发函数从上面来看可以分为三类kvm_set_pic_irqkvm_set_ioapic_irqkvm_set_msi,他们分别对应不同的中断方式。

对于PIC来说

它主要是设置kvm里面的虚拟中断控制器结构体struct kvm_pic完成虚拟中断控制器的设置。如果是边缘触发,需要触发电平先01或者先10,完成一个正常的中断模拟。

对于IOAPIC来说

相对要复杂一点,它的大概调用过程如下,有兴趣的可以去跟一下。

kvm_set_ioapic_irq -> kvm_ioapic_set_irq -> ioapic_set_irq-> ioapic_service -> kvm_irq_delivery_to_apic-> kvm_apic_set_irq-> __apic_accept_irq

 整个流程是先检查IOAPIC状态,如果符合注入条件,则组建中断结构体,发送到指定VCPULAPIC,设置LAPIC的寄存器,完成虚拟中断控制器设置。

这里只介绍两个关键的容易出问题的函数,第一个是中断消息的投递,如下:

位置:virt/kvm/ioapic.c

 

 
   

static int ioapic_service(struct kvm_ioapic *ioapic, int irq, bool line_status)
{
union kvm_ioapic_redirect_entry *entry = &ioapic->redirtbl[irq]; /*取得对应中断的重定向表*/
struct kvm_lapic_irq irqe;
int ret;

if (entry->fields.mask) /*检查中断是否被mask了,如果不允许中断则不触发*/
return -1;

ioapic_debug("dest=%x dest_mode=%x delivery_mode=%x "
"vector=%x trig_mode=%x\n",
entry->fields.dest_id, entry->fields.dest_mode,
entry->fields.delivery_mode, entry->fields.vector,
entry->fields.trig_mode);

irqe.dest_id = entry->fields.dest_id; /*构造APIC消息,准备发送到LAPIC*/
irqe.vector = entry->fields.vector;
irqe.dest_mode = entry->fields.dest_mode;
irqe.trig_mode = entry->fields.trig_mode;
irqe.delivery_mode = entry->fields.delivery_mode << 8;
irqe.level = 1;
irqe.shorthand = 0;

if (irqe.trig_mode == IOAPIC_EDGE_TRIG)
ioapic->irr &= ~(1 << irq);

if (irq == RTC_GSI && line_status) {
/*
* pending_eoi cannot ever become negative (see
* rtc_status_pending_eoi_check_valid) and the caller
* ensures that it is only called if it is >= zero, namely
* if rtc_irq_check_coalesced returns false).
*/
BUG_ON(ioapic->rtc_status.pending_eoi != 0);
ret = kvm_irq_delivery_to_apic(ioapic->kvm, NULL, &irqe,
ioapic->rtc_status.dest_map);
ioapic->rtc_status.pending_eoi = (ret < 0 ? 0 : ret);
} else
ret = kvm_irq_delivery_to_apic(ioapic->kvm, NULL, &irqe, NULL);

if (ret && irqe.trig_mode == IOAPIC_LEVEL_TRIG) /*如果投递成功并且是电瓶触发,设置目的中断请求寄存器*/
entry->fields.remote_irr = 1;

return ret;
}



然后来看下LAPIC如何接收中断,主要是在函数__apic_accept_irq中,这里就是将中断写入当前触发VCPUkvm_lapic结构体中的相应位置。

位置:arch/x86/kvm/lapic.c

 

 
   

static int __apic_accept_irq(struct kvm_lapic *apic, int delivery_mode,
int vector, int level, int trig_mode,
unsigned long *dest_map)
{
int result = 0;
struct kvm_vcpu *vcpu = apic->vcpu;
/*APIC投递模式,表明是完成什么功能*/
switch (delivery_mode) {
case APIC_DM_LOWEST:
vcpu->arch.apic_arb_prio++;
case APIC_DM_FIXED:
/* FIXME add logic for vcpu on reset */
if (unlikely(!apic_enabled(apic)))
break;

if (dest_map)
__set_bit(vcpu->vcpu_id, dest_map);

if (kvm_x86_ops->deliver_posted_interrupt) {
result = 1;
kvm_x86_ops->deliver_posted_interrupt(vcpu, vector);
} else {
result = !apic_test_and_set_irr(vector, apic);

if (!result) {
if (trig_mode)
apic_debug("level trig mode repeatedly "
"for vector %d", vector);
goto out;
}

kvm_make_request(KVM_REQ_EVENT, vcpu);
kvm_vcpu_kick(vcpu);
}
out:
trace_kvm_apic_accept_irq(vcpu->vcpu_id, delivery_mode,
trig_mode, vector, !result);
break;

case APIC_DM_REMRD:
result = 1;
vcpu->arch.pv.pv_unhalted = 1;
kvm_make_request(KVM_REQ_EVENT, vcpu);
kvm_vcpu_kick(vcpu);
break;

case APIC_DM_SMI:
apic_debug("Ignoring guest SMI\n");
break;

case APIC_DM_NMI:
result = 1;
kvm_inject_nmi(vcpu);
kvm_vcpu_kick(vcpu);
break;

case APIC_DM_INIT:
if (!trig_mode || level) {
result = 1;
/* assumes that there are only KVM_APIC_INIT/SIPI */
apic->pending_events = (1UL << KVM_APIC_INIT);
/* make sure pending_events is visible before sending
* the request */
smp_wmb();
kvm_make_request(KVM_REQ_EVENT, vcpu);
kvm_vcpu_kick(vcpu);
} else {
apic_debug("Ignoring de-assert INIT to vcpu %d\n",
vcpu->vcpu_id);
}
break;

case APIC_DM_STARTUP:
apic_debug("SIPI to vcpu %d vector 0x%02x\n",
vcpu->vcpu_id, vector);
result = 1;
apic->sipi_vector = vector;
/* make sure sipi_vector is visible for the receiver */
smp_wmb();
set_bit(KVM_APIC_SIPI, &apic->pending_events);
kvm_make_request(KVM_REQ_EVENT, vcpu);
kvm_vcpu_kick(vcpu);
break;

case APIC_DM_EXTINT:
/*
* Should only be called by kvm_apic_local_deliver() with LVT0,
* before NMI watchdog was enabled. Already handled by
* kvm_apic_accept_pic_intr().
*/
break;

default:
printk(KERN_ERR "TODO: unsupported delivery mode %x\n",
delivery_mode);
break;
}
return result;
}



对于MSI来说

就是将irq消息解析,然后构造发送给VCPULAPIC,后面和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之前被调用,执行路径为:

kvm_vcpu_ioctl->kvm_arch_vcpu_ioctl_run->__vcpu_run->vcpu_enter_guest->inject_pending_event

位置:arch/x86/kvm/x86.c

参数:发生退出的虚拟cpu结构体struct kvm_vcpu *vcpu。

注意这里将NMIexception的注入过程都注释掉了,同理。

 

 
   

static void inject_pending_event(struct kvm_vcpu *vcpu)
{
/* try to reinject previous events if any */
if (vcpu->arch.exception.pending) {
trace_kvm_inj_exception(vcpu->arch.exception.nr,
vcpu->arch.exception.has_error_code,
vcpu->arch.exception.error_code);
kvm_x86_ops->queue_exception(vcpu, vcpu->arch.exception.nr,
vcpu->arch.exception.has_error_code,
vcpu->arch.exception.error_code,
vcpu->arch.exception.reinject);
return;
}

if (vcpu->arch.nmi_injected) {
kvm_x86_ops->set_nmi(vcpu);
return;
}
/*如果存在老的还没有注入的中断,则注入之,set_irq指向vmx_irq_inject*/
if (vcpu->arch.interrupt.pending) {
kvm_x86_ops->set_irq(vcpu);
return;
}

/* try to inject new event if pending */
if (vcpu->arch.nmi_pending) {
if (kvm_x86_ops->nmi_allowed(vcpu)) {
--vcpu->arch.nmi_pending;
vcpu->arch.nmi_injected = true;
kvm_x86_ops->set_nmi(vcpu);
}
} else if (kvm_cpu_has_injectable_intr(vcpu)) {/*检测虚拟中断控制器,看是否有中断发生*/
if (kvm_x86_ops->interrupt_allowed(vcpu)) {
/*获得当前中断号,并且将当前中断pending标记为true*/
kvm_queue_interrupt(vcpu, kvm_cpu_get_interrupt(vcpu),
false);
/*将中断写入VMCS的中断信息位*/
kvm_x86_ops->set_irq(vcpu);
}
}
}


set_irq(),实现写入VMCS,代码如下:

位置: arch/x86/kvm/vmx.c

 

 
   

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;

trace_kvm_inj_virq(irq);

++vcpu->stat.irq_injections;
if (vmx->rmode.vm86_active) {
int inc_eip = 0;
if (vcpu->arch.interrupt.soft)
inc_eip = vcpu->arch.event_exit_inst_len;
if (kvm_inject_realmode_interrupt(vcpu, irq, inc_eip) != EMULATE_DONE)
kvm_make_request(KVM_REQ_TRIPLE_FAULT, vcpu);
return;
}
/*看是外部中断还是软中断,我们之前注入的地方默认是false,所以是走下面分支*/
intr = irq | INTR_INFO_VALID_MASK;
/*写VMCS位,指令长度*/
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中断信息位*/
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/i386/i8259.c), IOAPIC(hw/i386/ioapic.c,pic和apic相同处理,将pic扩展到24个), MSI(hw/pci/msix.c),在这里中断控制器里面都会触发这个QEMUkvm_set_irq函数。

至于这个kvm_set_irq函数位置在qemu-kvm.c通过一个KVM_IRQ_LINEIOCTL调用内核KVM模块里面的kvm_set_irq函数。

 

这里我就不分析QEMU中的中断源了,毕竟主要将的是KVM,也希望大家针对QEMU中断的中断源进行讨论。发表自己的观点。

 

你可能感兴趣的:(虚拟化)