深度探索Linux系统虚拟化:原理与实现
上QQ阅读APP看书,第一时间看更新

1.4.1 MP Table

操作系统有两种获取处理器信息的方式:一种是Intel的MultiProcessor Specification(后续简称MP Spec)约定的方式;另外一种是ACPI MADT(Multiple APIC Description Table)约定的方式。MP Spec约定的核心数据结构包括两部分:MP Floating Pointer Structure(后续简称MPF)和MP Configuration Table(后续简称MP Table)的地址,如图1-6所示。

图1-6 MP Configuration数据结构

处理器的信息记录于MP Table,MP Table包含多个entry,entry分为不同的类型,有的entry是描述处理器信息的,有的entry是描述总线信息的,有的entry是描述中断信息的,等等。每个处理器类型的entry记录了一个处理器的信息。

而MP Table地址记录在MPF中,MP标准约定MPF可以存放在如下几个位置:

1)存放在BIOS扩展数据区的前1KB内。

2)系统基础内存最高的1KB内。比如对于640KB内存,那么MPF存放在639KB~640KB内。

3)存放在主板BIOS区域,即0xF0000~0xFFFFF之间。

操作系统启动时,将在MP Spec约定的位置搜索MPF。那么操作系统如何确定何处内存为MPF呢?根据MP Spec约定,MPF起始的4字节为_MP_。在定位了MPF后,操作系统就可以顺藤摸瓜,找到MP Table,从中获取处理器信息。

1.VMM准备处理器信息

kvmtool将MP Table放置在了主板BIOS所在的区域(0xF0000~0xFFFFF),在BIOS实际占据地址的末尾处。kvmtool首先申请了一块内存区,在其中组织MPF和MP Table,然后将组织好的数据结构复制到Guest中主板BIOS所在的区域,代码如下:


commit 0c7c14a747e9eb2c3cacef60fb74b0698c9d3adf
kvm tools: Add MP tables support
kvmtool.git/mptable.c
01 void mptable_setup(struct kvm *kvm, unsigned int ncpus)
02 {
03     unsigned long real_mpc_table, size;
04     struct mpf_intel *mpf_intel;
05     struct mpc_table *mpc_table;
06     struct mpc_cpu *mpc_cpu;
07     struct mpc_bus *mpc_bus;
08     …
09     void *last_addr;
10     …
11     real_mpc_table = ALIGN(MB_BIOS_BEGIN + bios_rom_size, 16);
12     …
13     mpc_table = calloc(1, MPTABLE_MAX_SIZE);
14     …
15     MPTABLE_STRNCPY(mpc_table->signature,   MPC_SIGNATURE);
16     MPTABLE_STRNCPY(mpc_table->oem,     MPTABLE_OEM);
17     …
18     mpc_cpu = (void *)&mpc_table[1];
19     for (i = 0; i < ncpus; i++) {
20         mpc_cpu->type       = MP_PROCESSOR;
21         mpc_cpu->apicid     = i;
22         …
23         mpc_cpu++;
24     }
25 
26     last_addr = (void *)mpc_cpu;
27     …
28     mpc_bus     = last_addr;
29     mpc_bus->type   = MP_BUS;
30     mpc_bus->busid  = pcibusid;
31     …
32     last_addr = (void *)&mpc_bus[1];
33     …
34     mpf_intel = (void *)ALIGN((unsigned long)last_addr, 16);
35     …
36     mpf_intel->physptr  = (unsigned int)real_mpc_table;
37     …
38     size = (unsigned long)mpf_intel + sizeof(*mpf_intel) – 
39             (unsigned long)mpc_table;
40     …
41     memcpy(guest_flat_to_host(kvm, real_mpc_table), 
42              mpc_table, size);
43     …
44 }

函数mptable_setup首先申请了一块临时的内存区,见第13行代码。然后开始组织结构体MP Table,其中第15~17行代码是组织header部分。

