标签:lan 类型 fun reference interrupt func memory clu lse
kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick)
Ioctl Cmd |
Implement function |
Src file |
KVM_CREATE_VCPU |
kvm_vm_ioctl_create_vcpu |
Kvm_main.c |
KVM_SET_USER_MEMORY_REGION |
kvm_vm_ioctl_set_memory_region |
Kvm_main.c |
KVM_GET_DIRTY_LOG |
kvm_vm_ioctl_get_dirty_log |
Arch/x86/kvm/x86.c |
KVM_REGISTER_COALESCED_MMIO |
kvm_vm_ioctl_register_coalesced_mmio |
Mmio.c |
KVM_UNREGISTER_COALESCED_MMIO |
kvm_vm_ioctl_unregister_coalesced_mmio |
Mmio.c |
KVM_IRQFD |
kvm_irqfd |
Eventfd.c |
KVM_IOEVENTFD |
kvm_ioeventfd |
Eventfd.c |
KVM_SIGNAL_MSI |
kvm_send_userspace_msi |
Irqchip.c |
KVM_SET_GSI_ROUTING |
kvm_set_irq_routing |
Irqchip.c |
KVM_CREATE_DEVICE |
kvm_ioctl_create_device |
Kvm_main.c |
defautl |
kvm_arch_vm_ioctl kvm_vm_ioctl_assigned_device |
Arch/x86/kvm/x86.c Assigend-dev.c |
QEMU创建CPU线程,在初始化的时候设置好相应的虚拟CPU寄存器的值,然后调用KVM的接口,运行虚拟机,在物理CPU上执行虚拟机代码。
在虚拟机运行时,KVM会截获虚拟机中的敏感指令,当虚拟机中的代码是敏感指令或者满足了一定的退出条件时,CPU会从VMX non-root模式退出到KVM,这就是下图的VM exit。虚拟机的退出首先陷入到KVM进行处理,但是如果遇到KVM无法处理的事件,比如虚拟机写了设备的寄存器地址,那么KVM就会将这个操作交给QEMU处理。当QEMU/KVM处理好了退出事件后,又会将CPU置于VMX non-root模式,也就是下图的VM Entry。
KVM使用VMCS结构来保存VM Exit和VM Entry
为了一窥中断处理的具体流程,这里我们选择最简单模拟串口为例进行分析。qemu作为设备模拟器会模拟很多传统的设备,isa-serial就是其中之一。我们看下串口触发中断时候的调用栈:
#0 0x00005555557dd543 in kvm_set_irq (s=0x5555568f4440, irq=4, level=1) at /home/fang/code/qemu/accel/kvm/kvm-all.c:991
#1 0x0000555555881c0f in kvm_pic_set_irq (opaque=0x0, irq=4, level=1) at /home/fang/code/qemu/hw/i386/kvm/i8259.c:114
#2 0x00005555559cb0aa in qemu_set_irq (irq=0x5555577c9dc0, level=1) at hw/core/irq.c:45
#3 0x0000555555881fda in kvm_pc_gsi_handler (opaque=0x555556b61970, n=4, level=1) at /home/fang/code/qemu/hw/i386/kvm/ioapic.c:55
#4 0x00005555559cb0aa in qemu_set_irq (irq=0x555556b63660, level=1) at hw/core/irq.c:45
#5 0x00005555559c06e7 in qemu_irq_raise (irq=0x555556b63660) at /home/fang/code/qemu/include/hw/irq.h:16
#6 0x00005555559c09b3 in serial_update_irq (s=0x555557b77770) at hw/char/serial.c:145
#7 0x00005555559c138c in serial_ioport_write (opaque=0x555557b77770, addr=1, val=2, size=1) at hw/char/serial.c:404
可以看到qemu用户态有个函数kvm_set_irq,这个函数是用户态通知kvm内核态触发一个中断的入口。函数中通过调用 kvm_vm_ioctl注入一个中断,调用号是 KVM_IRQ_LINE(pic类型中断),入参是一个 kvm_irq_level 的数据结构(传入了irq编号和中断的电平信息)。模拟isa串口是个isa设备使用边沿触发,所以注入中断会调用2次这个函数前后2次电平相反。
int kvm_set_irq(KVMState *s, int irq, int level)
{
struct kvm_irq_level event;
int ret;
assert(kvm_async_interrupts_enabled());
event.level = level;
event.irq = irq;
ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event);
if (ret < 0) {
perror("kvm_set_irq");
abort();
}
return (s->irq_set_ioctl == KVM_IRQ_LINE) ? 1 : event.status;
}
这个ioctl在内核的处理对应到下面这段代码,pic类型中断进而会调用到kvm_vm_ioctl_irq_line函数。
kvm_vm_ioctl
{
......
#ifdef __KVM_HAVE_IRQ_LINE
case KVM_IRQ_LINE_STATUS:
case KVM_IRQ_LINE: { /* 处理pic上产生的中断 */
struct kvm_irq_level irq_event;
r = -EFAULT;
if (copy_from_user(&irq_event, argp, sizeof(irq_event)))
goto out;
r = kvm_vm_ioctl_irq_line(kvm, &irq_event,
ioctl == KVM_IRQ_LINE_STATUS);
if (r)
goto out;
r = -EFAULT;
if (ioctl == KVM_IRQ_LINE_STATUS) {
if (copy_to_user(argp, &irq_event, sizeof(irq_event)))
goto out;
}
r = 0;
break;
}
#endif
#ifdef CONFIG_HAVE_KVM_IRQ_ROUTING /* 处理ioapic的中断 */
case KVM_SET_GSI_ROUTING: {
struct kvm_irq_routing routing;
struct kvm_irq_routing __user *urouting;
struct kvm_irq_routing_entry *entries = NULL;
r = -EFAULT;
if (copy_from_user(&routing, argp, sizeof(routing)))
goto out;
r = -EINVAL;
if (!kvm_arch_can_set_irq_routing(kvm))
goto out;
if (routing.nr > KVM_MAX_IRQ_ROUTES)
goto out;
if (routing.flags)
goto out;
if (routing.nr) {
r = -ENOMEM;
entries = vmalloc(routing.nr * sizeof(*entries));
if (!entries)
goto out;
r = -EFAULT;
urouting = argp;
if (copy_from_user(entries, urouting->entries,
routing.nr * sizeof(*entries)))
goto out_free_irq_routing;
}
r = kvm_set_irq_routing(kvm, entries, routing.nr,
routing.flags);
out_free_irq_routing:
vfree(entries);
break;
}
......
}
kvm_vm_ioctl_irq_line函数中会进一步调用内核态的kvm_set_irq函数(用户态用同名函数额),这个函数是整个中断处理的入口:
int kvm_vm_ioctl_irq_line(struct kvm *kvm, struct kvm_irq_level *irq_event,
bool line_status)
{
if (!irqchip_in_kernel(kvm))
return -ENXIO;
irq_event->status = kvm_set_irq(kvm, KVM_USERSPACE_IRQ_SOURCE_ID,
irq_event->irq, irq_event->level,
line_status);
return 0;
}
kvm_set_irq函数的入参有5个,kvm代表某个特性的的虚拟机,irq_source_id可以是KVM_USERSPACE_IRQ_SOURCE_ID或者KVM_IRQFD_RESAMPLE_IRQ_SOURCE_ID(这个是irqfd这个我们这里不讨论),irq是传入的设备irq号,对于串口来说第一个port的irq=4而且irq=gsi,level代表电平。kvm_irq_map函数会获取改gsi索引上注册的中断路由项(kvm_kernel_irq_routing_entry),while循环中会挨个调用每个中断路由项上的set方法触发中,在guest中会忽略没有实现的芯片类型发送的中断消息。
对于PIC而言,set函数对应于kvm_set_pic_irq函数,对于IOAPIC而言set函数对应于kvm_set_ioapic_irq(不同的chip不一样额)。对于串口而言,我们会进一步调用kvm_pic_set_irq来处理中断。
/*
* Return value:
* < 0 Interrupt was ignored (masked or not delivered for other reasons)
* = 0 Interrupt was coalesced (previous irq is still pending)
* > 0 Number of CPUs interrupt was delivered to
*/
int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level,
bool line_status)
{
struct kvm_kernel_irq_routing_entry irq_set[KVM_NR_IRQCHIPS];
int ret = -1, i, idx;
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.
*/
idx = srcu_read_lock(&kvm->irq_srcu);
i = kvm_irq_map_gsi(kvm, irq_set, irq);
srcu_read_unlock(&kvm->irq_srcu, idx);
/* 依次调用同一个gsi上的所有芯片的set方法 */
while (i--) {
int r;
r = irq_set[i].set(&irq_set[i], kvm, irq_source_id, level,
line_status);
if (r < 0)
continue;
ret = r + ((ret < 0) ? 0 : ret);
}
return ret;
}
/* 查询出此gsi号上对应的所有的“中断路由项” */
int kvm_irq_map_gsi(struct kvm *kvm,
struct kvm_kernel_irq_routing_entry *entries, int gsi)
{
struct kvm_irq_routing_table *irq_rt;
struct kvm_kernel_irq_routing_entry *e;
int n = 0;
irq_rt = srcu_dereference_check(kvm->irq_routing, &kvm->irq_srcu,
lockdep_is_held(&kvm->irq_lock));
if (irq_rt && gsi < irq_rt->nr_rt_entries) {
hlist_for_each_entry(e, &irq_rt->map[gsi], link) { /* 遍历此gsi对应的中断路由项 */
entries[n] = *e;
++n;
}
}
return n;
}
kvm_pic_set_irq 函数中,根据传入的irq编号check下原先的irq_state将本次的level与上次的irq_state进行逻辑“异或”判断是否发生电平跳变从而进行边沿检测(pic_set_irq1)。如果是的话设置IRR对应的bit,然后调用和pic_update_irq更新pic相关的寄存器并唤醒vcpu注入中断。
int kvm_pic_set_irq(struct kvm_pic *s, int irq, int irq_source_id, int level)
{
int ret, irq_level;
BUG_ON(irq < 0 || irq >= PIC_NUM_PINS);
pic_lock(s);
/* irq_level = 1表示该irq引脚有电平跳变,出发中断 */
irq_level = __kvm_irq_line_state(&s->irq_states[irq],
irq_source_id, level);
/* 一个pic最多8个irq */
ret = pic_set_irq1(&s->pics[irq >> 3], irq & 7, irq_level);
pic_update_irq(s);
trace_kvm_pic_set_irq(irq >> 3, irq & 7, s->pics[irq >> 3].elcr,
s->pics[irq >> 3].imr, ret == 0);
pic_unlock(s);
return ret;
}
最后的最后,pic_unlock函数中在如果wakeup为true(又中断产生时)的时候会遍历每个vcpu,在满足条件的情况下调用kvm_make_request为vcpu注入中断,然后kick每个vcpu。
static void pic_unlock(struct kvm_pic *s)
__releases(&s->lock)
{
bool wakeup = s->wakeup_needed;
struct kvm_vcpu *vcpu;
int i;
s->wakeup_needed = false;
spin_unlock(&s->lock);
if (wakeup) { /* wakeup在pic_update_irq中被更新 */
kvm_for_each_vcpu(i, vcpu, s->kvm) {
if (kvm_apic_accept_pic_intr(vcpu)) {
/* 中断注入会在kvm_enter_guest时候执行 */
kvm_make_request(KVM_REQ_EVENT, vcpu);
kvm_vcpu_kick(vcpu);
return;
}
}
}
}
kvm_vcpu_kick(vcpu): 这个函数的功能就是,判断vcpu是否正在物理cpu上运行,如果是,则让vcpu退出,以便进行中断注入。
本文讲述一个网络数据包从到达物理网卡,一直到中断注入给VM的整个过程。
为了讲述清晰,假设宿主物理机有两个物理CPU,分别为CPU0和CPU1。假设GuestOS运行在CPU1上,物理网卡接到数据包后把中断请求发送到CPU0.
1.网络数据包Package到达物理网卡NIC, NIC收到数据包后,向CPU0发送中断请求,通知CPU0有网络数据包到达。
2.CPU0收到中断请求后,调用中断处理函数,处理这个中断请求。但是,这个数据包可能是发给VM的,所以这里Host不会直接调用Host的中断处理函数,而是会调用QEMU中实现的Bridge(Brigde就是一个网桥)。Bridge会把请求再转发给QEMU模拟的TAP设备。
3.TAP设备会进行判断这个请求是发给谁的,如果是发给Host的,则直接调用Host的中断处理函数;如果是发给VM的,那么就会TAP就会调用一系列函数,比如select,知道调用到kvm_vm_ioctl(, KVM_IRQ_LINE_STATUS,)。到此为止,QEMU模拟结束,开始进入KVM执行。
4.kvm_set_irq() (irqchip.c)函数说明
由 setup_routing_entry() (在irqchip.c中)代码,可知上面ira_set[i].set(..) 调用的就是kvm_set_ioapic_irq().
具体注册路径为:setup_routing_entry() -->kvm_set_routing_entry()-->e->set = kvm_set_ioapic_irq()
5. ioapic_service()说明
6.ioapic_deliver()
这个函数主要功能:读取中断重定向表,设置中断请求的dest_id, vector,dest_mode等请求信息,然后再调用kvm_irq_delivery_to_apic()把中断请求发送给LAPIC
7.kvm_irq_delivery_to_apic()函数
这是一个非常关键的函数! 是一个枢纽函数,无论IOAPIC发给LAPIC中断请求(外部I/O中断),还是LAPIC发个LAPIC中断请求(IPI中断),最后都是调用的这个函数!
这个函数的功能有: 根据传入的参数irq,获取目标LAPIC编号dest_id,再根据dest_id找到其对应的vcpu,最后调用kvm_apic_set_irq() 设置目标LAPIC的中断请求寄存器。
8.__apic_accept_irq()
函数功能:向lapic添加一个Pending IRQ
首先根据delivery_mode选择添加的方式,基本都会调用下面两个函数:
kvm_make_request(..,vcpu): 这个函数就是向目标vcpu添加一个reqest请求。当目标vcpu再次enter_guest时,check到这个中断请求,进行中断注入,vcpu重新运行,捕获这个中断。
kvm_vcpu_kick(vcpu): 这个函数的功能就是,判断vcpu是否正在物理cpu上运行,如果是,则让vcpu退出,以便进行中断注入。
----------------------------------------
到此为止,kvm模拟的IOAPIC, LAPIC就结束了。下面就是vcpu_enter_guest()进行中断注入的过程了
----------------------------------------
9.vcpu_enter_guest()
当vcpu再次被调度进行vm_entry时,就会执行这个vcpu_enter_guest()函数。
在vcpu run之前,会调用kvm_check_reqest(KVM_REQ_EVENT,vcpu)检查是否有中断请求需要注入。
在上面8中,调用过kvm_make_request(),所以这里if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win)条件判断成立,执行下面的inject_pending_event(vcpu)
10.inject_pending_event()
这个函数的功能就是,把中断请求写入到目标vcpu的中断请求寄存器。最后就是写VMCS中断的irq寄存器。
调用为kvm_x86_ops->set_irq(vcpu),这正调用的就是vmx_inject_irq()
在static struct kvm_x86_ops vmx_x86_ops ={ .. .set_irq = vmx_inject_irq }.
vmx_inject_irq() 调用vmx_write32() 写VMCS 的IRQ寄存器。
11. vmx_vcpu_run()
vcpu_enter_guest()最后调用 vmx_vcpu_run(), vcpu开始执行。
在执行前,会读取VMCS中的寄存器信息,由于前面写了IRQ,所以vcpu运行就会知道有中断请求到来,再调用GuestOS的中断处理函数处理中断请求。
----------------------
到此,从一个网络包到达网卡,到qemu 和kvm模拟中断,到中断注入给vcpu,再到vcpu运行发现中断,调用中断处理函数,完整过程大概如此。
----------------------
处理器间IPI中断
IPI中断处理过程要简单的多。
LAPIC中有两个重要的寄存器:
LAPIC_ICR: 存放中断向量
LAPIC_ICR2: 存放中断请求目标
如果一个vcpu0要个vcpu1发IPI中断,vcpu0只要调用apic_reg_write()写LAPIC的这两个寄存器就可以了。
再调apic_sent_ipi()-->kvm_irq_deliver_to_apic()
标签:lan 类型 fun reference interrupt func memory clu lse
原文地址:https://www.cnblogs.com/dream397/p/14161550.html