|
写写中断的东西,但是还是有些地方不理解,希望大家多多指教。
中断,这个概念相信大家已经不陌生了,我也没什么资格来介绍中断,就简单的说一下。我认为它从宏观上看可以分为软件部分和硬件部分。
软件部分:
软件部分在操作系统中实现,如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开始。
函数位置: x86/irq_comm.c/
函数参数: kvm,发生中断的客户机的结构体指针,我们知道在KVM模块中有个vmlist,链接了所有注册的虚拟机,当多次使用QEMU命令以后会产生多个虚拟机,不同虚拟机中公用一个KVM模块,通过这个kvm结构体来辨识是哪个虚拟机,当然每个虚拟机里面可以对应多个VCPU,别把这个概念弄混淆了;irq_source_id中断资源ID,对于KVM设备我们都会申请一个中断资源ID,注册KVM IO设备时申请的;irq中断请求号,这个是转化GSI之前的,比如时钟是0号,这里就是0,而不是32;level表示中断的高低电平。
int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level)
{
struct kvm_kernel_irq_routing_entry *e, irq_set[KVM_NR_IRQCHIPS];
struct kvm_irq_routing_table *irq_rt;
......
/*检查中断请求号范围*/
if (irq nr_rt_entries)
/*提取中断路由表中对应的中断路由实体,map[irq]是一个对应中断的路由实体表头结点,这里遍历它能够得到所有对应的路由实体。
hlist_for_each_entry(e, n, &irq_rt->map[irq], link)
irq_set[i++] = *e;
while(i--) {
int r;
/*触发对应路由实体的触发函数,这个函数在之前的安装中断路由的时候已经注册,下面具体说明*/
r = irq_set.set(&irq_set, 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, struct kvm_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) {
/*普通的IRQCHIP,分为PIC和IOAPIC,路由函数分别是kvm_set_pic_irq和kvm_set_ioapic_irq,PIC分为主从两块芯片。*/
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;
... ...
}
/*MSI中断消息系统的中断触发函数kvm_set_msi*/
case KVM_IRQ_ROUTING_MSI:
e->set = kvm_set_msi;
......
}
/*中断路由实体添加到中断
hlist_add_head(&e->link, &rt->map[e->gsi]);
...
}
这样就会通过kvm_set_irq()调转到对应的中断注入函数。这个触发函数从上面来看可以分为三类kvm_set_pic_irq、kvm_set_ioapic_irq、kvm_set_msi,他们分别对应不同的中断方式。
对于PIC来说
它主要是设置kvm里面的虚拟中断控制器结构体struct kvm_pic完成虚拟终端控制器的设置。如果是边缘触发,需要触发电瓶先0再1或者先1再0,完成一个正常的中断模拟。
对于IOAPIC来说
相对要复杂一点,它的大概调用过程如下,有兴趣的可以去跟一下。
kvm_set_ioapic_irq -> kvm_ioapic_set_irq -> ioapic_service -> ioapic_deliver -> kvm_irq_delivery_to_apic -> __apic_accept_irq -> apic_set_vector
整个流程是先检查IOAPIC状态,如果符合注入条件,则组建中断结构体,发送到指定VCPU的LAPIC,设置LAPIC的寄存器,完成虚拟中断控制器设置。
这里只介绍两个关键的容易出问题的函数,第一个是中断消息的投递,如下:
位置:x86/ioapic.c
static int ioapic_service(struct kvm_ioapic *ioapic, unsigned int idx)
{
union kvm_ioapic_redirect_entry *pent;
int injected = -1;
/*取得对应中断的重定向表*/
pent = &ioapic->redirtbl[idx];
/*检查中断是否被mask了,如果不允许中断则不触发*/
if (!pent->fields.mask) {
/*构造APIC消息,准备发送到LAPIC*/
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, int delivery_mode,
int vector, int level, int trig_mode)
{
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:
......
if (trig_mode) {
/*中断触发,设置中断位,设置apic里面的寄存器变量,偏移地址APIC_TMR和Linux真实APIC控制器相同*/
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);
......
|
|