Mctrain's Blog

What I learned in IT, as well as thought about life

Xen的启动之内存相关实现

| Comments

上篇博客介绍了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_bootmapl2_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_bootmapl2_bootmaphead.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,如果idxl4_table_offset(XEN_VIRT_START),即第261项,则在该项填上l3_xenmap的地址;如果idxl4_table_offset(DIRECTMAP_VIRT_START),即第262项,则在该项上填上l3_identmap的地址,其余的地址在这个时候都被设置为0。

接下来我们来看看l3_xenmapl3_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_identmapl2_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),在触发这个中断时如果EAX0xe820,那么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_BASElimit
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.
        }
        ...

这里有几个重要的变量:endreloc_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即找到smfnemfn所对应的那些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_COUNT0x10000,所以在我的机器中最大的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_STARTDIRECTMAP_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的创建会在下一篇博文中进行介绍。

Comments