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?