我会在接下来几篇博客里面介绍Xen的memory。这些都是我在看了各种资料,并且研究了Xen的代码之后的个人总结。之所以写这个系列,是感觉现在网络上没有什么比较具体介绍Xen内存的资料,这里有一个,但是是比较老的版本,而且主要介绍的是32位的系统。而我现在看的是Xen最新的版本(4.6),然后介绍的主要是64位的系统,希望能把自己花时间学到的东西分享出去。如果有什么错误的,或者解释的不清楚的地方,也希望各位指出来,共同讨论。
我应该会从五个方面来介绍Xen的内存:
现在我也还在研究中,所以这些应该会随着我自己学习的深入慢慢的进行整理。
好啦,不说废话了,直接进入这篇博客的主题:Xen内存布局的总体概览。
一般情况下我们讨论一个虚拟化系统中的内存,我们主要考虑的是客户虚拟机的内存机制,所以我们会提到shadow page table(影子页表),或者硬件内存虚拟化机制EPT(for Intel)/NPT(for AMD)等。然而这些其实都是针对于客户虚拟机来说的,也就是说对于客户虚拟机来说,它所看到的内存是怎么样的。但是如果我们从Xen本身的角度来考虑的话,Xen所看到的内存又是怎么样的呢?另外,对于整个系统来说,它的内存又是如何分布的呢?
首先我们要搞清楚几个概念:
第一,Xen有一个自己的页表,它是Xen的虚拟地址到物理地址的映射,这也是我们今天主要介绍的内容;
第二,客户虚拟机也有一个自己的页表,但是这个页表是关于客户虚拟机客户虚拟地址到客户物理地址的映射,Xen帮其维护了一个客户物理地址到机器物理地址的映射,这个会在之后的系列进行详细介绍;
第三,特权级虚拟机(即Domain-0)的内存管理和普通的客户虚拟机的内存管理不同。这也会在之后进行介绍。
好了,大致了解了上述几个概念,我们开始介绍Xen的虚拟内存布局。换句话说,在Xen的内存实现中,不同的虚拟地址范围对应的都是什么物理地址?在第一点里提到的Xen的那个页表到底是如何对虚拟内存进行映射的?
在Xen源码的xen/include/asm-x86/config.h
文件中描述了该映射:
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
| /*
* Memory layout:
* 0x0000000000000000 - 0x00007fffffffffff [128TB, 2^47 bytes, PML4:0-255]
* Guest-defined use (see below for compatibility mode guests).
* 0x0000800000000000 - 0xffff7fffffffffff [16EB]
* Inaccessible: current arch only supports 48-bit sign-extended VAs.
* 0xffff800000000000 - 0xffff803fffffffff [256GB, 2^38 bytes, PML4:256]
* Read-only machine-to-phys translation table (GUEST ACCESSIBLE).
* 0xffff804000000000 - 0xffff807fffffffff [256GB, 2^38 bytes, PML4:256]
* Reserved for future shared info with the guest OS (GUEST ACCESSIBLE).
* 0xffff808000000000 - 0xffff80ffffffffff [512GB, 2^39 bytes, PML4:257]
* ioremap for PCI mmconfig space
* 0xffff810000000000 - 0xffff817fffffffff [512GB, 2^39 bytes, PML4:258]
* Guest linear page table.
* 0xffff818000000000 - 0xffff81ffffffffff [512GB, 2^39 bytes, PML4:259]
* Shadow linear page table.
* 0xffff820000000000 - 0xffff827fffffffff [512GB, 2^39 bytes, PML4:260]
* Per-domain mappings (e.g., GDT, LDT).
* 0xffff828000000000 - 0xffff82bfffffffff [256GB, 2^38 bytes, PML4:261]
* Machine-to-phys translation table.
* 0xffff82c000000000 - 0xffff82cfffffffff [64GB, 2^36 bytes, PML4:261]
* vmap()/ioremap()/fixmap area.
* 0xffff82d000000000 - 0xffff82d03fffffff [1GB, 2^30 bytes, PML4:261]
* Compatibility machine-to-phys translation table.
* 0xffff82d040000000 - 0xffff82d07fffffff [1GB, 2^30 bytes, PML4:261]
* High read-only compatibility machine-to-phys translation table.
* 0xffff82d080000000 - 0xffff82d0bfffffff [1GB, 2^30 bytes, PML4:261]
* Xen text, static data, bss.
* 0xffff82d0c0000000 - 0xffff82dffbffffff [61GB - 64MB, PML4:261]
* Reserved for future use.
* 0xffff82dffc000000 - 0xffff82dfffffffff [64MB, 2^26 bytes, PML4:261]
* Super-page information array.
* 0xffff82e000000000 - 0xffff82ffffffffff [128GB, 2^37 bytes, PML4:261]
* Page-frame information array.
* 0xffff830000000000 - 0xffff87ffffffffff [5TB, 5*2^40 bytes, PML4:262-271]
* 1:1 direct mapping of all physical memory.
* 0xffff880000000000 - 0xffffffffffffffff [120TB, PML4:272-511]
* PV: Guest-defined use.
* 0xffff880000000000 - 0xffffff7fffffffff [119.5TB, PML4:272-510]
* HVM/idle: continuation of 1:1 mapping
* 0xffffff8000000000 - 0xffffffffffffffff [512GB, 2^39 bytes PML4:511]
* HVM/idle: unused
*
* Compatibility guest area layout:
* 0x0000000000000000 - 0x00000000f57fffff [3928MB, PML4:0]
* Guest-defined use.
* 0x00000000f5800000 - 0x00000000ffffffff [168MB, PML4:0]
* Read-only machine-to-phys translation table (GUEST ACCESSIBLE).
* 0x0000000100000000 - 0x0000007fffffffff [508GB, PML4:0]
* Unused.
* 0x0000008000000000 - 0x000000ffffffffff [512GB, 2^39 bytes, PML4:1]
* Hypercall argument translation area.
* 0x0000010000000000 - 0x00007fffffffffff [127TB, 2^46 bytes, PML4:2-255]
* Reserved for future use.
*/
|
这里有几个比较关键的地址空间(注:PML4:M-N表示该地址空间占用了第四级页表的第M到第N项):
-
0x0000000000000000 - 0x00007fffffffffff [128TB, PML4:0-255]
是为客户虚拟机准备的虚拟地址空间,这个会在之后的系列里面讨论,我现在也还不是很清楚它们是如何被使用的。 -
0xffff800000000000 - 0xffff803fffffffff [256GB, PML4:256]
是客户虚拟机只读的machine-to-physical table(MPT),MPT会在介绍客户虚拟机的时候进行介绍,现在只需要知道它是记录了HPA到GPA的映射的表。 -
0xffff804000000000 - 0xffff807fffffffff [256GB, PML4:256]
是Xen和虚拟机共享shared info信息的内存地址,shared info会在虚拟机启动的时候进行介绍,其主要是一些Xen需要让客户虚拟机启动时用到的一些信息。 -
0xffff810000000000 - 0xffff817fffffffff [512GB, PML4:258]
记录了每个客户虚拟机的page table,这个在客户虚拟机部分进行介绍,现在暂时还不清楚是用来干嘛的。 -
0xffff818000000000 - 0xffff81ffffffffff [512GB, PML4:259]
记录shadow page table的信息。 -
0xffff820000000000 - 0xffff827fffffffff [512GB, PML4:260]
这是记录每个虚拟机的(per-domain mapping)一些相关信息,包括一些GDT,LDT之类的,也是在之后客户虚拟机部分进行介绍。 -
0xffff828000000000 - 0xffff82bfffffffff [256GB, PML4:261]
这个也是MPT,但是客户虚拟机不可访问,现在我还不太清楚具体是做什么的,我猜它应该是P2M。 -
0xffff82c000000000 - 0xffff82cfffffffff [64GB, PML4:261]
用于vmap()/ioremap()/fixmap,现在还不清楚是做什么的。 -
0xffff82d080000000 - 0xffff82d0bfffffff [1GB, PML4:261]
这个最重要,映射了Xen的code和data,还包括bss之类的。这也是在Xen启动的时候会被最早映射到物理内存中的内容。 -
0xffff82dffc000000 - 0xffff82dfffffffff [64MB, PML4:261]
这个用于记录一些superpage相关的信息。 -
0xffff82e000000000 - 0xffff82ffffffffff [128GB, PML4:261]
这个也非常关键,Xen会为每个物理页都生成一个page frame的数据结构,里面记录了每个物理页相关的信息。 -
0xffff830000000000 - 0xffff87ffffffffff [5TB, PML4:262-271]
这个是一个一对一的直接映射,即每一个物理地址都会在这个地址空间中找到一个相应的虚拟地址的映射。在Xen的代码里面__va()
这个宏就是对应了这个地址空间中的某个地址。 -
0xffff880000000000 - 0xffffffffffffffff [120TB, PML4:272-511]
用于PV的客户虚拟机,这个之后介绍,现在也不清楚要如何用。 -
0xffff880000000000 - 0xffffff7fffffffff [119.5TB, PML4:272-510]
用于HVM客户虚拟机。
当然还有一些其它的,现在我也还暂时不清楚到底是用来做什么的,先在这里列出来吧:
-
0xffff808000000000 - 0xffff80ffffffffff [512GB, PML4:257]
用于ioremap for PCI mmconfig space。 -
0xffff82d000000000 - 0xffff82d03fffffff [1GB, PML4:261]
又是一个MPT,叫做Compatibility MPT,现在不清楚是做什么的。 -
0xffff82d040000000 - 0xffff82d07fffffff [1GB, PML4:261]
又是一个MPT,叫做High read-only compatibility MPT。
这里有几个点需要强调一下:
首先,上面所说的这个内存分布状况会被反映在Xen的页表中,同时某些信息也会被映射在虚拟机的内存空间中,至于有哪些我们在之后的系列慢慢介绍。
另外,所有虚拟内存到物理内存的映射都会在[PML4:262-271]
,也就是direct map那里反映出来,如果我们看__va()
的实现:
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))
|
其中DIRECTMAP_VIRT_START
就是direct map虚拟地址的首地址:0xffff830000000000
。
另外,对于page frame,也即前面提到的,Xen会为每一个物理页生成一个对应的page_info数据结构:
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
| struct page_info
{
union {
struct page_list_entry list;
paddr_t up;
struct page_sharing_info *sharing;
};
unsigned long count_info;
union {
struct {
unsigned long type_info;
} inuse;
struct {
unsigned long type:5; /* What kind of shadow is this? */
unsigned long pinned:1; /* Is the shadow pinned? */
unsigned long head:1; /* Is this the first page of the shadow? */
unsigned long count:25; /* Reference count */
} sh;
struct {
bool_t need_tlbflush;
} free;
} u;
union {
struct {
__pdx_t _domain;
} inuse;
struct {
__pdx_t back;
} sh;
struct {
unsigned int order;
} free;
} v;
union {
u32 tlbflush_timestamp;
struct {
u16 nr_validated_ptes;
s8 partial_pte;
};
u32 shadow_flags;
__pdx_t next_shadow;
};
};
|
其中主要记录了该页相关的一些信息,比如它是什么类型(normal page,或者是页表页,或者是free的)?被引用的次数(count_info)?以及和其它page之间的链表关系等。这些信息在相关page被回收或者分配的时候会被用到。
其它的内存映射的具体细节就需要在之后慢慢进行介绍了。
最后我们画一张图,来总体描述一下各个内存地址空间在整个页表中的分布情况:

