• KVM中断虚拟化浅析


    2017-08-24


    今天咱们聊聊KVM中断虚拟化,虚拟机的中断源大致有两种方式,来自于用户空间qemu和来自于KVM内部。

    中断虚拟化起始关键在于对中断控制器的虚拟化,中断控制器目前主要有APIC,这种架构下设备控制器通过某种触发方式通知IO APIC,IO APIC根据自身维护的重定向表pci irq routing table格式化出一条中断消息,把中断消息发送给local APIC,local APIC局部与CPU,即每个CPU一个,local APIC 具备传统中断控制器的相关功能以及各个寄存器,中断请求寄存器IRR,中断屏蔽寄存器IMR,中断服务寄存器ISR等,针对这些关键部件的虚拟化是中断虚拟化的重点。在KVM架构下,每个KVM虚拟机维护一个Io APIC,但是每个VCPU有一个local APIC。

     核心数据结构介绍:

    kvm_irq_routing_table

    struct kvm_irq_routing_table {
        /*ue->gsi*/
        int chip[KVM_NR_IRQCHIPS][KVM_IRQCHIP_NUM_PINS];
        struct kvm_kernel_irq_routing_entry *rt_entries;
        u32 nr_rt_entries;
        /*
         * Array indexed by gsi. Each entry contains list of irq chips
         * the gsi is connected to.
         */
        struct hlist_head map[0];
    };

    这个是一个中断路由表,每个KVM都有一个, chip是一个二维数组,表示三个芯片的各个管脚,每个芯片有24个管脚,每个数组项纪录对应管脚的GSI号;rt_entries是一个指针,指向一个kvm_kernel_irq_routing_entry数组,数组中共有nr_rt_entries项,每项对应一个IRQ;map其实可以理解为一个链表头数组,可以根据GSi号作为索引,找到同一IRQ关联的所有kvm_kernel_irq_routing_entry。具体中断路由表的初始化部分见本文最后一节

    struct kvm_kernel_irq_routing_entry {
        u32 gsi;
        u32 type;
        int (*set)(struct kvm_kernel_irq_routing_entry *e,
               struct kvm *kvm, int irq_source_id, int level,
               bool line_status);
        union {
            struct {
                unsigned irqchip;
                unsigned pin;
            } irqchip;
            struct msi_msg msi;
        };
        struct hlist_node link;
    };

    gsi是该entry对应的gsi号,一般和IRQ是一样,set方法是该IRQ关联的触发方法,通过该方法把IRQ传递给IO-APIC,;link就是连接点,连接在上面同一IRQ对应的map上;

    中断注入在KVM内部流程起始于一个函数kvm_set_irq

    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)
                irq_set[i++] = *e;
        rcu_read_unlock();
        /*依次调用同一个irq上的所有芯片的set方法*/
        while(i--) {
            int r;
            /*kvm_set_pic_irq  kvm_set_ioapic_irq*/
            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;
    }

     kvm指定特定的虚拟机,irq_source_id是中断源ID,一般有KVM_USERSPACE_IRQ_SOURCE_ID和KVM_IRQFD_RESAMPLE_IRQ_SOURCE_ID;irq是全局的中断号,level指定高低电平,需要注意的是,针对边沿触发,需要两个电平触发来模拟,先高电平再低电平。回到函数中,首先要收集的是同一irq上注册的所有的设备信息,这主要在于irq共享的情况,非共享的情况下最多就一个。设备信息抽象成一个kvm_kernel_irq_routing_entry,这里临时放到irq_set数组中。然后对于数组中的每个元素,调用其set方法,目前大都是APIC架构,因此set方法基本都是kvm_set_ioapic_irq,在传统pic情况下,是kvm_set_pic_irq。我们以kvm_set_ioapic_irq为例进行分析,该函数没有实质性的操作,就调用了kvm_ioapic_set_irq函数

    int kvm_ioapic_set_irq(struct kvm_ioapic *ioapic, int irq, int irq_source_id,
                   int level, bool line_status)
    {
        u32 old_irr;
        u32 mask = 1 << irq;//irq对应的位
        union kvm_ioapic_redirect_entry entry;
        int ret, irq_level;
    
        BUG_ON(irq < 0 || irq >= IOAPIC_NUM_PINS);
    
        spin_lock(&ioapic->lock);
        old_irr = ioapic->irr;
        /*判断请求高电平还是低电平*/
        irq_level = __kvm_irq_line_state(&ioapic->irq_states[irq],
                         irq_source_id, level);
    
        entry = ioapic->redirtbl[irq];
        irq_level ^= entry.fields.polarity;
        /*模拟低电平*/
        if (!irq_level) {
            ioapic->irr &= ~mask;
            ret = 1;
        } else {
        /*判断触发方式*/
            int edge = (entry.fields.trig_mode == IOAPIC_EDGE_TRIG);
    
            if (irq == RTC_GSI && line_status &&
                rtc_irq_check_coalesced(ioapic)) {
                ret = 0; /* coalesced */
                goto out;
            }
            /*设置中断信号到中断请求寄存器*/
            ioapic->irr |= mask;
            /*如果是电平触发且旧的irr和请求的irr不相等,调用ioapic_service*/
            if ((edge && old_irr != ioapic->irr) ||
                (!edge && !entry.fields.remote_irr))
                ret = ioapic_service(ioapic, irq, line_status);
            else
                ret = 0; /* report coalesced interrupt */
        }
    out:
        trace_kvm_ioapic_set_irq(entry.bits, irq, ret == 0);
        spin_unlock(&ioapic->lock);
    
        return ret;
    }

    到这里,中断已经到达模拟的IO-APIC了,IO-APIC最重要的就是它的重定向表,针对重定向表的操作主要在ioapic_service中,之前都是做一些准备工作,在进入ioapic_service函数之前,主要有两个任务:1、判断触发方式,主要是区分电平触发和边沿触发。2、设置ioapic的irr寄存器。之前我们说过,电触发需要两个边沿触发来模拟,前后电平相反。这里就要先做判断是对应哪一次。只有首次触发才会进行后续的操作,而二次触发相当于reset操作,就是把ioapic的irr寄存器清除。在电平触发模式下且请求的irq和ioapic中保存的irq不一致,就会对其进行更新,进入ioapic_service函数。

    static int ioapic_service(struct kvm_ioapic *ioapic, unsigned int idx,
            bool line_status)
    {
        union kvm_ioapic_redirect_entry *pent;
        int injected = -1;
        /*获取重定向表项*/
        pent = &ioapic->redirtbl[idx];
    
        if (!pent->fields.mask) {
            /*send irq to local apic*/
            injected = ioapic_deliver(ioapic, idx, line_status);
            if (injected && pent->fields.trig_mode == IOAPIC_LEVEL_TRIG)
                pent->fields.remote_irr = 1;
        }
        return injected;
    }

     该函数比较简单,就是获取根据irq号,获取重定向表中的一项,然后向本地APIC传递,即调用ioapic_deliver函数,当然前提是kvm_ioapic_redirect_entry没有设置mask,ioapic_deliver主要任务就是根据kvm_ioapic_redirect_entry,构建kvm_lapic_irq,这就类似于在总线上的传递过程。构建之后调用kvm_irq_delivery_to_apic,该函数会把消息传递给相应的VCPU ,具体需要调用kvm_apic_set_irq函数,继而调用__apic_accept_irq,该函数中会根据不同的传递模式处理消息,大部分情况都是APIC_DM_FIXED,在该模式下,中断被传递到特定的CPU,其中会调用kvm_x86_ops->deliver_posted_interrupt,实际上对应于vmx.c中的vmx_deliver_posted_interrupt

    static void vmx_deliver_posted_interrupt(struct kvm_vcpu *vcpu, int vector)
    {
        struct vcpu_vmx *vmx = to_vmx(vcpu);
        int r;
        /*设置位图*/
        if (pi_test_and_set_pir(vector, &vmx->pi_desc))
            return;
        /*标记位图更新标志*/
        r = pi_test_and_set_on(&vmx->pi_desc);
        kvm_make_request(KVM_REQ_EVENT, vcpu);
    #ifdef CONFIG_SMP
        if (!r && (vcpu->mode == IN_GUEST_MODE));
    
        else
    #endif
            kvm_vcpu_kick(vcpu);
    }

    这里主要是设置vmx->pi_desc中的位图即struct pi_desc 中的pir字段,其是一个32位的数组,共8项。因此最大标记256个中断,每个中断向量对应一位。设置好后,请求KVM_REQ_EVENT事件,在下次vm-entry的时候会进行中断注入。

    具体注入过程:

    在vcpu_enter_guest (x86.c)函数中,有这么一段代码

    if (kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win) {
            kvm_apic_accept_events(vcpu);
            if (vcpu->arch.mp_state == KVM_MP_STATE_INIT_RECEIVED) {
                r = 1;
                goto out;
            }
            /*注入中断在vcpu加载到真实cpu上后,相当于某些位已经被设置*/
            inject_pending_event(vcpu);//中断注入
    ……

    即在进入非跟模式之前会检查KVM_REQ_EVENT事件,如果存在pending的事件,则调用kvm_apic_accept_events接收,这里主要是处理APIC初始化期间和IPI中断的,暂且不关注。之后会调用inject_pending_event,在这里会检查当前是否有可注入的中断,而具体检查过程时首先会通过kvm_cpu_has_injectable_intr函数,其中调用kvm_apic_has_interrupt->apic_find_highest_irr->vmx_sync_pir_to_irr,vmx_sync_pir_to_irr函数对中断进行收集,就是检查vmx->pi_desc中的位图,如果有,则会调用kvm_apic_update_irr把信息更新到apic寄存器里。然后调用apic_search_irr获取IRR寄存器中的中断,没找到的话会返回-1.找到后调用kvm_queue_interrupt,把中断记录到vcpu中。

    static inline void kvm_queue_interrupt(struct kvm_vcpu *vcpu, u8 vector,
        bool soft)
    {
        vcpu->arch.interrupt.pending = true;
        vcpu->arch.interrupt.soft = soft;
        vcpu->arch.interrupt.nr = vector;
    }

    最后会调用kvm_x86_ops->set_irq,进行中断注入的最后一步,即写入到vmcs结构中。该函数指针指向vmx_inject_irq

    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;
        }
        intr = irq | INTR_INFO_VALID_MASK;//设置有中断向量的有效性
        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_write32(VM_ENTRY_INTR_INFO_FIELD, intr);
    }

    最终会写入到vmcs的VM_ENTRY_INTR_INFO_FIELD中,这需要按照一定的格式。具体格式详见intel手册。0-7位是向量号,8-10位是中断类型(硬件中断或者软件中断),最高位是有效位,12位是NMI标志。

    #define INTR_INFO_VECTOR_MASK           0xff            /* 7:0 */
    #define INTR_INFO_INTR_TYPE_MASK        0x700           /* 10:8 */
    #define INTR_INFO_DELIVER_CODE_MASK     0x800           /* 11 */
    #define INTR_INFO_UNBLOCK_NMI        0x1000        /* 12 */
    #define INTR_INFO_VALID_MASK            0x80000000      /* 31 */

     中断路由表的初始化

    用户空间qemu通过KVM_CREATE_DEVICE API接口进入KVM的kvm_vm_ioctl处理函数,继而进入kvm_arch_vm_ioctl,根据参数中的KVM_CREATE_IRQCHIP标志进入初始化中断控制器的流程,首先肯定是注册pic和io APIC,这里我们就不详细阐述,重点在于后面对中断路由表的初始化过程。中断路由表的初始化通过kvm_setup_default_irq_routing函数实现,

    int kvm_setup_default_irq_routing(struct kvm *kvm)
    {
        return kvm_set_irq_routing(kvm, default_routing,
                       ARRAY_SIZE(default_routing), 0);
    }

    首个参数kvm指定特定的虚拟机,后面default_routing是一个全局的kvm_irq_routing_entry数组,就定义在irq_comm.c中,该数组没别的作用,就是初始化kvm_irq_routing_table,看下kvm_set_irq_routing

    int kvm_set_irq_routing(struct kvm *kvm,
                const struct kvm_irq_routing_entry *ue,
                unsigned nr,
                unsigned flags)
    {
        struct kvm_irq_routing_table *new, *old;
        u32 i, j, nr_rt_entries = 0;
        int r;
        /*正常情况下,nr_rt_entries=nr*/
        for (i = 0; i < nr; ++i) {
            if (ue[i].gsi >= KVM_MAX_IRQ_ROUTES)
                return -EINVAL;
            nr_rt_entries = max(nr_rt_entries, ue[i].gsi);
        }
        nr_rt_entries += 1;
        /*为中断路由表申请空间*/
        new = kzalloc(sizeof(*new) + (nr_rt_entries * sizeof(struct hlist_head))
                  + (nr * sizeof(struct kvm_kernel_irq_routing_entry)),
                  GFP_KERNEL);
    
        if (!new)
            return -ENOMEM;
        /*设置指针*/
        new->rt_entries = (void *)&new->map[nr_rt_entries];
    
        new->nr_rt_entries = nr_rt_entries;
        for (i = 0; i < KVM_NR_IRQCHIPS; i++)
            for (j = 0; j < KVM_IRQCHIP_NUM_PINS; j++)
                new->chip[i][j] = -1;
        /*初始化每一项kvm_kernel_irq_routing_entry*/
        for (i = 0; i < nr; ++i) {
            r = -EINVAL;
            if (ue->flags)
                goto out;
            r = setup_routing_entry(new, &new->rt_entries[i], ue);
            if (r)
                goto out;
            ++ue;
        }
        mutex_lock(&kvm->irq_lock);
        old = kvm->irq_routing;
        kvm_irq_routing_update(kvm, new);
        mutex_unlock(&kvm->irq_lock);
    
        synchronize_rcu();
        /*释放old*/
        new = old;
        r = 0;
    out:
        kfree(new);
        return r;
    }

    可以参考一个宏:

    #define IOAPIC_ROUTING_ENTRY(irq)
    { .gsi = irq, .type = KVM_IRQ_ROUTING_IRQCHIP, .u.irqchip.irqchip = KVM_IRQCHIP_IOAPIC, .u.irqchip.pin = (irq) }

    这是初始化default_routing的一个关键宏,没一项都是通过该宏传递irq号(0-23)64位下是0-47,可见gsi就是irq号,所以实际上,回到函数中nr_rt_entries就是数组中项数,接着为kvm_irq_routing_table分配空间,注意分配的空间包含三部分:kvm_irq_routing_table结构、nr_rt_entries个hlist_head和nr个kvm_kernel_irq_routing_entry,所以kvm_irq_routing_table的大小是和全局数组的大小一样的。整个结构如下图所示

    根据上图就可以理解new->rt_entries = (void *)&new->map[nr_rt_entries];这行代码的含义,接下来是对没项的table的chip数组做初始化,这里初始化为-1.接下来就是一个循环,对每一个kvm_kernel_irq_routing_entry做初始化,该过程是通过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)
            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;
    }

     之前的初始化过程我们已经看见了,.type为KVM_IRQ_ROUTING_IRQCHIP,所以这里实际上就是把e->gsi = ue->gsi;e->type = ue->type;然后调用了kvm_set_routing_entry,该函数中主要是设置了kvm_kernel_irq_routing_entry中的set函数,APIC的话设置的是kvm_set_ioapic_irq函数,而pic的话设置kvm_set_pic_irq函数,然后设置irqchip的类型和管脚,对于IOAPIC也是直接复制过来,PIC由于管脚计算是irq%8,所以这里需要加上8的偏移。之后设置table的chip为gis号。回到setup_routing_entry函数中,就把kvm_kernel_irq_routing_entry以gsi号位索引,加入到了map数组中对应的双链表中。再回到kvm_set_irq_routing函数中,接下来就是更新kvm结构中的irq_routing指针了。

    中断虚拟化流程

    kvm_set_irq
      kvm_ioapic_set_irq
         ioapic_service
          ioapic_deliver
            kvm_irq_delivery_to_apic
              kvm_apic_set_irq
                __apic_accept_irq
                  vmx_deliver_posted_interrupt

    具体注入阶段
    vcpu_enter_guest
       kvm_apic_accept_events
          inject_pending_event
            kvm_queue_interrupt
              vmx_inject_irq
                vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr);

     中断路由表初始化

    x86.c kvm_arch_vm_ioctl
      kvm_setup_default_irq_routing irq_common.c
        kvm_set_irq_routing irq_chip.c
          setup_routing_entry irq_chip.c
            kvm_set_routing_entry irq_chip.c
              e->set = kvm_set_ioapic_irq; irq_common.c

    以马内利!

    参考资料:

    LInux3.10.1源码

  • 相关阅读:
    Vertica的这些事(九)——-vertica存储统计信息
    Vertica的这些事(八)——-Vertica-管理
    Vertica的这些事(六)——-vertica中group-by-和join-语句的优化
    Vertica的这些事(五)——-谈谈vertica的flex-table
    Vertica的这些事(四)——-vertica加密数据
    Vertica的这些事(三)——Vertica中实现Oracle中的ws_concat功能
    Vertica的这些事(二)——SQL-Server、Oracle、MySQL和Vertica数据库常用函数对比
    Docker_安装和卸载(2)
    Docker_简介(1)
    Jenkins_创建git任务(3)
  • 原文地址:https://www.cnblogs.com/ck1020/p/7424922.html
Copyright © 2020-2023  润新知