zoukankan      html  css  js  c++  java
  • KVM_IOEVENTFD KVM_IRQFD

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

    virtio_notify
    voidvirtio_notify(VirtIODevice *vdev, VirtQueue *vq)
    
    {
    
        if (!vring_notify(vdev, vq)) {
    
            return;
    
        }
    
        vdev->isr |= 0x01;
    
        virtio_notify_vector(vdev, vq->vector);
    
    }
    
    static void  virtio_notify_vector(VirtIODevice *vdev, uint16_t vector)
    
    {
    
        if (vdev->binding->notify) {
    
           vdev->binding->notify(vdev->binding_opaque, vector);
    
        }
    
    }
    
    vdev->binding->notify = virtio_pci_notify ==>
    
    static void  virtio_pci_notify(void *opaque, uint16_t vector)
    
    {
    
        VirtIOPCIProxy *proxy = opaque;
    
        if (msix_enabled(&proxy->pci_dev))
    
            msix_notify(&proxy->pci_dev,vector);
    
        else
    
            qemu_set_irq(proxy->pci_dev.irq[0],proxy->vdev->isr & 1);
    
    }
    
    Call msix or qemu_set_irq according to category. Msix is ​​similar to msi.
  • 相关阅读:
    1.1 Introduction中 Apache Kafka™ is a distributed streaming platform. What exactly does that mean?(官网剖析)(博主推荐)
    Android 涂鸦最佳实践
    使用 stvd 编译STM8S 时能看到使用RAM ROM大小的方法
    【四】注入框架RoboGuice使用:(Your First System Service Injection)
    让演示样例代码在手机上换行显示
    归并排序算法
    HDU 1573 X问题 中国剩余定理
    【leetcode】6. ZigZag Conversion
    sdfs
    翻转句子中单词的顺序 C语言
  • 原文地址:https://www.cnblogs.com/dream397/p/14161550.html
Copyright © 2011-2022 走看看