# 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中。

其中就包含了

* 中断向量号
* 中断类型

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://richardweiyang-2.gitbook.io/understanding_qemu/00-advance_interrupt_controller/02-qemu_kernel_emulate.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
