6. 2.5页表的建立
前面已经建立了为内存页面管理所需的数据结构,现在是进一步完善页面映射机制,并且建立起内存页面映射管理机制的时候了,与此相关的主要函数有:
· paging_init() 函数
· pagetable_init() 函数
1.paging_init() 函数
这个函数仅被调用一次,即由setup_arch()调用以建立页表,对此函数的具体描述如下:
pagetable_init();
这个函数实际上才真正地建立页表,后面会给出详细描述.
__asm__( "movl %%ecx,%%cr3\n" ::"c"(__pa(swapper_pg_dir)));
因为pagetable_init()已经建立起页表,因此把swapper_pg_dir(页目录)的地址装入CR3寄存器。
#if CONFIG_X86_PAE
/*
* We will bail out later - printk doesnt work right now so
* the user would just see a hanging kernel.
*/
if (cpu_has_pae)
set_in_cr4(X86_CR4_PAE);
#endif
__flush_tlb_all();
上面这一句是个宏,它使得转换旁路缓冲区(TLB)无效。TLB总是要维持几个最新的虚地址到物理地址的转换。每当页目录改变时,TLB就需要被刷新。
#ifdef CONFIG_HIGHMEM
kmap_init();
#endif
如果使用了CONFIG_HIGHMEM选项, 就要对大于896MB的内存进行初始化,我们不准备对这部分内容进行详细讨论。
{
unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0};
unsigned int max_dma, high, low;
max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS)
>> PAGE_SHIFT;
低于16MB的内存只能用于DMA,因此,上面这条语句用于存放16MB的页面
low = max_low_pfn;
high = highend_pfn;
if (low < max_dma)
zones_size[ZONE_DMA] = low;
else {
zones_size[ZONE_DMA] = max_dma;
zones_size[ZONE_NORMAL] = low - max_dma;
#ifdef CONFIG_HIGHMEM
zones_size[ZONE_HIGHMEM] = high - low;
#endif
}
计算三个管理区的大小,并存放在zones_size数组中。三个管理区是:
ZONE_DMA:从0~16MB分配给这个区
ZONE_NORMAL:从16MB~896MB分配给这个区
ZONE_DMA :896MB以上分配给这个区
free_area_init(zones_size);
}
return;
这个函数用来初始化内存管理区并创建内存映射表,详细介绍参见后面内容。
2.pagetable_init()函数
这个函数真正地在页目录swapper_pg_dir中建立页表,描述如下:
unsigned long vaddr, end;
pgd_t *pgd, *pgd_base;
int i, j, k;
pmd_t *pmd;
pte_t *pte, *pte_base;
/*
* This can be zero as well - no problem, in that case we exit
* the loops anyway due to the PTRS_PER_* conditions.
*/
end = (unsigned long)__va(max_low_pfn*PAGE_SIZE);
计算max_low_pfn 的虚拟地址,并把它存放在end.中。
pgd_base = swapper_pg_dir;
让pgd_base (页目录基地址) 指向 swapper_pg_dir.
#if CONFIG_X86_PAE
for (i = 0; i < PTRS_PER_PGD; i++)
set_pgd(pgd_base + i, __pgd(1 + __pa(empty_zero_page)));
#endif
如果PAE 被激活, PTRS_PER_PGD就为4,且变量 swapper_pg_dir 用作页目录指针表,宏set_pgd() 定义于 include/asm-i386/pgtable-3level.h.中。
i = __pgd_offset(PAGE_OFFSET);
pgd = pgd_base + i;
宏__pgd_offset()在给定地址的页目录中检索相应的下标。因此__pgd_offset(PAGE_OFFSET)返回0x300 (或 十进制768 ),即内核地址空间开始处的下标。因此,pgd现在指向页目录表的第768项。
for (; i < PTRS_PER_PGD; pgd++, i++) {
vaddr = i*PGDIR_SIZE;
if (end && (vaddr >= end))
break;
如果使用了CONFIG_X86_PAE选项, PTRS_PER_PGD 就为4,否则,一般情况下它都为1024,即页目录的项数。PGDIR_SIZE给出一个单独的页目录项所能映射的RAM总量,在两级页目录中它为4MB,当使用CONFIG_X86_PAE选项时,它为1GB。计算虚地址vaddr,并检查它是否到了虚拟空间的顶部。
#if CONFIG_X86_PAE
pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
set_pgd(pgd, __pgd(__pa(pmd) + 0x1));
#else
pmd = (pmd_t *)pgd;
#endif
如果使用了CONFIG_X86_PAE选项,则分配一页(4K)的内存给bootmem分配器用,以保存中间页目录,并在总目录中设置它的地址。否则,没有中间页目录,就把中间页目录直接映射到总目录。
if (pmd != pmd_offset(pgd, 0))
BUG();
for (j = 0; j < PTRS_PER_PMD; pmd++, j++) {
vaddr = i*PGDIR_SIZE + j*PMD_SIZE;
if (end && (vaddr >= end))
break;
if (cpu_has_pse) {
unsigned long __pe;
set_in_cr4(X86_CR4_PSE);
boot_cpu_data.wp_works_ok = 1;
__pe = _KERNPG_TABLE + _PAGE_PSE + __pa(vaddr);
/* Make it "global" too if supported */
if (cpu_has_pge) {
set_in_cr4(X86_CR4_PGE);
__pe += _PAGE_GLOBAL;
}
set_pmd(pmd, __pmd(__pe));
continue;
}
现在,开始填充页目录(如果有PAE,就是填充中间页目录)。计算表项所映射的虚地址,如果没有激活PAE, PMD_SIZE大小就为0,因此,vaddr = i * 4MB。例如,表项0x300所映射的虚地址为0x300 * 4MB = 3GB。接下来,我们检查PSE(Page Size Extension)是否可用,如果是,就要避免使用页表而直接使用4MB的页。宏cpu_has_pse()用来检查处理器是否具有扩展页,如果有,则宏set_in_cr4()就启用它。
从PentiumII处理器开始,就可以有附加属性PGE (Page Global Enable)。当一个页被标记为全局的,且设置了PGE,那么,在任务切换发生或CR3被装入时,就不能使该页所在的页表(或页目录项)无效。这将提高系统性能,也是让内核处于3GB以上的原因之一。选择了所有属性后,设置中间页目录项。
pte_base = pte = (pte_t *)
alloc_bootmem_low_pages(PAGE_SIZE);
如果PSE不可用,就执行这一句,它为一个页表(4K)分配空间。
for (k = 0; k < PTRS_PER_PTE; pte++, k++) {
vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE;
if (end && (vaddr >= end))
break;
在一个页表中有1024个表项(如果启用PAE,就是512个),每个表项映射4K(1页)。
*pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL);
}
宏mk_pte_phys()创建一个页表项,这个页表项的物理地址为__pa(vaddr)。属性PAGE_KERNEL表示只有在内核态才能访问这一页表项。
set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base)));
if (pte_base != pte_offset(pmd, 0))
BUG();
}
}
通过调用set_pmd()把该页表追加到中间页目录中。这个过程一直继续,直到把所有的物理内存都映射到从PAGE_OFFSET开始的虚拟地址空间。
/*
* 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;
fixrange_init(vaddr, 0, pgd_base);
在内存的最高端(4GB - 128MB),有些虚地址直接用在内核资源的某些部分中,这些地址的映射定义在/include/asm/fixmap.h中,枚举类型__end_of_fixed_addresses用作索引,宏__fix_to_virt()返回给定索引的虚地址。函数fixrange_init()为这些虚地址创建合适的页表项。注意,这里仅仅创建了页表项,而没有进行映射。这些地址的映射是由set_fixmap()函数完成的。
#if CONFIG_HIGHMEM
/*
* Permanent kmaps:
*/
vaddr = PKMAP_BASE;
fixrange_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);
pgd = swapper_pg_dir + __pgd_offset(vaddr);
pmd = pmd_offset(pgd, vaddr);
pte = pte_offset(pmd, vaddr);
pkmap_page_table = pte;
#endif
如果使用了CONFIG_HIGHMEM选项,我们就可以访问896MB以上的物理内存,这些内存的地址被暂时映射到为此目的而保留的虚地址上。PKMAP_BASE 的值为0xFE000000(即4064MB),LAST_PKMAP 的值为 1024。因此,从4064MB开始,由fixrange_init()在页表中创建的表项能覆盖4MB的空间。接下来,把覆盖4MB内存的页表项赋给pkmap_page_table。
#if 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