IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Xen的启动之内存相关实现

    Liu Yutao发表于 2015-07-30 21:36:00
    love 0

    上篇博客介绍了Xen的整体内存分布情况,这篇博文主要从Xen的启动入手,介绍Xen在启动的时候是如何初始化它的内存,包括如何分配内存区域,如何初始化页表,以及如何在不同阶段初始化不同的内存分配器等等。

    这篇博文借鉴了一些这篇文章的内容,不过主要介绍的是最新版本的Xen在64位机器下的内存相关实现。

    好,现在开始进入正题!

    Xen的启动

    在介绍Xen启动的内存实现之前,先大致介绍下Xen的启动的整个流程(参考这里):

    汇编部分:

    汇编部分代码都在xen-source/xen/arch/x86/boot/目录下

    head.S,这个是整个Xen的入口:ENTRY(start):

    1
    2
    3
    
    ENTRY(start)
            jmp     __start
    ...
    

    里面主要做了下面几件事:

    • 装入GDT (trampoline_gdt):
    GDT项 说明 段选择子
    1 ring0 code, 32-bit mode BOOT_CS32 (0x0008)
    2 ring0 code, 64-bit mode BOOT_CS64 (0x0010)
    3 ring0 data BOOT_DS (0x0018)
    4 real-mode code BOOT_PSEUDORM_CS (0x0020)
    5 real-mode data BOOT_PSEUDORM_DS (0x0028)
    • 获取Multiboot相关的信息,放置在某段内存空间,之后启动的时候会被用到;
    • 初始化BSS;
    • 初始化最早期的页表l3_bootmap和l2_bootmap,注意这个时候还没有开启分页功能。这个部分会在后面进行详细介绍;
    • 解析早期命令行参数;
    • 调整trampoline.S代码的内存位置,移动到 BOOT_TRAMPOLINE(0x8c00处);
    • 跳转到 trampoline_boot_cpu_entry。

    trampoline.S,主要工作为:

    • 进入实模式,读取内存,磁盘,视频信息;
    • 进入保护模式,将页表基地址idle_pg_table载入CR3,idle_pg_table会在之后进行详细的介绍;
    • 开启EFER(Extended Feature Enable Register);
    • 开启分页模式,同时将CR0中的PG, AM, WP, NE, ET, MP, PE位都设上;
    • 进入__high_start,即x86_64.S的代码。

    x86_64.S,主要工作为:

    • 重新载入GDT (gdt_descr):
    GDT项 说明 段选择子
    0xe001 ring0 code, 64-bit mode __HYPERVISOR_CS64 (0xe008)
    0xe002 ring0 data __HYPERVISOR_DS32 (0xe010)
    0xe003 reserved -
    0xe004 ring 3 code, compatibility FLAT_RING3_CS32 (0xe023)
    0xe005 ring 3 data FLAT_RING3_DS32 (0xe02b)
    0xe006 ring 3 code, 64-bit mode FLAT_RING3_CS64 (0xe033)
    0xe007 ring 0 code, compatibility __HYPERVISOR_CS32 (0xe038)
    0xe008~0xe009 TSS -
    0xe00a~0xe00b LDT -
    0xe00c per-cpu entry -
    • 装入堆栈指针:
    1
    2
    
    mov stack_start(%rip),%rsp
    or  $(STACK_SIZE-CPUINFO_sizeof),%rsp
    

    注意,Xen会通过or $(STACK_SIZE-CPUINFO_sizeof),%rsp方式在栈顶预留一个cpu_info结构,这个结构包含很多重要的成员:

    1. 客户系统的切换上下文 
    2. 当前运行的`vcpu`指针
    3. 物理处理器编号
    ...
    
    • 跳转到__start_xen。

    C部分:

    即xen-source/xen/arch/x86/setup.c文件中的__start_xen函数,主要逻辑如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    void __init __start_xen(multiboot_info_t *mbi) {
      // 注意,默认的情况下,参数 mbi 将从堆栈传递,这个值是前面汇编代码中的ebx值 
      // 初始化IDT table
      // 初始化系统相关table和描述符,包括TSS,GDT,LDT,TR等
      // 解析命令行
      // 初始化 console
      // 内存初始化,这些是这篇博文的重点
      // 其它的一些设备初始化
      // trap_init,初始化 IDT
      // CPU的初始化
      // 创建domaim-0,下一篇博文的重点
      // `domain_unpause_by_systemcontroller(dom0)`,调度domain-0 
      // `reset_stack_and_jump(init_done)`,Xen进入idle循环
    }
    

    内存初始化

    在介绍完Xen大致的的启动流程之后,我们就要开始来重点介绍和内存相关的具体实现了。以下的内容主要回答下面几个问题:

    • Xen是如何从实模式启动,然后进入保护模式,并开启分页模式的?
    • Xen的页表是如何建立的?即如何建立虚拟内存到物理内存的映射?
    • Xen在启动过程中是如何进行内存分配的?
    • 整个系统运行起来之后,Xen是如何管理自己的内存的?

    对于第一个问题,简单来说,Xen在启动的时候是处于实模式的,也就是可以直接访问物理内存,通过预先定义的ENTRY地址start开始执行最初的汇编代码。在开启分页机制之前,Xen会先初始化一段最基本的页表,即在页表中映射物理内存的0~16M地址空间,其中包括了Xen所需的最基本的代码和数据,然后通过设置CR0中的某些位开启分页机制。

    为了回答余下的三个问题,我们先来看看Xen页表的组织结构:

    xen/arch/x86/boot/x86_64.S
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    GLOBAL(__page_tables_start)
    
    ......
    
    /* Top-level master (and idle-domain) page directory. */
    GLOBAL(idle_pg_table)
            .quad sym_phys(l3_bootmap) + __PAGE_HYPERVISOR
            idx = 1
            .rept L4_PAGETABLE_ENTRIES - 1
            .if idx == l4_table_offset(DIRECTMAP_VIRT_START)
            .quad sym_phys(l3_identmap) + __PAGE_HYPERVISOR
            .elseif idx == l4_table_offset(XEN_VIRT_START)
            .quad sym_phys(l3_xenmap) + __PAGE_HYPERVISOR
            .else
            .quad 0
            .endif
            idx = idx + 1
            .endr
            .size idle_pg_table, . - idle_pg_table
    
    GLOBAL(__page_tables_end)
    

    其中idle_pg_table是页表的基地址,也就是第四级页表l4的地址,会在trampoline.S里,在开启分页机制之前被载入cr3:

    trampoline.S
    1
    2
    3
    4
    
            /* Load pagetable base register. */
            mov     $sym_phys(idle_pg_table),%eax
            add     bootsym_rel(trampoline_xen_phys_start,4,%eax)
            mov     %eax,%cr3
    

    可以看到,它的第0项是l3_bootmap:

    x86_64.S
    1
    2
    
    GLOBAL(idle_pg_table)
            .quad sym_phys(l3_bootmap) + __PAGE_HYPERVISOR
    

    l3_bootmap和l2_bootmap在head.S启动代码里面被初始化了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
            /* Initialise L2 boot-map page table entries (16MB). */
            mov     $sym_phys(l2_bootmap),%edx
            mov     $PAGE_HYPERVISOR|_PAGE_PSE,%eax
            mov     $8,%ecx
    1:      mov     %eax,(%edx)
            add     $8,%edx
            add     $(1<<L2_PAGETABLE_SHIFT),%eax
            loop    1b
            /* Initialise L3 boot-map page directory entry. */
            mov     $sym_phys(l2_bootmap)+__PAGE_HYPERVISOR,%eax
            mov     %eax,sym_phys(l3_bootmap) + 0*8
            /* Hook 4kB mappings of first 2MB of memory into L2. */
            mov     $sym_phys(l1_identmap)+__PAGE_HYPERVISOR,%edi
            mov     %edi,sym_phys(l2_xenmap)
            mov     %edi,sym_phys(l2_bootmap)
    

    可以看到,l3_bootmap的第0项是l2_bootmap的地址,而l2_bootmap第0项是l1_identmap,它会以4K的页的方式映射0~2M的物理内存地址空间,而1~7项则是以7个2M的大页映射了2~16M的物理内存。而在这0~16M的物理内存中,存放了Xen的代码、数据的信息,需要在启动的时候被用到,所以需要在最初始的阶段映射到虚拟地址空间中。

    继续看idle_page_table,如果idx为l4_table_offset(XEN_VIRT_START),即第261项,则在该项填上l3_xenmap的地址;如果idx为l4_table_offset(DIRECTMAP_VIRT_START),即第262项,则在该项上填上l3_identmap的地址,其余的地址在这个时候都被设置为0。

    接下来我们来看看l3_xenmap和l3_identmap分别是什么:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    GLOBAL(l2_xenmap)
            idx = 0
            .rept 8
            .quad sym_phys(__image_base__) + (idx << L2_PAGETABLE_SHIFT) + (PAGE_HYPERVISOR | _PAGE_PSE)
            idx = idx + 1
            .endr
            .fill L2_PAGETABLE_ENTRIES - 8, 8, 0
            .size l2_xenmap, . - l2_xenmap
    
    l3_xenmap:
            idx = 0
            .rept L3_PAGETABLE_ENTRIES
            .if idx == l3_table_offset(XEN_VIRT_START)
            .quad sym_phys(l2_xenmap) + __PAGE_HYPERVISOR
            .elseif idx == l3_table_offset(FIXADDR_TOP - 1)
            .quad sym_phys(l2_fixmap) + __PAGE_HYPERVISOR
            .else
            .quad 0
            .endif
            idx = idx + 1
            .endr
            .size l3_xenmap, . - l3_xenmap
    

    我们这里只需要关注这个l2_xenmap,也就是l3_xenmap的第322项(l3_table_offset(XEN_VIRT_START)),里面0~7项以2M大页的方式映射了__image_base__的内容:

    1
    2
    3
    
    #define sym_phys(sym)     ((sym) - __XEN_VIRT_START)
    
    .quad sym_phys(__image_base__) + (idx << L2_PAGETABLE_SHIFT) + (PAGE_HYPERVISOR | _PAGE_PSE)
    

    我们可以从xen/arch/x86/xen.lds.S文件中看到:

    1
    2
    3
    4
    5
    6
    
    SECTIONS
    {
      . = __XEN_VIRT_START;
      __image_base__ = .;
      ...
    }
    

    也就是说,__image_base__即为__XEN_VIRT_START的地址,所以,l2_xenmap映射的内存也是0~16M的物理内存。

    再来看l3_identmap,可以从下面的代码看出来,l3_identmap的0~3项映射了4个连续的第二级页表页,以l2_identmap作为起始地址。而l2_identmap和l2_bootmap一样,也是第0项映射了一个L1的页表页l1_identmap,第1~7项以7个2M大页的形式映射了2~16M的物理内存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    /* Mapping of first 16 megabytes of memory. */
    GLOBAL(l2_identmap)
            .quad sym_phys(l1_identmap) + __PAGE_HYPERVISOR
            pfn = 0
            .rept 7
            pfn = pfn + (1 << PAGETABLE_ORDER)
            .quad (pfn << PAGE_SHIFT) | PAGE_HYPERVISOR | _PAGE_PSE
            .endr
            .fill 4 * L2_PAGETABLE_ENTRIES - 8, 8, 0
            .size l2_identmap, . - l2_identmap
    
    GLOBAL(l3_identmap)
            idx = 0
            .rept 4
            .quad sym_phys(l2_identmap) + (idx << PAGE_SHIFT) + __PAGE_HYPERVISOR
            idx = idx + 1
            .endr
            .fill L3_PAGETABLE_ENTRIES - 4, 8, 0
            .size l3_identmap, . - l3_identmap
    

    最后我们来看l1_identmap,它位于xen/arch/x86/boot/head.S文件中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    /*
     * Mapping of first 2 megabytes of memory. This is mapped with 4kB mappings
     * to avoid type conflicts with fixed-range MTRRs covering the lowest megabyte
     * of physical memory. In any case the VGA hole should be mapped with type UC.
     */
    GLOBAL(l1_identmap)
            pfn = 0
            .rept L1_PAGETABLE_ENTRIES
            /* VGA hole (0xa0000-0xc0000) should be mapped UC. */
            .if pfn >= 0xa0 && pfn < 0xc0
            .long (pfn << PAGE_SHIFT) | PAGE_HYPERVISOR_NOCACHE | MAP_SMALL_PAGES
            .else
            .long (pfn << PAGE_SHIFT) | PAGE_HYPERVISOR | MAP_SMALL_PAGES
            .endif
            .long 0
            pfn = pfn + 1
            .endr
            .size l1_identmap, . - l1_identmap
    

    可以看到它其实就是以4K的正常页的方式映射了0~2M的物理内存地址空间。

    到此为止,我们可以画一张最基本的页表分布图:

    xen boot pagetable 1


    好了,到现在为止,我们介绍了在进入__start_xen之前,页表是如何初始化的,在这个过程中,0~16M(即Xen的代码和数据)的物理内存被映射在了三段虚拟内存空间中,它们分别是:

    • 0~16M的的虚拟地址空间;
    • XEN_VIRT_STAET ~ XEN_VIRT_START + 16M,即0xffff82d080000000~0xffff82d081000000的虚拟地址空间中;
    • DIRECTMAP_VIRT_START ~ DIRECTMAP_VIRT_START + 16M,即0xffff830000000000~0xffff830001000000的虚拟地址空间中

    接下来,在__start_xen的代码中将对这段虚拟内存空间进行一次调整,并且将其他物理内存映射到相应的虚拟内存空间中,同时在不同的阶段初始化不同的内存分配器。具体来说,它将完成以下几个内存相关的步骤:

    • 获取E820物理内存分布;
    • 将16M~4G的内存空间进行大页的映射,同时将0~16M的地址空间映射到更高的虚拟地址空间中;
    • 计算modules(kernel和initrd)的地址,并且预留内存空间给它们;
    • 遍历所有的物理内存,将它们映射到虚拟地址空间中,对于16M~4G的内存,会把原来的小页(4K page)也进行映射,并且通过init_boot_pages创建boot内存分配器,对于4G以上的内存,直接映射(1G,2M或者4K)的页;
    • 将modules的内存映射到Xen的虚拟地址空间中;
    • 初始化frametable;
    • end_boot_allocator,并且初始化堆分配器;
    • 创建页表中其它虚拟地址空间的映射;
    • 初始化Domain-0的内存

    除了最后一个步骤,其它步骤都会在接下来的部分进行介绍。

    获取E820物理内存分布

    什么是E820?其实就是BIOS的一个中断(具体来说是int 0x15),在触发这个中断时如果EAX是0xe820,那么BIOS就能返回系统的物理内存布局。由于系统内存会有很多段,而且每段的类型属性也不一样,所以我们得到的E820内存分布也被分成了很多个不同类型的内存段。

    在Xen的__start_xen代码里,会通过下列代码来打印出当前系统物理内存的分段情况:

    1
    2
    3
    4
    5
    
        /* Sanitise the raw E820 map to produce a final clean version. */
        max_page = raw_max_page = init_e820(memmap_type, e820_raw, &e820_raw_nr);
    
        /* Create a temporary copy of the E820 map. */
        memcpy(&boot_e820, &e820, sizeof(e820));
    

    通过命令:

    $ xl dmesg
    

    可以看到系统E820的分布情况,比如我的E820是这样的:

    xen e820

    这是我在自己计算机上启动Xen所得到的数据,其中usable的区间就是实际被映射到物理内存上的地址空间,可以看到在我的例子中有七个可用的物理地址区间,大约32GB:

    0000000000000000 - 0000000000058000 (usable) ~352K
    0000000000059000 - 00000000000a0000 (usable) ~156K
    0000000000100000 - 00000000a63d9000 (usable) ~2659M
    00000000a63e0000 - 00000000a7404000 (usable) ~4M
    00000000a7961000 - 00000000b9f97000 (usable) ~295M
    00000000bafff000 - 00000000bb000000 (usable) ~4K
    0000000100000000 - 000000083f600000 (usable) ~29686M
    

    其它几个选项代表不同的意思,如下所示(参考这里):

    • Usable:已经被映射到物理内存的物理地址;
    • Reserved:这些区间是没有被映射到任何地方,不能当作RAM来使用,但是kernel可以决定将这些区间映射到其他地方,比如PCI设备。通过检查/proc/iomem这个虚拟文件,就可以知道这些reserved的空间,是如何进一步分配给不同的设备来使用了。
    • ACPI data:映射到用来存放ACPI数据的RAM空间,操作系统应该将ACPI Table读入到这个区间内。
    • ACPI NVS:映射到用来存放ACPI数据的非易失性存储空间,操作系统不能使用。
    • Unusable:表示检测到发生错误的物理内存。这个在上面例子里没有,因为比较少见。

    得到这些物理内存的分布之后,我们就需要将可用(usable)的那些映射到对应的虚拟地址空间中了。

    第一轮映射

    在第一轮映射中,主要是将16M~4G的物理内存以大页(2M page)的形式映射到对应的虚拟地址空间,同时将0~16M的地址空间从原来的0~16M的虚拟地址空间映射到更高的虚拟地址空间中,这是由于一些兼容性的原因,因为大部分系统0~16M虚拟地址空间是有其它作用的。

    这一段代码非常复杂,这里就不详细说明了,主要是要注意以下几点:

    • 这里有两个阈值:BOOTSTRAP_MAP_BASE和limit:
    1
    2
    
    #define BOOTSTRAP_MAP_BASE (16UL << 20)
    limit = ARRAY_SIZE(l2_identmap) << L2_PAGETABLE_SHIFT;
    

    它们最后的取值分别是16M和4G。当物理内存地址位于16M~4G时,且是usable的时候,会通过map_pages_to_xen函数将它们map到对应的虚拟内存中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
            s = max_t(uint64_t, s, BOOTSTRAP_MAP_BASE);
            if ( (boot_e820.map[i].type != E820_RAM) || (s >= e) )
                continue;
    
            if ( s < limit )
            {
                end = min(e, limit);
                set_pdx_range(s >> PAGE_SHIFT, end >> PAGE_SHIFT);
                map_pages_to_xen((unsigned long)__va(s), s >> PAGE_SHIFT,
                                 (end - s) >> PAGE_SHIFT, PAGE_HYPERVISOR);
            }
    

    正如在上篇博客提到过的,这里__va(s)即将物理地址s映射在direct map的那一段内存中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    static inline void *__maddr_to_virt(unsigned long ma)
    {
        ASSERT(pfn_to_pdx(ma >> PAGE_SHIFT) < (DIRECTMAP_SIZE >> PAGE_SHIFT));
        return (void *)(DIRECTMAP_VIRT_START +
                        ((ma & ma_va_bottom_mask) |
                         ((ma & ma_top_mask) >> pfn_pdx_hole_shift)));
    }
    
    #define maddr_to_virt(ma)   __maddr_to_virt((unsigned long)(ma))
    
    #define __va(x)             (maddr_to_virt(x))
    

    所以所有16M~4G的物理内存s都被映射在了s+DIRECTMAP_VIRT_START的那段虚拟内存中,比如物理内存0x1000000(16M)被映射在了0xffff830001000000虚拟地址上。

    • 另外一点需要注意的是,在这个过程中,并不需要分配新的页表,而且在这个阶段并没有初始化任何内存分配器,所以也无法分配新的内存页来作为页表。那么它是如何做到这点的呢?

    如果对之前提到的页表有印象的话,应该还记得,我们在l3_identmap中创建了4个l2_identmap页表项:

    1
    2
    3
    4
    5
    6
    7
    8
    
    GLOBAL(l3_identmap)
            idx = 0
            .rept 4
            .quad sym_phys(l2_identmap) + (idx << PAGE_SHIFT) + __PAGE_HYPERVISOR
            idx = idx + 1
            .endr
            .fill L3_PAGETABLE_ENTRIES - 4, 8, 0
            .size l3_identmap, . - l3_identmap
    

    其中只有第一个l2_identmap的前8项(0~16M)被初始化了,而其余的并没有初始化。一个常识是,每一个L3页表项代表了1个G的内存,那么我们有4个L3的页表项,那么就代表了4G的内存!而且所有这些页表项指向的L2的页表也已经存在了,所以说不需要重新分配新的页表页,就能够处理4G以内的所有以大页形式描述的内存了。

    • 最后一个问题,也就是如何将0~16M的物理内存映射到更高的虚拟地址空间中?

    在这里我就不具体说它是如何重映射的了,无非就是找到一个连续的地址空间,然后将内存从0~16M拷贝到新的地址空间,并且将原来的page table entries的内容拷贝到新的page table entries。这里来说下它是如何选择这块新的虚拟地址空间的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    #define reloc_size ((__pa(&_end) + mask) & ~mask)
            /* Is the region suitable for relocating Xen? */
            if ( !xen_phys_start && e <= limit )
            {
                /* Don't overlap with modules. */
                end = consider_modules(s, e, reloc_size + mask,
                                       mod, mbi->mods_count, -1);
                end &= ~mask;
            }
            else
                end = 0;
            if ( end > s )
            {
                l4_pgentry_t *pl4e;
                l3_pgentry_t *pl3e;
                l2_pgentry_t *pl2e;
                uint64_t load_start;
                int i, j, k;
    
                /* Select relocation address. */
                e = end - reloc_size;
                xen_phys_start = e;
    
                load_start = (unsigned long)_start - XEN_VIRT_START;
                barrier();
                move_memory(e + load_start, load_start, _end - _start, 1);
                ...
                // update page tables and reload cr3 to invalidate TLB.
            }
            ...
    

    这里有几个重要的变量:end,reloc_size。

    对于end:

    1
    2
    
                end = consider_modules(s, e, reloc_size + mask,
                                       mod, mbi->mods_count, -1);
    

    我们暂且不需要知道consider_modules是如何计算的,这个步骤主要是要得出在0~4G的物理地址空间中,最大的那个usable的,并且是2M对其的那个地址。从E820可以看出,4G之内最大的那个地址段是00000000a7961000 - 00000000b9f97000,而里面最大的2M对齐的地址即为0xb9e00000。

    而对于reloc_size:

    1
    
    #define reloc_size ((__pa(&_end) + mask) & ~mask)
    

    其中_end是Xen的代码和数据段的结束的地址,它在xen.lds.S中定义,它表示的是xen的所有代码和数据的最大的内存地址。所以reloc_size即表示xen的代码和数据段所占的内存空间有多大(最后会被1M向上对齐)。在我的系统中,它是0x400000,即4M的大小。

    所以,这段需要被relocate的地址xen_phys_start即为end - reloc_size,所以说,Xen的代码和数据段最后被重映射到了4G内存之内的最大的地址空间中。可以看到,Xen先通过move_memory将内容拷贝到高地址,然后再更新页表。

    第二轮映射

    在第二轮映射中,它会遍历所有的物理内存(包括小于16M和大于4G的内存),将它们映射到虚拟地址空间中。对于0~4G的内存,会把原来未映射的小页(4K page)也进行映射,而对于4G以上的内存,则直接映射(1G,2M或者4K)的页。并且它还通过init_boot_pages将所有可用的物理页加入数据结构bootmem_region_list中,建立boot内存分配器,用于在boot阶段分配内存。

    这是这段代码的主体部分。在这段代码中有好多个条件判断,主要作用就是将需要映射的物理内存的地址范围做一个划分,我们通过注释来说明这个过程。

    首先通过一张图来更好地解释其划分的依据:

    xen memory mapping helper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    
        for ( i = 0; i < boot_e820.nr_map; i++ )
        {
            uint64_t mask = PAGE_SIZE - 1; /* 最小对齐为4K页粒度 */
    
            s = (boot_e820.map[i].addr + mask) & ~mask; /* 起始地址4K向上对齐 */
            e = (boot_e820.map[i].addr + boot_e820.map[i].size) & ~mask; /* 尾地址4K向下对齐 */
            s = max_t(uint64_t, s, 1<<20); /* 1M地址以内的不进行考虑 */
            if ( (boot_e820.map[i].type != E820_RAM) || (s >= e) ) /* 只考虑usable的内存 */
                continue;
    
            set_pdx_range(s >> PAGE_SHIFT, e >> PAGE_SHIFT); /* 设置pdx_range,之后具体介绍 */
    
            /* 以下开始对内存范围进行划分,划分的两个重要依据是图中的A点(16M)和B点(4G) */
            map_s = max_t(uint64_t, s, BOOTSTRAP_MAP_BASE);
            map_e = min_t(uint64_t, e,
                          ARRAY_SIZE(l2_identmap) << L2_PAGETABLE_SHIFT);
    
            init_boot_pages(s, min(map_s, e)); /* 先将A点以下的内存区域加入boot分配器 */
            s = map_s;
            if ( s < map_e ) /* 将A点到e或者B点(如果e大于B点)的大页内存区域加入boot分配器 */
            {
                uint64_t mask = (1UL << L2_PAGETABLE_SHIFT) - 1;
    
                map_s = (s + mask) & ~mask; /* 首地址2M向上对齐 */
                map_e &= ~mask; /* 尾地址2M向下对齐 */
                init_boot_pages(map_s, map_e);
            }
    
            if ( map_s > map_e ) /* 如果内存范围在A点之内,则这个过程结束 */
                map_s = map_e = s;
    
            if ( map_e < e )  /* 如果e位在B点之上 */
            {
                uint64_t limit = __pa(HYPERVISOR_VIRT_END - 1) + 1;
                uint64_t end = min(e, limit);
    
                if ( map_e < end ) /* 先映射B点到HYPERVISOR_VIRT_END的地址空间,并且将其加入boot内存分配器 */
                {
                    map_pages_to_xen((unsigned long)__va(map_e), PFN_DOWN(map_e),
                                     PFN_DOWN(end - map_e), PAGE_HYPERVISOR);
                    init_boot_pages(map_e, end);
                    map_e = end;
                }
            }
            if ( map_e < e ) /* 映射HYPERVISOR_VIRT_END到e的地址空间 */
            {
                /* This range must not be passed to the boot allocator and
                 * must also not be mapped with _PAGE_GLOBAL. */
                map_pages_to_xen((unsigned long)__va(map_e), PFN_DOWN(map_e),
                                 PFN_DOWN(e - map_e), __PAGE_HYPERVISOR);
            }
            if ( s < map_s ) /* 将A点到B点的4K页内存区域进行映射,并且加入boot分配器 */
            {
                map_pages_to_xen((unsigned long)__va(s), s >> PAGE_SHIFT,
                                 (map_s - s) >> PAGE_SHIFT, PAGE_HYPERVISOR);
                init_boot_pages(s, map_s);
            }
        }
    

    所以这就是划分不同的内存段进行映射,并且将可用的内存加入boot分配器。由于0~4G的内存区域中的2M的大页已经在之前被映射了,所以在这个阶段主要就是把它们加入boot分配器,同时映射4G以上的内存区域。

    下面我们来重点分析3个函数:

    • set_pdx_range
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    #define PDX_GROUP_SHIFT L2_PAGETABLE_SHIFT
    
    #define PDX_GROUP_COUNT ((1 << PDX_GROUP_SHIFT) / \
                             (sizeof(*frame_table) & -sizeof(*frame_table)))
    
    void set_pdx_range(unsigned long smfn, unsigned long emfn)
    {
        unsigned long idx, eidx;
    
        idx = pfn_to_pdx(smfn) / PDX_GROUP_COUNT;
        eidx = (pfn_to_pdx(emfn - 1) + PDX_GROUP_COUNT) / PDX_GROUP_COUNT;
    
        for ( ; idx < eidx; ++idx )
            __set_bit(idx, pdx_group_valid);
    }
    

    这是啥意思呢?比较好理解的是,PDX_GROUP_COUNT表示的是在一个L2大页(2M)的内存中可以装多少个frame_table数据结构。这里有一个比较tricky的地方,就是这个(sizeof(*frame_table) & -sizeof(*frame_table))。这里顺便普及一个知识:

    “x & -x” means that the greatest power of 2 that is a factor of x.

    在我们这里,sizeof(*frame_table)的值是0x20,所以PDX_GROUP_COUNT即为0x10000(64K)。

    另外如果跟进代码的话会发现,pfn_to_pdx(pfn)其实就是pfn,所以set_pdx_range即找到smfn到emfn所对应的那些2M的大页,并且把pdx_group_valid中对应的bit给设上,表示说这些内存空间对应的pdx是valid的,这在之后有用。

    • init_boot_pages
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    static void __init bootmem_region_add(unsigned long s, unsigned long e)
    {
        unsigned int i;
    
        if ( (bootmem_region_list == NULL) && (s < e) )
            bootmem_region_list = mfn_to_virt(s++);
    
        if ( s >= e )
            return;
    
        for ( i = 0; i < nr_bootmem_regions; i++ )
            if ( s < bootmem_region_list[i].e )
                break;
    
        memmove(&bootmem_region_list[i+1], &bootmem_region_list[i],
                (nr_bootmem_regions - i) * sizeof(*bootmem_region_list));
        bootmem_region_list[i] = (struct bootmem_region) { s, e };
        nr_bootmem_regions++;
    }
    
    void __init init_boot_pages(paddr_t ps, paddr_t pe)
    {
        ps = round_pgup(ps);
        pe = round_pgdown(pe);
        if ( pe <= ps )
            return;
    
        bootmem_region_add(ps >> PAGE_SHIFT, pe >> PAGE_SHIFT);
    
        ...
    }
    

    这段代码非常简单,就是将ps到pe的内存空间给加到bootmem_region_list中,之后进行分配内存的时候会被用到。

    • map_pages_to_xen

    这个函数特别复杂,也是Xen里面非常重要的一个函数:

    1
    2
    3
    4
    5
    6
    7
    8
    
    int map_pages_to_xen(
      unsigned long virt,
      unsigned long mfn,
      unsigned long nr_mfns,
      unsigned int flags)
    {
      ......
    }
    

    这里就不展开来说,它所做的就是将以virt开头的nr_mfns个页映射到mfn的物理地址空间中。它的做法就是从idle_page_table开始走页表,最后凑齐所有的龙珠,哦不,所有的页表,然后在最后一级页表对应的页表项上写上mfn及其相应的flags。需要注意的是,它会根据你的虚拟地址来判断是否要用大页,以及用多大的大页(1G or 2M)。当然,如果需要新建一个页表,在boot阶段会通过alloc_boot_pages(nr_pfns, pfn_align)来分配对应的内存页:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    unsigned long __init alloc_boot_pages(
        unsigned long nr_pfns, unsigned long pfn_align)
    {
        for ( i = nr_bootmem_regions - 1; i >= 0; i-- )
        {
            struct bootmem_region *r = &bootmem_region_list[i];
            pg = (r->e - nr_pfns) & ~(pfn_align - 1);
            if ( pg < r->s )
                continue;
    
            _e = r->e;
            r->e = pg;
            bootmem_region_add(pg + nr_pfns, _e);
            return pg;
        }
    
        BOOT_BUG_ON(1);
        return 0;
    }
    

    其实也就是从我们之前通过init_boot_pages加到bootmem_region_list的内存来获取相应的内存页。这里就不详述了。

    modules(kernel,initrd)的内存映射

    这个步骤非常简单,就是运行了下面这段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    
        for ( i = 0; i < mbi->mods_count; ++i )
        {
            set_pdx_range(mod[i].mod_start,
                          mod[i].mod_start + PFN_UP(mod[i].mod_end));
            map_pages_to_xen((unsigned long)mfn_to_virt(mod[i].mod_start),
                             mod[i].mod_start,
                             PFN_UP(mod[i].mod_end), PAGE_HYPERVISOR);
        }
    

    这些函数之前都介绍过了,这里就不具体讲了。一般情况下,系统中会有两个modules,一个是kernel,还有一个是initrd,所以这个循环会进行两次,每次将不同的module映射到对应的地址空间。

    page_info数据结构列表FrameTable的初始化

    这个过程对应的函数是init_frametable:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    static void __init init_frametable_chunk(void *start, void *end)
    {
        unsigned long s = (unsigned long)start;
        unsigned long e = (unsigned long)end;
    
        for ( ; s < e; s += step << PAGE_SHIFT )
        {
            step = 1UL << (cpu_has_page1gb &&
                           !(s & ((1UL << L3_PAGETABLE_SHIFT) - 1)) ?
                           L3_PAGETABLE_SHIFT - PAGE_SHIFT :
                           L2_PAGETABLE_SHIFT - PAGE_SHIFT);
    
            while ( step && s + (step << PAGE_SHIFT) > e + (4 << PAGE_SHIFT) )
                step >>= PAGETABLE_ORDER;
            do {
                mfn = alloc_boot_pages(step, step);
            } while ( !mfn && (step >>= PAGETABLE_ORDER) );
            if ( !mfn )
                panic("Not enough memory for frame table");
            map_pages_to_xen(s, mfn, step, PAGE_HYPERVISOR);
        }
    
        memset(start, 0, end - start);
        memset(end, -1, s - e);
    }
    
    void __init init_frametable(void)
    {
        unsigned int max_idx = (max_pdx + PDX_GROUP_COUNT - 1) / PDX_GROUP_COUNT;
    
        for ( sidx = 0; ; sidx = nidx )
        {
            eidx = find_next_zero_bit(pdx_group_valid, max_idx, sidx);
            nidx = find_next_bit(pdx_group_valid, max_idx, eidx);
            if ( nidx >= max_idx )
                break;
            init_frametable_chunk(pdx_to_page(sidx * PDX_GROUP_COUNT),
                                  pdx_to_page(eidx * PDX_GROUP_COUNT));
        }
    
        end_pg = pdx_to_page(max_pdx - 1) + 1;
        top_pg = mem_hotplug ? pdx_to_page(max_idx * PDX_GROUP_COUNT - 1) + 1
                             : end_pg;
        init_frametable_chunk(pdx_to_page(sidx * PDX_GROUP_COUNT), top_pg);
        memset(end_pg, -1, (unsigned long)top_pg - (unsigned long)end_pg);
    }
    

    在搞清楚这段代码之前,我们需要先有一个概念。frametable其实是位于0xffff82e000000000 - 0xffff82ffffffffff内存中的一堆struct page_info的数据结构,每一个page_info记录了一个物理页的相应的信息。另外,之前提到的那个pdx,全称为page descriptor index,在我的机器上,有大约32G的内存,也就是从0x0~0x83f600000。前面计算过PDX_GROUP_COUNT为0x10000,所以在我的机器中最大的pdx即为0x83f600000 << 12 << 16 = 84(向上对齐)。

    所以这段代码就是找到从0到84中所有在之前通过set_pdx_range设上的pdx,然后将其映射到对应的page_info的内存上。在我的机器上,0x0~0xc,以及0x10~0x84是在之前通过set_pdx_range设上的pdx,所以通过init_frametable_chunk将它们对应的page_info映射到frametable的虚拟地址空间中。

    • 堆分配器的初始化

    在前面的所有操作完成之后,__start_xen会调用end_boot_allocator():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    void __init end_boot_allocator(void)
    {
        ...
    
        for ( i = nr_bootmem_regions; i-- > 0; )
        {
            struct bootmem_region *r = &bootmem_region_list[i];
            if ( r->s < r->e )
                init_heap_pages(mfn_to_page(r->s), r->e - r->s);
        }
        init_heap_pages(virt_to_page(bootmem_region_list), 1);
    
        ...
    }
    

    它会调用init_heap_pages初始化堆分配器。

    堆分配器是Xen的主内存分配器,这是一个和Linux的内存分配器类似的分配器。这里就不对其进行介绍了,反正之后的内存分配都是依靠这个堆分配器了。

    • 其它虚拟地址空间在页表中的映射

    到此为止,变量system_state已经从SYS_STATE_early_boot变成了SYS_STATE_boot。之后所有的内存分配也从boot allocator变成了alloc_xenheap_page()或者alloc_domheap_page()。另外Xen已经将所有的物理内存都映射到了DIRECTMAP_VIRT_START到DIRECTMAP_VIRT_END之间。

    接下来就是对虚拟地址空间中其它区域进行映射。在上篇博客里面提到在Xen的虚拟地址空间中还有好多其它区域,比如MPT,vmap等。这些区域也需要在xen启动的时候进行初始化,主要通过下面两个函数:

    • vm_init():
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
     void __init vm_init(void)
     {
         unsigned int i, nr;
         unsigned long va;
    
         vm_base = (void *)VMAP_VIRT_START;
         vm_end = PFN_DOWN(arch_vmap_virt_end() - vm_base);
         vm_low = PFN_UP((vm_end + 7) / 8);
         nr = PFN_UP((vm_low + 7) / 8);
         vm_top = nr * PAGE_SIZE * 8;
    
         for ( i = 0, va = (unsigned long)vm_bitmap; i < nr; ++i, va += PAGE_SIZE )
         {
             struct page_info *pg = alloc_domheap_page(NULL, 0);
    
             map_pages_to_xen(va, page_to_mfn(pg), 1, PAGE_HYPERVISOR);
             clear_page((void *)va);
         }
         bitmap_fill(vm_bitmap, vm_low);
    
         /* Populate page tables for the bitmap if necessary. */
         map_pages_to_xen(va, 0, vm_low - nr, MAP_SMALL_PAGES);
     }
    

    vm_init()主要就是映射一部分物理内存到VMAP_VIRT_START开始的一段虚拟地址空间中。

    • paging_init()

    这个函数复杂很多,它主要用来map好几个不同的machine-to-physical table (MPT),这些MPT我目前为止还不太清楚用来做什么,之后慢慢补上,以及创建linear guest page table。这里就不详述了。

    到目前为止,Xen启动阶段内存的虚拟化就告一段落了,接下来就要进行CPU、设备的初始化,以及Domain-0的创建了。关于Domain-0的创建会在下一篇博文中进行介绍。



沪ICP备19023445号-2号
友情链接