浅析linux内核内存管理之分页
硬件中的分页
硬件中的分页分常规分页和扩展分页:
常规分页,32位的线性地址被分为3个域:
- Directory(目录) 最高10位
- Table(页表) 中间10位
- Offset(偏移量) 最低12位
扩展分页,32位的线性地址被分为2个域:
- Directory 最高10位
- Offset 其余22位
扩展分页和正常分页的页目录项基本相同,除了:
- Page Size标志必须被设置
- 20位物理地址字段只有最高10位是有意义的。这是因为每一个物理地址都是在以4MB为边界的地方开始的,故这个地址的最低22位为0
页目录项:
页表项:
Linux中的分页
从linux 2.6.11开始采用了四级分页模型:
- 页全局目录(Page Global Directory) —— ——>PDT(PAE未开启)或PDPT(PAE开启)
- 页上级目录(Page Upper Directory)
- 页中间目录(Page Middle Directory) —— ——>PDT(PAE开启)
- 页表(Page Table) —— —— —— —— ——>PT
PTRS_PER_PTE,PTRS_PER_PMD,PTRS_PER_PUD以及PTRS_PER_PGD
用于计算页表,页中间目录,页上级目录和页全局目录表中表项的个数。当PAE被禁止时,它们产生的值分别为1024,1,1和1024。当PAE被激活时,产生的值分别为512,512,1和4。
注意:PAE关闭时,PGD就是PDT,此时不用PUD,PMD。虽然它们两个在线性地址中,但长度为0,2^0=1,也就是说,它们都是有一个数组元素的数组。当PAE启动时,PGD指示四个PDPT entry中的哪一个, 不使用PUD。
那么使用分页有什么好处呢?
就拿二级分页来说吧,如果不用二级分页,用一级,那么4GB需要4MB物理内存存放entry,一个页4KB,entry4B。如果用两级分页,2^10*2^10*4KB=4GB。所以需要两个页就行,每个页1024 entry。4KB+4KB=8KB。实际上,用不上8KB的,并不是所有entry都是存在的,正如下边说的那个函数,有的entry会为空,这样又节省很多空间。所以实际页表与页目录占用的空间小于8KB,多合算阿。
页表处理
pte_t,pmd_t,pud_t和pgd_t分别描述页表项,页中间目录项,页上级目录和页全局目录的格式。当PAE被激活的时他们都是64位的数据类型,否则都是32位数据类型。pgprot_t是另一个64位(PAE激活时)或32位(PAE禁用时)的数据类型,它表示与一个单独表项相关的保护标志。
五个类型转换宏(__pte,__pmd,__pud,__pgd和__pgprot)把一个无符号整数转换成所需的类型。另外的五个类型转换宏(pte_val,pmd_val,pud_val,pgd_val和pgprot_val)执行相反的转换,即把上面提到的四种特殊的类型转换成一个无符号整数。
内核还提供许多宏用于读取或修改页表项:
- 如果相应的表项值为0,那么,宏pte_none,pmd_none,pud_none和pgd_none产生的值为1,否则产生的值为0。
- 宏pte_clear,pmd_clear,pud_clear和pgd_clear清除相应页表一个表项,由此禁止进程使用由该页表项映射的线性地址。ptep_get_and_clear()函数清除一个页表项并返回前一个值。
- set_pte,set_pmd,set_pud和set_pgd向一个页表项中写入指定的值。
- 如果a和b两个表项页表项指向同一页并且指定相同的访问优先级,那么pte_same(a,b)返回1,否则返回0。
- 如果页中间目录指向一个大型页(2MB或4MB),那么pmd_large(e)返回1,否则返回0.
如果一个页表项的Present标志或者Page Size标志等于1,则pte_present宏产生的值为1,否则为0。前面讲过页表项的Page Size标志对微处理器的分页单元来讲没有意义,然而,对于当前在主存中却没有读,写或执行权限的页,内核将其Present和Page Size分别标记为0和1。这样,任何试图对此类页的访问都会引起一个缺页异常,因为页的Present标志被清0,而内核可以通过检查Page Size的值来检查到产生异常并不是因为缺页。
读页标志的函数:
设置页标志的函数:
对页表项操作的宏:
这里需要注意的是,如果是32位未开启PAE,则pud_offset,pmd_offset返回的仍是pgd;如果是32位开启PAE,则pud_offset返回的是pgd。pgd_offset,pud_offset,pmd_offset,pte_offset_map等返回的都是线性地址,比如,pmd_offset返回的pmd中相应表项的线性地址,*pmd_offset(pud,addr)才是表项中的值,即PT的地址。还有一个要注意的地方就是pte_offset_map,如果页表被保存在高端内存,那么就建立一个临时内核映射,这样就可以编辑页表了,等编辑完了,在解除临时内核映射。举个例子,do_anonymous_page()函数中有这么一段:
解释一下:
- if (write_access) {
- /* Allocate our own private page. */
- pte_unmap(page_table);
- spin_unlock(&mm->page_table_lock);
- if (unlikely(anon_vma_prepare(vma)))
- goto no_mem;
- page = alloc_zeroed_user_highpage(vma, addr);
- if (!page)
- goto no_mem;
- spin_lock(&mm->page_table_lock);
- page_table = pte_offset_map(pmd, addr);
- if (!pte_none(*page_table)) {
- pte_unmap(page_table);
- page_cache_release(page);
- spin_unlock(&mm->page_table_lock);
- goto out;
- }
- mm->rss++;
- acct_update_integrals();
- update_mem_hiwater();
- entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,
- vma->vm_page_prot)),
- vma);
- lru_cache_add_active(page);
- SetPageReferenced(page);
- page_add_anon_rmap(page, vma, addr);
- }
- set_pte(page_table, entry);
- pte_unmap(page_table);
调用handle_mm_fault分配一个新的页框,分配了pud,pmd,实际返回的都是pgd中的表项(如果是x86 32,未开启PAE),调用pte_alloc_map,如果pgd中的表项为空,则新分配一个页表,如果是HIGHPTE就从highmem分,如果不空并且是HIGHPTE则调用kmap_atomic将这个页表映射到临时内核映射区的一个窗口KM_PTE0,这样内核就可以编辑这个页表了,即设置相应表项。do_anonymous_page中从highmem为引起page fault的线性地址分配页框。第一次pte_unmap之后又pte_offset_map是为了防止一旦alloc_page导致进程睡眠,临时内核映射被覆盖。然后用新分配到的页框设置页表项,临时内核很宝贵的,由于编辑完页表了,解除临时内核映射,分配页框任务完成。
页分配函数:
在x86 32位未开启PAE的时候,调用pud_alloc,pmd_alloc其实并未真正分配,上边说了,其实此时pud,pmd只有一项,目的就是在请求pud,pmd的时候返回pgd。如下边map_vm_area()中的pud_alloc,并没有真正分配。而是通过pud,pmd一一转手,目的应该是与64位兼容。
- int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)
- {
- 。。。。。。。。。。。
- pgd = pgd_offset_k(address);
- spin_lock(&init_mm.page_table_lock);
- for (i = pgd_index(address); i <= pgd_index(end-1); i++) {
- pud_t *pud = pud_alloc(&init_mm, pgd, address);
- 。。。。。。。。。。
- }
- 。。。。。。。。。。
- }
写了一个测试程序:
- #include <linux/init.h>
- #include <linux/module.h>
- #include <linux/kernel.h>
- #include <linux/slab.h>
- #include <linux/gfp.h>
- #include <asm/pgtable.h>
- #include <asm/page.h>
- #include <linux/sched.h>
- #include <linux/mm.h>
- #include <linux/highmem.h>
- long unsigned int vaddr;
- static int __init paging_test_init(void){
- pte_t *pte_tmp = NULL;
- pmd_t *pmd_tmp = NULL;
- pud_t *pud_tmp = NULL;
- pgd_t *pgd_tmp = NULL;
- long unsigned int paddr;
- printk("paging test init!\n");
- vaddr = __get_free_page(GFP_KERNEL);
- pgd_tmp = pgd_offset(current->mm, vaddr);
- if(pgd_present(*pgd_tmp))
- {
- pud_tmp = pud_offset(pgd_tmp, vaddr);
- if(pud_present(*pud_tmp))
- {
- pmd_tmp = pmd_offset(pud_tmp, vaddr);
- if(pmd_present(*pmd_tmp))
- {
- if(!pmd_large(*pmd_tmp)){
- pte_tmp = pte_offset_map(pmd_tmp, vaddr);
- if(pte_present(*pte_tmp))
- {
- paddr = (pte_val(*pte_tmp) & PAGE_MASK) | (vaddr & ~PAGE_MASK);
- printk("physical address of 0x%lx is 0x%lx\n", vaddr, paddr);
- printk("__pa(vaddr) is 0x%lx", __pa(vaddr));
- }
- else
- {
- printk("pte entry is not present!\n");
- return -1;
- }
- }
- else
- {
- paddr = (pmd_val(*pmd_tmp) & PMD_MASK) | (vaddr & ~PMD_MASK);
- printk("Use Large Page PSE = 1\n");
- printk("physical address of 0x%lx is 0x%lx\n", vaddr, paddr);
- printk("__pa(vaddr) is 0x%lx", __pa(vaddr));
- }
- }
- else
- {
- printk("pte entry is not present!\n");
- }
- }
- else
- {
- printk("pud entry is not present!\n");
- return -1;
- }
- }
- else
- {
- printk("pgd entry is not present!\n");
- return -1;
- }
- return 0;
- }
- static void __exit paging_test_exit(void){
- printk("paging test exit!\n");
- if(vaddr)
- __free_page(vaddr);
- }
- module_init(paging_test_init);
- module_exit(paging_test_exit);
- MODULE_LICENSE("GPL");
- [ 361.088415] paging test init!
- [ 361.088419] Use Large Page PSE = 1
- [ 361.088422] physical address of 0xf5c7d000 is 0x35c7d000
- [ 361.088424] __pa(vaddr) is 0x35c7d000
注意判断是否是大页,好多人没有考虑这个地方,导致各种诡异的问题。