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的参与,所以又略有不同。

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

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

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

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

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

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

kernel部分

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

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

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

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

  • 对应了一个内存空间操作的回调函数: 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中断的流程。

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

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

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

kernel部分

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

  • 从qemu通过ioctl发送中断

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

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

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

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

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

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

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

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

  • kvm_make_request(KVM_REQ_EVENT, vcpu)

  • kvm_vcpu_kick()

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

接收中断

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

话不多书,直接看流程

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

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

其中就包含了

  • 中断向量号

  • 中断类型

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

Last updated

Was this helpful?