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源码

posted @ 2017-09-04 19:25  jack.chen  Views(7071)  Comments(0Edit  收藏  举报

以马内利