紧接在header后面的就是各种entry了,首先是处理器类型的entry,见第18~24行代码。MP Spec约定,在处理器类型的entry中,需要提供处理器对应的LAPIC的ID,作为发送核间中断时的目的地址。根据代码可见,0号CPU对应的LAPIC的ID为0,1号CPU对应的LAPIC的ID为1,以此类推。

在处理器之后,还有各种总线、中断等entry,见代码第28~33行,这里我们不一一讨论了。

在组织完MP Table后,函数mptable_setup开始组织MPF。MPF紧邻MP Table,见第36行代码,其中的字段physptr指向了MP Table。

最后,将组织好的MPF和MP Table复制到主板BIOS区域,见第41、42行代码。其中,real_mpc_table是Guest中指向主板BIOS占据地址的结尾,见第11行代码。这个地址是Guest的地址空间,因此如果kvmtool需要访问,需要调用函数guest_flat_to_host将其转换为对应的Host的地址,见第41行代码。复制的区域包括整个MP Table和MPF,见第38、39行代码。

2.Guest读取处理器信息

虚拟机启动后,将扫描MP Spec约定的存放MPF的位置:


linux-1.3.31/arch/i386/mm/init.c
unsigned long paging_init(unsigned long start_mem, …)
{
    …
    smp_scan_config(0x0,0x400); /* Scan the bottom 1K for … */
    …
    smp_scan_config(639*0x400,0x400); /* Scan the top 1K of …*/
    smp_scan_config(0xF0000,0x10000);   /* Scan the 64K … */
    …
}

操作系统如何确定某处内存存放的为MPF呢?根据MP Spec约定,MPF起始的4字节为_MP_,如图1-7所示。

图1-7 MPF格式

操作系统只要在MP Spec约定的几个区域内,以4字节为单位搜索到关键字_MP_,就可以认定这是结构体MPF。在下面的代码中,函数smp_scan_config以4字节为单位,地毯式匹配关键字_MP_,其中宏SMP_MAGIC_IDENT就是_MP_。当找到MPF后,如果其中指向MP Table的字段mpf_physptr非空,则调用函数smp_read_mpc遍历MP Table:


linux-1.3.31/arch/i386/kernel/smp.c
void smp_scan_config(unsigned long base, unsigned long length)
{
    unsigned long *bp=(unsigned long *)base;
    struct intel_mp_floating *mpf;
    …
    while(length>0)
    {
        if(*bp==SMP_MAGIC_IDENT)
        {
            mpf=(struct intel_mp_floating *)bp;
            if(mpf->mpf_length==1 &&
                !mpf_checksum((unsigned char *)bp,16) &&
                mpf->mpf_specification==1)
            {
                …
                if(mpf->mpf_physptr)
                    smp_read_mpc((void *)mpf->mpf_physptr);
                …
            }
        }
        bp+=4;
        length-=16;
    }
}

内核中定义了一个bitmask类型的变量cpu_present_map,比如发现了0号CPU,则设置变量cpu_present_map的第0位为1,之后根据cpu_present_map中标识的位启动对应的CPU。所以,函数smp_read_mpc的主要作用就是遍历MP Table,找出具体的CPU信息,将cpu_present_map中对应的位置位,记录下系统中有哪些处理器。

在前面kvmtool设置MP Table时,CPU entry中设置了LAPIC的ID,并且是从0开始的,0号CPU对应的LAPIC的ID为0,1号CPU对应的LAPIC的ID为1,所以,使用LAPIC的ID作为CPU的索引即可。函数smp_read_mpc中查找CPU的代码如下:


linux-1.3.31/arch/i386/kernel/smp.c
static int smp_read_mpc(struct mp_config_table *mpc)
{
    …
    while(count<mpc->mpc_length)
    {
        switch(*mpt)
        {
            case MP_PROCESSOR:
            {
                struct mpc_config_processor *m=
                    (struct mpc_config_processor *)mpt;
                if(m->mpc_cpuflag&CPU_ENABLED)
                {
                    …
                        cpu_present_map|=(1<<m->mpc_apicid);
                }
                mpt+=sizeof(*m);
                count+=sizeof(*m);
                break;
            }
            …
        }
    }
    …
}