码迷,mamicode.com
首页 > 其他好文 > 详细

KVM_IOEVENTFD KVM_IRQFD

时间:2020-12-24 12:27:36      阅读:0      评论:0      收藏:0      [点我收藏+]

标签: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

 

cpu虚拟化

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

技术图片

(1)pic中断处理流程

为了一窥中断处理的具体流程,这里我们选择最简单模拟串口为例进行分析。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)函数说明

 

  1.  
    int kvm_set_irq(struct kvm *kvm, int irq_source_id, u32 irq, int level,bool line_status){
  2.  
    ...
  3.  
     
  4.  
    if (irq < irq_rt->nr_rt_entries)
  5.  
    /*提取中断路由表中对应的中断路由实体,map[irq]是一个对应中断的路由实体表头结点,这里遍历它能够得到所有对应的路由实体。*/
  6.  
    hlist_for_each_entry(e, &irq_rt->map[irq], link)
  7.  
     
  8.  
    while(i--) {
  9.  
    int r;
  10.  
    /*触发对应路由实体的触发函数,这个函数在之前的安装中断路由的时候已经注册,注册函数是 <span style="color: rgb(51, 51, 51); font-family: Arial; font-size: 14px; line-height: 26px; text-align: -webkit-left; ">setup_routing_entry() </span>*/
  11.  
    r = irq_set[i].set(&irq_set[i], kvm, irq_source_id, level);
  12.  
    if (r < 0)
  13.  
    continue;
  14.  
    ret = r + ((ret < 0) ? 0 : ret);
  15.  
    }
  16.  
    return ret;
  17.  
    }

由 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()说明

 

  1.  
    static int ioapic_service(struct kvm_ioapic *ioapic, unsigned int idx,
  2.  
    bool line_status)
  3.  
    {
  4.  
    union kvm_ioapic_redirect_entry *pent;
  5.  
    int injected = -1;
  6.  
    /*读取“中断重定向表”*/
  7.  
    pent = &ioapic->redirtbl[idx];
  8.  
    /*检查中断是否被屏蔽,如果被屏蔽,则不触发中断*/
  9.  
    if (!pent->fields.mask) {
  10.  
    /*发送到LAPIC*/
  11.  
    injected = ioapic_deliver(ioapic, idx, line_status);
  12.  
    if (injected && pent->fields.trig_mode == IOAPIC_LEVEL_TRIG)
  13.  
    pent->fields.remote_irr = 1;
  14.  
    }
  15.  
     
  16.  
    return injected;
  17.  
    }


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()

 

KVM_IOEVENTFD KVM_IRQFD

标签:lan   类型   fun   reference   interrupt   func   memory   clu   lse   

原文地址:https://www.cnblogs.com/dream397/p/14161550.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!