纯Qemu模拟

虽然说纯软件模拟的方式在大部分的情况下已经被替代了,但是对软件模拟方式的理解能够加深对虚拟化的理解,并且对软硬件进化的过程有个概念。

继承关系

    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

接着我们就打开这个函数,看看究竟这个是怎么玩的。

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这个数据结构

    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选项。

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中执行的。貌似这两段内存空间是重合的?没搞懂,不过代码注释好像是这么写的。

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。

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的理解,希望是正确的。

Last updated