这儿记载一下Linux内核做二级内存映射的进程,以中断向量表的映射进程为例。
在S3C6410架构下,Linux选用的是粗粒度小页内存管理方式,即内存段(section)的巨细为1M,而页(page)的巨细为4K。在榜首级内存映射中,每一个PGD项掩盖1M的内存区域;假如有二级内存映射的话,每一个PTE项掩盖4K的内存区域。
下面咱们来看一下二级内存映射表的规划。假如段的巨细是1M而页的巨细是4K的话,那么每一张二级映射表即页表中就需求有1M/4K=256个表项。而不论是PGD仍是PTE,每一个表项的巨细是4字节,即一个长整形数的巨细。一张页表的巨细为256*4=1024/1K字节,所以,页表的巨细与页的巨细并不能对并,一张4K巨细的内存页能够存得下4张这样的页表。Linux选用了这样一种规划来寄存页表:(文件arch/arm/include/asm/pgalloc.h)
在一张4K巨细的内存页中,寄存了4张不同的页表,它们依次是:榜首张页表的ARM版别(也被叫做硬件版别),第二张(与榜首张表的虚拟空间是接连的)页表的ARM版别,榜首张页表的内核版别(也被叫做Linux版别),第二张页表的内核版别。同一张表的内核版别与ARM版别不是接连寄存,而是间离隔的。
页表为什么会有内核版别和硬件版别的区别呢?由于内核需求的一些信息(比方dirty、access等)在ARM需求的页表信息中没有,所以Linux需求别的一份满意自己需求的映射表。
或许正是由于页表巨细(1K)与页巨细(4K)的不匹配,也造成了内存映射核算方面的许多费事。直观地来了解,已然每一个一级页表项映射的内存空间是1M,那么在代码中一个一级页表项pgd_t的巨细就应该界说为4字节,PGDIR_SIZE应该界说为1M,但现实不是这样:
[c]#define PGDIR_SHIFT 21#define PGDIR_SIZE (1UL << PGDIR_SHIFT)typedef unsigned long pgd_t[2];[/c]
PGDIR_AIZE被界说为2M,而pgd_t被界说为8个字节。其实这两个PGD仍然是相互独立的,并没有任何相关。
这给了解和核算都带来了费事,但仅有的一条优点便是更好地处理了页表巨细与页巨细不匹配的问题。由于每两个相邻的页表是放在一同处理的,所以爽性把两个相邻的PGD也界说在一同,这样当其间的一个被映射时也要确保另一个得到映射。
下面看一个映射中断向量表的实践进程,经过调用栈paging_init()->devicemaps_init()->create_mapping()->alloc_init_section()->alloc_init_pte(),最终抵达了函数alloc_init_pte(),这段代码包含了我的注释和打印(以[Michael]开关):
[c]static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr,unsigned long end, unsigned long pfn,const struct mem_type *type){pte_t *pte;printk(MICHAEL_DBG "alloc_init_pte()\n");if (pmd_none(*pmd)) {pte = alloc_bootmem_low_pages(2 * PTRS_PER_PTE * sizeof(pte_t));printk(MICHAEL_DBG "pmd is still blank, pte = 0x%x\n", pte);printk(MICHAEL_DBG "will populate pmd\n");__pmd_populate(pmd, __pa(pte) | type->prot_l1);}pte = pte_offset_kernel(pmd, addr);do {void *linux_pte = (void *)pte;void *hw_pte = linux_pte - 2048;printk(MICHAEL_DBG "pmd has been populated, pte = 0x%x, pfn = 0x%x, pfn_pte = 0x%x\n", pte, pfn, pfn_pte(pfn, __pgprot(type->prot_pte)));printk(MICHAEL_DBG "before set_pte_ext(): hw_pte = 0x%x, *hw_pte = 0x%x, linux_pte = 0x%x, *linux_pte = 0x%x\n", hw_pte, *((unsigned int*)hw_pte), linux_pte, *((unsigned int *)linux_pte));set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);printk(MICHAEL_DBG "after set_pte_ext(): hw_pte = 0x%x, *hw_pte = 0x%x, linux_pte = 0x%x, *linux_pte = 0x%x\n", hw_pte, *((unsigned int*)hw_pte), linux_pte, *((unsigned int *)linux_pte));pfn ;} while (pte , addr = PAGE_SIZE, addr != end);}[/c]
先看前面一段(去掉了注释和打印):
[c]if (pmd_none(*pmd)) {pte = alloc_bootmem_low_pages(2 * PTRS_PER_PTE * sizeof(pte_t));__pmd_populate(pmd, __pa(pte) | type->prot_l1);}[/c]
先阐明一下,由于在S3C6410上,最多只支撑内存的二级映射即PGD->PTE->page,所以并不存在真实的PMD,即便当它出现时,它也与PGD相同。
这段代码查看一级映射项PGD是不是空,假如是空的话就阐明一级映射还没有树立过,(二级)页表不存在,所以就先经过boomem来请求一张页面做为页表,有了页表就能够填充PGD了,填充PGD的代码__pmd_populate()在《Arm-Linux二级页表的问题》一篇中现已讲过,不再赘述。
这一段履行完之后,页表有了,但页表仍是空的,下面要给指定的表项填充内容:
[c]pte = pte_offset_kernel(pmd, addr);do {set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0);pfn ;} while (pte , addr = PAGE_SIZE, addr != end);}[/c]
addr是需求映射的页面的物理地址,pte_offset_kernel(pmd, addr)核算出这个物理地址在页表中对应的方位,不过需求留意的是,这儿核算出的pte值指的是这个页面所对应的页表项在硬件页表中的方位。接下来调用set_pte_ext(),这是一个依硬件而不同的函数,比方在S3C6410上,它的完成是armv6_set_pte_ext(),是在arch/arm/mm/proc-macros.S文件顶用汇编代码完成的。set_pte_ext()的作用是一起填充硬件页表和内核页表。
看一下在skyeye模拟器上运转这个内核的log:
vectors = 0xc02aa000
init_mm.pgd = 0xc0004000, addr = 0xffff0000, pgd_index() = 0x7ff, PGDIR_SHIFT = 21
alloc_init_pte()
pmd is still blank, pte = 0xc02ab000
will populate pmd
__pmd_populate():
&pmdp[0] = 0xc0007ff8, pmdp[0] = 0x502ab021
&pmdp[1] = 0xc0007ffc, pmdp[1] = 0x502ab421
pmd has been populated, pte = 0xc02abfc0, pfn = 0x502aa, pfn_pte = 0x502aa34b
before set_pte_ext(): hw_pte = 0xc02ab7c0, *hw_pte = 0x0, linux_pte = 0xc02abfc0, *linux_pte = 0x0
after set_pte_ext(): hw_pte = 0xc02ab7c0, *hw_pte = 0x502aa02a, linux_pte = 0xc02abfc0, *linux_pte = 0x502aa34b
咱们经过bootmem请求到的中断向量表页的方位是0xc02aa000,这现已是一个虚拟地址,但咱们需求把它从头映射到指定地址0xffff0000去。在alloc_init_pte()中,首要承认PGD为空,所以请求一页内存做为页表,得到的页面是0xc02ab000,紧挨着中断向量表那一样。
接下来填充PGD。咱们要映射的的方针虚拟地址是0xffff0000,它在PGD表中的序号是0xffff0000/1M=0xfff,每个PGD占4字节,而PGD表的开端方位是0xc0004000,所以0xffff0000所对应的PGD的方位是0xc0004000 ((0xffff0000/1M) * 4) =
现在页表有了,下面要做的便是填充指定的页表项。方针虚拟地址0xffff0000在Linux页表中表项的地址是0xc02abfc0,这是由pte_offset_kernel(pmd, addr)核算出来的,然后调用set_pte_ext()写入页表项,汇编代码的细节这儿先不深究,只看写入的内容。最终两行打印分别是调用set_pte_ext()前后硬件页表和内核页表的内容,能够看到两张表里的内容都现已填好:
before set_pte_ext(): hw_pte = 0xc02ab7c0, *hw_pte = 0x0, linux_pte = 0xc02abfc0, *linux_pte = 0x0
after set_pte_ext(): hw_pte = 0xc02ab7c0, *hw_pte = 0x502aa02a, linux_pte = 0xc02abfc0, *linux_pte = 0x502aa34b