虽然说纯软件模拟的方式在大部分的情况下已经被替代了,但是对软件模拟方式的理解能够加深对虚拟化的理解,并且对软硬件进化的过程有个概念。
继承关系
Copy 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 "apic"
+-----------------------------+
|class_init | = apic_class_init
| |
|instance_size | = sizeof(APICCommonState)
| |
|realize | = apic_realize
+-----------------------------+
这个继承层次也挺长的了,不过还好,没有cpu那个长。而且总的来说也还算蛮清晰的。
初始化
APIC的初始化和CPU还是有很大关联的,因为在硬件上他们两个人就是在一起的。所以qemu中APIC的创建也是随着CPU一起创建的。
为了说明问题,我们还是以x86 cpu为例。这个创建的过程就在x86_cpu_realizefn函数里。相关内容参见x86 cpu
接着我们就打开这个函数,看看究竟这个是怎么玩的。
Copy x86_cpu_realizefn
x86_cpu_apic_create
apic_common_initfn
set apic->id with cpu->apic_id
x86_cpu_apic_realize
object_property_set_bool(cpu->apic_state, realized)
apic_common_realize
apic_realize
// only one memory region installed
memory_region_add_subregion_overlap(&apic->io_memory)
实现
了解了初始化的流程,接下来我们看看APIC是怎么模拟的。
先来看看APICCommonState这个数据结构
Copy APICCommonState
+-----------------------------+
|io_memory |
| ops | = apic_io_ops
| |
|apicbase | = APIC_DEFAULT_ADDRESS
| |
|isr/irr/tmr |
|lvt |
|sipi_vector |
| |
|cpu |
| (X86CPU *) |
| +------------------------+
| |interrupt_request |
| | |
+----+------------------------+
首先是这个io_memory,这个就是在初始化时注册的内存空间。有意思的是不管有多少cpu,这个空间只有一个。而其中关键的就是对应的apic_io_ops了,具体的模拟手段都隐藏在这个操作中。
其次是apicbase,这就是系统默认的APIC访问的地址了。这没啥说的,就是按照手册来。
剩下的就是APIC的一些寄存器了,每次读写都会对这些寄存器访问。
发送中断
那现在我们就来看看对发送中断的模拟。
我们以IPI为例,在SDM中10.6小节描述了如何发送IPI。简单来说就是通过写ICR(Interrupt Command Register)来达到目的。
对应的在代码中,写APIC最后要走到apic_mem_write中0x30选项。
Copy case 0x30:
s->icr[0] = val;
apic_deliver(dev, (s->icr[1] >> 24) & 0xff, (s->icr[0] >> 11) & 1,
(s->icr[0] >> 8) & 7, (s->icr[0] & 0xff),
(s->icr[0] >> 15) & 1);
你看,是不是和手册上说的一样。写入时设置了ICR?
实际上msi的中断也是在apic_mem_write中执行的。貌似这两段内存空间是重合的?没搞懂,不过代码注释好像是这么写的。
Copy apic_mem_write
apic_send_msi()
apic_deliver_irq
apic_bus_deliver
apic_deliver
apic_startup
cpu_interrupt = generic_handle_interrupt
qemu_cpu_kick_thread
pthread_kill(cpu->thread->thread, SIG_IPI)
apic_bus_deliver
cpu_interrupt
apic_set_irq
apic_update_irq
cpu_interrupt
从这里看,所有的路径基本都会走到cpu_interrupt函数。而这个函数的工作是设置s->interrupt_request并发送一个信号给vcpu thread。
处理中断
处理中断和vcpu thread有很大关系。这里我们只看没有kvm介入时tcg的情况 qemu_tcg_rr_cpu_thread_fn。
Copy tcg_cpu_exec
cpu_exec
cpu_handle_interrupt
cpu_handle_exception()
cc->do_interrupt = x86_cpu_do_interrupt
do_interrupt_all
do_interrupt_protected
cpu_handle_interrupt()
cc->cpu_exec_interrupt = x86_cpu_exec_interrupt
当vcpu thread起来后,每个周期中会去检测有没有异常和中断。当检测到有的时候,则模拟中断的处理。
这个代码可是老复杂了,感觉能写这个代码的人简直是神。
不过从上面的分析中可以看出,软件模拟的情况下中断处理会有两个问题:
中断处理是在vcpu thread处理的间歇执行的,而并没有强制打断vcpu thread的正常执行。所以这个中断将会有较大的延时。
如果持续有中断好像会一直处理中断,所以中断太过频繁,也会导致系统无法继续。
以上是对用户态软件模拟的LAPIC的理解,希望是正确的。