研究之后发现,除了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发送中断,或者应该讲如何做到操作一个apic来发送中断的。
又因为我们在内核和qemu中分别模拟了apic设备,所以在这里也是有两种情况。
就我的理解,原理或者说起因都是一样的。那就是都去写apic的寄存器。在qemu和kernel中分别对应了两个内存处理的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部分
到了内核部分就有点意思了。因为内核部分从来源上又分成两个部分。
因为第二中情况最后会调用到第一种情况,所以下面先展示在内核中如何接收到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)
也就是设置了标示后,唤醒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中。
其中就包含了
好了,我想到这里整个完整的流程就算是理清楚了。