Qemu/kernel混合模拟

研究之后发现,除了qemu模拟apic的操作之外,还有一种模拟方式是qemu/kernel协同模拟。这个确实搞的有点绕。

让我们对这种方式进行一下探索,并看看他们之间有什么区别。

继承关系

    TYPE_OBJECT
    +-----------------------------+
    |class_init                   | = object_class_init
    |                             |
    |instance_size                | = sizeof(Object)
    +-----------------------------+


    TYPE_DEVICE
    +-----------------------------+
    |class_size                   | = sizeof(DeviceClass)
    |class_init                   | = device_class_init
    |                             |
    |instance_size                | = sizeof(Object)
    |instance_init                | = device_initfn
    |instance_finalize            | = device_finalize
    |                             |
    |realize                      | = apic_common_realize
    +-----------------------------+

    APICCommonClass TYPE_APIC_COMMON "apic-common"
    +-----------------------------+
    |class_size                   | = sizeof(APICCommonClass)
    |class_init                   | = apic_common_class_init   
    |                             |
    |instance_size                | = sizeof(APICCommonState)
    |instance_init                | = apic_common_initfn       
    |                             |
    +-----------------------------+

    APICCommonClass TYPE_APIC "kvm-apic"                  
    +-----------------------------+                   
    |class_init                   | = kvm_apic_class_init
    |                             |                   
    |instance_size                | = sizeof(APICCommonState)
    |                             |                   
    |realize                      | = kvm_apic_realize
    +-----------------------------+

从继承关系上看,这个混合模拟的设备类型和纯qemu模拟的设备类型只在最后一个类型上有所差别。

那究竟差在哪里呢?对了,就是这个class_init函数做的操作不同。这个函数主要设置了APICCommonClass中的回调函数,其中就包括了realize函数。

好了,这里先卖一个关子,我们一会儿再过来看具体差别。

初始化

作为混合模拟,所以apic设备在内核和qemu中各有一部分。

qemu部分

总的来说,初始化的大致流程和qemu模拟的相似。因为从硬件角度上看,apic是和cpu绑定的。所以模拟硬件的生成也是要再vcpu创建时生成。

但是因为有kernel的参与,所以又略有不同。

x86_cpu_realizefn
    x86_cpu_apic_create
        apic_common_initfn
        set apic->id with cpu->apic_id
    qemu_init_vcpu
        qemu_kvm_start_vcpu
            qemu_kvm_cpu_thread_fn
                kvm_init_vcpu
                    kvm_get_vcpu
                        kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)vcpu_id)
    x86_cpu_apic_realize
        object_property_set_bool(cpu->apic_state, realized)
            apic_common_realize
                kvm_apic_realize
        // only one memory region installed
        memory_region_add_subregion_overlap(&apic->io_memory)

感觉这里实际上创建了两个模拟的apic设备:

  • acpi_common_initfn: 这里在Qemu中创建了APICCommonState结构体

  • KVM_CREATE_VCPU: 这里到内核里创建了自己的apic

不知道是不是可以改进一点点?

接着是注册MemoryRegion,这个过程和qemu模拟的设备是一样的,只是注册的函数不同。这样看来,对guest这个接口是一样的。

    APICCommonState
    +-----------------------------+       
    |io_memory                    |
    |    ops                      | = kvm_apic_io_ops
    |                             |                                                                            
    |apicbase                     | = APIC_DEFAULT_ADDRESS
    |                             |
    |isr/irr/tmr                  |
    |lvt                          |
    |sipi_vector                  |
    |                             |
    |cpu                          |
    |    (X86CPU *)               |
    |    +------------------------+
    |    |interrupt_request       |
    |    |                        |
    +----+------------------------+

因为继承关系上和qemu模拟的设备一样,所以实现上也是非常类似。其中有区别的一点是对应注册的函数改成了kvm_apic_io_ops。

kernel部分

因为有一(大)部分apic的模拟在内核中,所以在使用前也需要在内核中初始化好。

