上篇博客 介绍了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 1 b
/* 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的创建会在下一篇博文中进行介绍。