ARM Linux (S3C6410架构/2.6.35内核)的内存映射(三)
2016-06-22 来源:eefocus
在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