还记得上面那一段qemu中的初始化部分么?其中qemu_init_vcpu部分停在了kvm_vm_ioctl(KVM_CREATE_VCPU)这。从字面上看这里是创建vcpu,但是要记住vcpu和lapic是绑定在一起的。所以这里就是进入kernel初始化的入口。

 vmx_create_vcpu
   kvm_vcpu_init
      kvm_arch_vcpu_init
           kvm_create_lapic

可以看出,从函数调用的关系上这个还是相对简洁的。不过对应的数据结构就有点长了,毕竟这次大部分工作要在内核中完成。

    vcpu
    +----------------------------------+
    |arch                              |
    |   (struct kvm_vcpu_arch)         |
    |   +------------------------------+
    |   |apic_arb_prio                 |
    |   |                              |
    |   |apic  --+                     |
    +---+------------------------------+
                 |
                 |
    kvm_lapic    v
    +----------------------------------+
    |vcpu                              |
    |    (struct kvm_vcpu*)            |
    +----------------------------------+
    |dev                               |
    |    (struct kvm_io_device)        |
    |    +-----------------------------+
    |    |ops                          | = apic_mmio_ops
    |    |   (kvm_io_device_ops*)      |
    +----+-----------------------------+
    |base_address                      | = MSR_IA32_APICBASE_ENABLE
    |    (unsigned long)               |
    +----------------------------------+
    |lapic_timer                       |
    |    (struct kvm_timer)            |
    |    +-----------------------------+
    |    |pending                      |
    |    |period                       |
    |    |timer                        | = apic_timer_fn
    |    |                             |
    +----+-----------------------------+
    |divide_count                      |
    |    (u32)                         |
    |isr_count                         |
    |    (s16)                         |
    |highest_isr_cache                 |
    |    (int)                         |
    |pending_events                    |
    |    (unsigned long)               |
    |sipi_vector                       |
    |    (unsigned int)                |
    +----------------------------------+
    |sw_enabled                        |
    |irr_pending                       |
    |lvt0_in_nmi_mode                  |
    |    (bool)                        |
    +----------------------------------+
    |vapic_addr                        |
    |    (gpa_t)                       |
    |                                  |
    |regs                              | = address to one page
    |    (void*)                       |
    +----------------------------------+

在这个结构中有两点值得注意:

  • 对应了一个内存空间操作的回调函数: apic_mmio_ops

  • 分配了一页空间作为apic包含的寄存器:regs

发送中断

接下来我们来看看虚拟环境下如何向一个apic发送中断,或者应该讲如何做到操作一个apic来发送中断的。

又因为我们在内核和qemu中分别模拟了apic设备,所以在这里也是有两种情况。

就我的理解,原理或者说起因都是一样的。那就是都去写apic的寄存器。在qemu和kernel中分别对应了两个内存处理的ops

  • kvm_apic_io_ops

  • apic_mmio_ops

qemu部分

当guest通过kvm_apic_io_ops写寄存器时就会触发对应apic中断的流程。

kvm_apic_mem_write()
    kvm_send_msi()
        kvm_irqchip_send_msi(kvm_state, *msg)
            kvm_vm_ioctl(s, KVM_SIGNAL_MSI, &msi)
            route = kvm_lookup_msi_route(s, msg)
            kvm_set_irq(s, route->kroute.gsi, 1)
                kvm_vm_ioctl(s, s->irq_set_ioctl, $event)  KVM_IRQ_LINE/_STATUS

这个流程就相对简单,qemu直接通过ioctl把中断传递给了内核,让内核来进行处理。

再仔细和apic_mem_write做对比,kvm_apic_mem_write的流程简直简单到爆。那说明原来对apic内部寄存器操作的动作根本不会出来?

那意思是说通过qemu模拟的apic发送中断的只有ioapic了么?

kernel部分

到了内核部分就有点意思了。因为内核部分从来源上又分成两个部分。

  • 从qemu通过ioctl发送中断

  • 在内核中通过直接写lapic发送中断

