内核页表建立的两个阶段
对于理解这节需要先熟悉线性地址到物理地址的映射过程及X86平台的分页机制,可以参考《深入理解Linux内核》第二章(内存寻址)。
内核页表的建立有两个阶段:
- 第一阶段:内核映像刚装入内存后,CPU运行于实模式,此时分页功能没有开启。在开启分页功能之前,内核需要创建一个有限的地址空间,包括内核的代码段和数据段、初始页表和用于存放动态数据结构的共128KB大小的空间。我们把这个阶段成为临时内核页表。
- 第二阶段:内核充分利用剩余的RAM并适当的建立页表。
临时映射
由于Linux由BIOS加载后,起始阶段其实是运行在实模式,此时并没有开启分页机制。那Linux在开启分页机制之前需要先做哪些准备工作以支持分页机制?答案是页目录项及页表项,可以根据x86的硬件分页机制知道,此时的两级映射。
初始阶段内存的使用情况
一般来说,Linux内核安装在RAM中从物理地址0x0010 0000开始的地方,也就是说,从第二个MB开始。
为什么内核没有安装在RAM第一个MB开始的地方?因为PC体系结构有几个独特的地方必须考虑到。例如:
- 页框0由BIOS使用,存放加电自检(Power-On Self-Test, POST)期间检查到的系统硬件配置。
- 物理地址从0x000a 0000到0x000f ffff的范围通常留给BIOS例程,并且映ISA图形卡上的内部内存。
- 第一个MB内的其它页框可能由特定计算机模型保留
对于内存初始阶段的前3MB的内存布局大致如下图所示,这个图我们可以通过查看System.map文件得到:
临时内核页表建立
临时页表由startup_32()函数(定义于arch/i386/kernel/head.S)初始化的,由于在这个阶段没有激活PAE,所以只是两级映射:页全局目录和页表。页全局目录放在swapper_pg_dir变量中,页表放在pg0变量及之后的内存中。所以建立的页表需要满足的映射内存大小为:最后一个页表的地址+128K。
临时内核页表需要映射的线性地址有哪些?由于此阶段需要使得实模式和保护模式都能对物理地址0x0000 0000到 "最后一个页表位置 + 128k"都寻址到。所以映射关系如下:
线性地址(0x0000 0000 到 “最后一个页表位置 + 128k”)=> 物理地址(0x0000 0000 到 “最后一个页表位置 + 128k”)
线性地址(0xc000 0000 到 0xc000 0000 +“最后一个页表位置 + 128k” )=> 物理地址(0x0000 0000 到 “最后一个页表位置 + 128k”)。
页目录项及页表填充过程:
/* page_pde_offset/4 = 0x300,正好为页目录的0x300项(0xc000 0000的前10位)
* 0x0000 0000 与 0xC000 0000在页目录项正好偏移0x300项 */
page_pde_offset = (__PAGE_OFFSET >> 20);
/* 页表是放在pg0开始的内存处 */
movl $(pg0 - __PAGE_OFFSET), %edi
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */
10:
leal 0x007(%edi),%ecx /* Create PDE entry */
/* 填充线性地址0x0000 0000开始的目录项 */
movl %ecx,(%edx) /* Store identity PDE entry */
/* 填充线性地址0xc000 0000开始的目录项 */
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
addl $4,%edx
/* 一个页表包含1024项 */
movl $1024, %ecx
11:
/* 填充页表项,一个页表项映射4k物理内存,所以每次加0x1000 */
stosl
addl $0x1000,%eax
loop 11b
/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
/* INIT_MAP_BEYOND_END=128*1024, 确定建立的页表是否能够映射“最后一个页表地址” + 128K的内存
* 如果不行继续建立映射关系 */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl %ebp,%eax
jb 10b
启用分页单元
/*
* Enable paging
*/
movl $swapper_pg_dir-__PAGE_OFFSET,%eax
movl %eax,%cr3 /* set the page table pointer.. */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* ..and set paging (PG) bit */
Linux内核页表的最终映射
建立完临时映射大概8M的内存空间后,Linux内核就运行在保护模式了,此时就可以开始进行一些内核的基本初始化工作了。
x86的硬件分页
如果要理清Linux里面是怎么建立页表的映射,我们需要首先清楚硬件的分页机制是怎样的,因为我们建立的页目录和页表等最终是需要MMU来使用的。在这之前我们需要知道三个基本模式:
-
常规分页:
在常规分页中,32位的线性地址被分成3个域:
Directory(目录): 最高10位
Table(页表): 中间10位
Offset(偏移量): 最低12位
-
扩展分页(PSE):
在扩展分页模式下,页框的大小不再是常规分页下的4KB,而是4MB. 在该模式下就可以把大段连续的线性地址转换成相应的物理地址,并且映射过程不需要页表,从而节省了建立页表所占用的内存,并且TLB也更容易命中。
在这种模式下,32位线性地址分成两个字段进行物理地址的转换:
Directory: 最高10位
Offset: 其余22位 -
物理地址扩展(PAE)分页:
PAE的推出是因为由于物理内存(RAM)需求增加导致的,之前的物理内存受限于地址总线只有32根,支持的最大物理内存大小为4G,如果处理器支持PAE机制,地址总线就可以扩展到36根,从而支持的最大物理内存为64G. 但是线性地址仍然为32位,最大为4G.
在这种模式下又可以分为大尺寸页(2M)和常规尺寸页(4KB)两种模式:- 大尺寸页:
32位线性地址按如下方式解释:
PDPT: 由位30-31进行索引
页目录: 由位21-29进行索引
页框中的偏移(2M):由位0-20进行索引 - 常规尺寸页:
32位线性地址按如下方式解释:
PDPT: 由位30-31进行索引
页目录: 由位21-29进行索引
页框中的偏移(2M):由位0-20进行索引
- 大尺寸页:
Linux中的分页
由于Linux需要支持多平台的可移植性,所以Linux中定义了四级分页模型,四种页表分别为:
- 页全局目录(pgd)
- 页上级目录(pud)
- 页中间目录(pmd)
- 页表(pte)
虽然Linux定义四级分页模型,但是最终针对不同平台及模式时,还是需要根据硬件的分页模式去适配的:
- 常规分页模式:采用的是两级分页,用了pgd, pte
- 扩展分页模式:采用的是一级映射,只用了pgd
- 物理地址扩展分页模式:
- 大页模式(2M):采用的是两级分页,用了pgd, pmd
- 常规页模式(4KB):采用的是三级分页,用了pgd,pmd, pte
在这里我们不需要太纠结于Linux中页表之间的层次关系,比如觉得pgd一定要索引到pud,其实pud,pmd及pte只是Linux中对页表的一个抽象,只是一个命名而已,应该关注的是里面的内容,比如说常规分页模式下,pgd的目录项是初始化为pte的基地址及一些标志位,虽然没有pud,但是能找到下一级页表就行。下面我们从代码实现中就能看到:
函数的调用关系为:pagetable_init -> kernel_physical_mapping_init
pagtable_init():
static void __init pagetable_init (void)
{
unsigned long vaddr;
pgd_t *pgd_base = swapper_pg_dir;
#ifdef CONFIG_X86_PAE
int i;
/* Init entries of the first-level page table to the zero page */
/* 在扩展模式下,pgd的前三项映射线性地址空间为0-3G,这为用户空间访问的地址范围,所以用一个空页对它进行初始化 */
for (i = 0; i < PTRS_PER_PGD; i++)
set_pgd(pgd_base + i, __pgd(__pa(empty_zero_page) | _PAGE_PRESENT));
#endif
/* Enable PSE if available */
if (cpu_has_pse) {
set_in_cr4(X86_CR4_PSE);
}
/* Enable PGE if available */
if (cpu_has_pge) {
set_in_cr4(X86_CR4_PGE);
__PAGE_KERNEL |= _PAGE_GLOBAL;
__PAGE_KERNEL_EXEC |= _PAGE_GLOBAL;
}
kernel_physical_mapping_init(pgd_base);
remap_numa_kva();
/*
* Fixed mappings, only the page table structure has to be
* created - mappings will be set by set_fixmap():
*/
vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
page_table_range_init(vaddr, 0, pgd_base);
permanent_kmaps_init(pgd_base);
#ifdef CONFIG_X86_PAE
/*
* Add low memory identity-mappings - SMP needs it when
* starting up on an AP from real-mode. In the non-PAE
* case we already have these mappings through head.S.
* All user-space mappings are explicitly cleared after
* SMP startup.
*/
pgd_base[0] = pgd_base[USER_PTRS_PER_PGD];
#endif
}
页面的建立是在函数kernel_physical_mapping_init()中:
/*
* 常规分页模式:采用的是两级分页,用了pgd, pte
* 扩展分页模式:采用的是一级映射,只用了pgd
* 物理地址扩展分页模式:
* 大页模式(2M):采用的是两级分页,用了pgd, pmd
* 常规页模式(4KB):采用的是三级分页,用了pgd,pmd, pte */
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
{
unsigned long pfn;
pgd_t *pgd;
pmd_t *pmd;
pte_t *pte;
int pgd_idx, pmd_idx, pte_ofs;
pgd_idx = pgd_index(PAGE_OFFSET);
pgd = pgd_base + pgd_idx;
pfn = 0;
for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {
/* 1, 如果PAE没有打开,pmd=pgd
* 2, 如果PAE打开,就会分配内存给到pmd,并把pmd的地址放到pgd的表项中 */
pmd = one_md_table_init(pgd);
if (pfn >= max_low_pfn)
continue;
for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) {
unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;
/* Map with big pages if possible, otherwise create normal page tables. */
/* 1,当处于扩展分页(PSE)时就不会再有下一级的页表,而是直接把页框基地址放到pmd页表项中
* 1.1 如果不是物理地址扩展分页机制(PAE)模式,那PTRS_PER_PTE为1024,所以一页的大小为1024*4K = 4M
* 1.2 如果是PAE模式,那PTRS_PER_PTE为512,所以一页的大小为512*4K = 2M
* 2, 当不处于扩展分页(PSE)时,就需要多建立一级页表pte */
if (cpu_has_pse) {
unsigned int address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1;
if (is_kernel_text(address) || is_kernel_text(address2))
set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC));
else
set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE));
pfn += PTRS_PER_PTE;
} else {
pte = one_page_table_init(pmd);
for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {
if (is_kernel_text(address))
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));
else
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
}
}
}
}
}
当建立完内核最终的线性映射之后,线性地址0xc0000000 - 0xc0000000+896M, 对应的物理地址就为0x00000000-0x00000000+896M,这就是所谓的线性映射。