浅析linux内核内存管理之分页

浅析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()函数中有这么一段:

  1. if (write_access) {  
  2.         /* Allocate our own private page. */  
  3.         pte_unmap(page_table);  
  4.         spin_unlock(&mm->page_table_lock);  
  5.   
  6.         if (unlikely(anon_vma_prepare(vma)))  
  7.             goto no_mem;  
  8.         page = alloc_zeroed_user_highpage(vma, addr);  
  9.         if (!page)  
  10.             goto no_mem;  
  11.   
  12.         spin_lock(&mm->page_table_lock);  
  13.         page_table = pte_offset_map(pmd, addr);  
  14.   
  15.         if (!pte_none(*page_table)) {  
  16.             pte_unmap(page_table);  
  17.             page_cache_release(page);  
  18.             spin_unlock(&mm->page_table_lock);  
  19.             goto out;  
  20.         }  
  21.         mm->rss++;  
  22.         acct_update_integrals();  
  23.         update_mem_hiwater();  
  24.         entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,  
  25.                              vma->vm_page_prot)),  
  26.                       vma);  
  27.         lru_cache_add_active(page);  
  28.         SetPageReferenced(page);  
  29.         page_add_anon_rmap(page, vma, addr);  
  30.     }  
  31.   
  32.     set_pte(page_table, entry);  
  33.     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位兼容。

  1. int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)  
  2. {  
  3.     。。。。。。。。。。。  
  4.     pgd = pgd_offset_k(address);  
  5.     spin_lock(&init_mm.page_table_lock);  
  6.     for (i = pgd_index(address); i <= pgd_index(end-1); i++) {  
  7.         pud_t *pud = pud_alloc(&init_mm, pgd, address);  
  8.      。。。。。。。。。。  
  9.     }  
  10.   
  11.     。。。。。。。。。。  
  12. }  

写了一个测试程序:

  1. #include <linux/init.h>  
  2. #include <linux/module.h>  
  3. #include <linux/kernel.h>  
  4. #include <linux/slab.h>  
  5. #include <linux/gfp.h>  
  6. #include <asm/pgtable.h>  
  7. #include <asm/page.h>  
  8. #include <linux/sched.h>  
  9. #include <linux/mm.h>  
  10. #include <linux/highmem.h>  
  11.       
  12. long unsigned int vaddr;  
  13.   
  14. static int __init paging_test_init(void){  
  15.     pte_t *pte_tmp = NULL;  
  16.     pmd_t *pmd_tmp = NULL;  
  17.     pud_t *pud_tmp = NULL;  
  18.     pgd_t *pgd_tmp = NULL;  
  19.     long unsigned int paddr;  
  20.     printk("paging test init!\n");  
  21.     vaddr = __get_free_page(GFP_KERNEL);  
  22.     pgd_tmp = pgd_offset(current->mm, vaddr);  
  23.     if(pgd_present(*pgd_tmp))  
  24.     {  
  25.         pud_tmp = pud_offset(pgd_tmp, vaddr);  
  26.         if(pud_present(*pud_tmp))  
  27.         {  
  28.             pmd_tmp = pmd_offset(pud_tmp, vaddr);  
  29.             if(pmd_present(*pmd_tmp))  
  30.             {  
  31.                 if(!pmd_large(*pmd_tmp)){  
  32.                     pte_tmp = pte_offset_map(pmd_tmp, vaddr);     
  33.                     if(pte_present(*pte_tmp))  
  34.                     {  
  35.                         paddr = (pte_val(*pte_tmp) & PAGE_MASK) | (vaddr & ~PAGE_MASK);  
  36.                         printk("physical address of 0x%lx is 0x%lx\n", vaddr, paddr);  
  37.                         printk("__pa(vaddr) is 0x%lx", __pa(vaddr));  
  38.                     }  
  39.                     else  
  40.                     {  
  41.                         printk("pte entry is not present!\n");  
  42.                         return -1;  
  43.                     }  
  44.                 }  
  45.                 else  
  46.                 {  
  47.                     paddr = (pmd_val(*pmd_tmp) & PMD_MASK) | (vaddr & ~PMD_MASK);  
  48.                     printk("Use Large Page PSE = 1\n");  
  49.                     printk("physical address of 0x%lx is 0x%lx\n", vaddr, paddr);  
  50.                     printk("__pa(vaddr) is 0x%lx", __pa(vaddr));  
  51.                 }  
  52.             }  
  53.             else  
  54.             {  
  55.                 printk("pte entry is not present!\n");  
  56.             }  
  57.         }  
  58.         else  
  59.         {  
  60.             printk("pud entry is not present!\n");  
  61.             return -1;  
  62.         }  
  63.     }  
  64.     else  
  65.     {  
  66.         printk("pgd entry is not present!\n");  
  67.         return -1;  
  68.     }  
  69.     return 0;  
  70. }  
  71.   
  72. static void __exit paging_test_exit(void){  
  73.     printk("paging test exit!\n");  
  74.         if(vaddr)  
  75.         __free_page(vaddr);  
  76. }  
  77.   
  78. module_init(paging_test_init);  
  79. module_exit(paging_test_exit);  
  80.   
  81. MODULE_LICENSE("GPL");  
测试结果:

  1. [  361.088415] paging test init!  
  2. [  361.088419] Use Large Page PSE = 1  
  3. [  361.088422] physical address of 0xf5c7d000 is 0x35c7d000  
  4. [  361.088424] __pa(vaddr) is 0x35c7d000  

注意判断是否是大页,好多人没有考虑这个地方,导致各种诡异的问题。

阅读更多

没有更多推荐了,返回首页