因为第二中情况最后会调用到第一种情况,所以下面先展示在内核中如何接收到guest的一次lapic的请求并发送中断的。

     vcpu_mmio_write
       kvm_iodevice_write(vcpu, vcpu->arch.apic->dev)
       apic_mmio_in_range()
       apic_mmio_write()
           kvm_lapic_reg_write()
              APIC_ID
              APIC_TASKPRI
              APIC_EOI
              APIC_LDR
              APIC_DFR
              APIC_SPIV
              APIC_ICR
                 apic_send_ipi()
                     kvm_irq_delivery_to_apic()

内核在接收到这个mmio写操作时,如果判断这个是在apic的范围内,那么就会调用到我们之前注册的apic_mmio_ops的write函数apic_mmio_write()。

其中会向其他apic发送ipi中断的操作最后调用到了kvm_irq_delivery_to_apic。记住这个函数,一会儿我们还会看到。

接下来看内核中在收到了来自qemu的中断操作请求是如何响应的。

     kvm_send_userspace_msi(kvm, &msi)
        kvm_set_msi(KVM_USERSPACE_IRQ_SOURCE_ID)
           kvm_msi_route_invalid()
           kvm_set_msi_irq()
           kvm_irq_delivery_to_apic()

瞧,这里最后一个函数也是kvm_irq_delivery_to_apic,真是殊途同归啊。

既然我们现在找到了这个共同的函数,那就来看看接下来内核是怎么处理的。

     kvm_irq_delivery_to_apic()
        kvm_irq_delivery_to_apic_fast()
        kvm_vector_to_index()
        kvm_get_vcpu()
        kvm_apic_set_irq()
            __apic_accept_irq()
                APIC_DM_LOWEST
                    vcpu->arch.apic_arb_prio++
                APIC_DM_FIXED
                    kvm_lapic_set_vector
                    kvm_lapic_clear_vector
                    if (vcpu->arch.apicv_active)
                      kvm_x86_ops->deliver_posted_interrupt(vcpu, vector);
                    else
                      kvm_lapic_set_irr
                      kvm_make_request(KVM_REQ_EVENT, vcpu)
                      kvm_vcpu_kick()
                APIC_DM_REMRD
                    kvm_make_request(KVM_REQ_EVENT, vcpu)
                    kvm_vcpu_kick()
                APIC_DM_SMI
                    kvm_make_request(KVM_REQ_EVENT, vcpu)
                    kvm_vcpu_kick()
                APIC_DM_NMI
                    kvm_inject_nmi(vcpu)
                    kvm_vcpu_kick(vcpu)
                APIC_DM_INIT
                    apic->pending_events = (1UL << KVM_APIC_INIT);
                    kvm_make_request(KVM_REQ_EVENT, vcpu)
                    kvm_vcpu_kick()
                APIC_DM_STARTUP
                    apic->sipi_vector = vector;
                    kvm_make_request(KVM_REQ_EVENT, vcpu)
                    kvm_vcpu_kick()
                APIC_DM_EXTINT

终于看到最后了,这个的奥秘就隐藏在了__apic_accept_irq()中。针对不同的情况做不同的处理,但是大部分情况都做了这么两件事:

  • kvm_make_request(KVM_REQ_EVENT, vcpu)

  • kvm_vcpu_kick()

也就是设置了标示后,唤醒vcpu来处理中断。完美~

接收中断

这个时候vcpu被唤醒去处理中断,按照我的理解其实没有直接处理中断的函数,而是在vcpu再次进入guest时检测KVM_REQ_EVENT事件。

话不多书,直接看流程

     vcpu_enter_guest()
         kvm_check_request(KVM_REQ_EVENT, vcpu)
         kvm_apic_accept_events()
         inject_pending_event(vcpu, req_int_win)
             kvm_x86_ops->set_irq(vcpu) = vmx_inject_irq()
                vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr)

最后的最后,通过写vmcs中的VM_ENTRY_INTR_INFO_FIELD字段通知到guest。

该字段的详细信息在SDM vol3, 24.8.3 VM-Entry Controls for Event Injection中。

其中就包含了

  • 中断向量号

  • 中断类型

好了,我想到这里整个完整的流程就算是理清楚了。

Last updated