Posted by Liu Yutao Jul 28th, 2015 System, Virtualization, Xen
上篇博客介绍了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项 | 说明 | 段选择子 |
---|
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项 | 说明 | 段选择子 |
---|
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. 物理处理器编号
...
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的物理内存地址空间。
到此为止,我们可以画一张最基本的页表分布图:

好了,到现在为止,我们介绍了在进入__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所得到的数据,其中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阶段分配内存。
这是这段代码的主体部分。在这段代码中有好多个条件判断,主要作用就是将需要映射的物理内存的地址范围做一个划分,我们通过注释来说明这个过程。
首先通过一张图来更好地解释其划分的依据:

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个函数:
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的,这在之后有用。
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
中,之后进行分配内存的时候会被用到。
这个函数特别复杂,也是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启动的时候进行初始化,主要通过下面两个函数:
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开始的一段虚拟地址空间中。
这个函数复杂很多,它主要用来map好几个不同的machine-to-physical table (MPT),这些MPT我目前为止还不太清楚用来做什么,之后慢慢补上,以及创建linear guest page table。这里就不详述了。
到目前为止,Xen启动阶段内存的虚拟化就告一段落了,接下来就要进行CPU、设备的初始化,以及Domain-0的创建了。关于Domain-0的创建会在下一篇博文中进行介绍。