背景

前面分析了kernel boot阶段内存管理实现的分段机制,可以发现页表描述符是按照ARM MMU硬件页表转换逻辑来设计的。kernel在初始化过程中只映射了内核 image部分的物理内存,在某个合适的时候内核需要将尽可能多的物理内存映射到页表中。
Linux设计为通用的操作系统,为了便于移植需要抽象出一些硬件细节,我们在驱动代码中看到大量的core层代码就是这个思想的体现。内核中只有和硬件完全相关的代码才会单独实现,这样做便于移植和添加新硬件。在内存管理中涉及到的PGD,PMD,PUD,PTE等要结合上下文理解是属于硬件还是软件层上的概念。对于32bit的ARM MMU根据相关文档最多只支持到二级页表。所以内核没有用到PMD和PUD。
pud_offset

 38 static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)                                                                                                                                    
 39 {                            
 40     return (pud_t *)pgd;     
 41 }

pmd_offset

160 static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)                                                                                                                                         
161 {                 
162     return (pmd_t *)pud;
163 }

可以在ARM平台上调用pmd和pud相关结构去获取offset其实直接返回的是pgd的offset。

变量

  • KERNEL_RAM_VADDR
    PAGE_OFFSET + TEXT_OFFSET = 0x8000 0000 + 0x0000 8000,
    表示内核image在RAM中的起始地址
  • PG_DIR_SIZE
    0x0000 4000
  • swapper_pg_dir
    KERNEL_RAM_VADDR - PG_DIR_SIZE = 0x8000 8000 - 0x0000 4000
    表示内核页表的起始地址
    pgd的基址
  • PGDIR_SHIFT
    21
    -MODULES_VADDR
    PAGE_OFFSET - SZ_16M = 0x8000 0000 - 0x0100 0000 = 0x7F00 0000
    众所周知内核会把地址空间划分成user space和kernel space。一般都说user space位于低地址的0~3G空间,kernel位于0xC000 0000开始的1G空间。但是tiny4412平台kernel space是从0x8000 0000开始的,也就是把0~4G对半分了,user space占低地址的2GB,kernel占高地址的2GB。
    但仔细研究可以发现起始user space并没有用满2GB。比如说这里的MODULES_VADDR就是位于kernel space和user space之间,并且占用的是user space尾部的16M。
  • pgd_t
    这居然是一个数组!如果不懂pgd_t为什么要定义成pgd[2]这样的二元数组,那一定不懂内核分页机制的实现过程!至少不算完全懂!
    pgd定义成数组的原因在源码arch/arm/include/asm/pgtable-2level.h的注释中已经说明,可以说注释中的图包含了内核页表转换的精髓!
35  *    pgd             pte
 36  * |        |
 37  * +--------+
 38  * |        |       +------------+ +0
 39  * +- - - - +       | Linux pt 0 |
 40  * |        |       +------------+ +1024
 41  * +--------+ +0    | Linux pt 1 |      
 42  * |        |-----> +------------+ +2048
 43  * +- - - - + +4    |  h/w pt 0  |      
 44  * |        |-----> +------------+ +3072
 45  * +--------+ +8    |  h/w pt 1  |      
 46  * |        |       +------------+ +4096

从解释中可以发现linux约定一次分配1page 4KB的大小来给硬件pte使用。根据ARM硬件小页地址变换过程可知。一条一级页表描述符(*pgd)能表示2的8 次方(256)个二级页表条目(pte),每条pte占4Byte,总共硬件pte需要占用256*4Byte=1KB。那我们分1个page全给硬件pte使用吗?并不是!linux遵循软硬件分离的设计思想,linux本身包含软件pte,size和硬件pte一样,只是pte描述符中控制位表示的含义不同。既然我们每描述一个硬件pte都需要对应有一个软件的pte,那索性linux就将一个page对半分,一半给硬件pte,一般给对应的软件pte。这样一来,半个page可以存放2*256条pte,也就是两个pgd元素,所以这里定义了pgd[2]这个数组!内核约定一个page中的上半部分给软件pte使用,下半部分给硬件pte使用。这样完全用完一个page一点不浪费。

  • PGDIR_SIZE
    1<< PGDIR_SHIFT == 1 << 21 == 2MB
    为什么是21bit而不是20bit?因为根据上一个变量的说明可知,内核为了完整使用一个page的空间需要在一个page中存放两条pgd对应的pte条目,一个pgd可以映射1MB物理内存,2个pgd就能映射2MB物理内存。所以内核应该是一次性映射2MB的物理内存。
  • *init_mm
    内核定义了一个静态mm_struct数据结构来管理内核页表相关定义。看下这里的pgd成员,swapper_pg_dir
 16 struct mm_struct init_mm = {                                                                                                                                                                            
 17     .mm_rb      = RB_ROOT,                
 18     .pgd        = swapper_pg_dir,
 19     .mm_users   = ATOMIC_INIT(2),
 20     .mm_count   = ATOMIC_INIT(1),         
 21     .mmap_sem   = __RWSEM_INITIALIZER(init_mm.mmap_sem),
 22     .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
 23     .mmlist     = LIST_HEAD_INIT(init_mm.mmlist),
 24     INIT_MM_CONTEXT(init_mm)
 25 };

-*VMALLOC_START
high_mem + VMALLOC_OFFSET
- VMALLOC_END
0xFF00 0000
- VMALLOC_OFFSET
8*1024*1024
- high_mem
high_mem = arm_lowmem_limit
- vmalloc_limit && vmalloc_min
vmalloc_limit == vmalloc_min
vmalloc_min = (VMALLOC_END - (240 << 20) - VMALLOC_OFFSET) = 0xFF00 0000 - 240MB - 8MB
物理地址越大vmalloc的空间越小,但是最小不得小于240M B+8MB
- arm_lowmem_limit
内核会区分high mem和low mem,两者使用方法不同。这里的arm_lowmem_limit就是用来记载当前物理内存low_mem段的大小,high mem则用于vmalloc。vmalloc有最小虚拟空间的限制,最小不能小于这个值。这个值就是vmalloc_limit,vmalloc_limit表示vmalloc的起始地址。当实际物理内存大于vmalloc_limit时,内核会将low_mem限制到vmalloc_limit;如果实际物理内存小于vmalloc_limit则将所有的物理内存赋给low_mem,vmalloc就会变大,变成从物理内存结束到0xFF00 0000为止的所有虚拟地址给vmalloc使用,远大于(240 + 8)MB。

  • virtual memory map
    这里写图片描述

paging_init流程分析

有了前面的基础,paging_init的流程比较好理解了。初始化memory 类型,清理boot阶段的页表这些就不看了,直接看创建页表的关键函数map_lowmem()。

1515 void __init paging_init(const struct machine_desc *mdesc)
1516 {         
1517     void *zero_page;
1518           
1519     build_mem_type_table();
1520     prepare_page_table();
1521     map_lowmem();
1522     dma_contiguous_remap();
1523     devicemaps_init(mdesc);
1524     kmap_init();
1525     tcm_init();
1526           
1527     top_pmd = pmd_off_k(0xffff0000);
1528           
1529     /* allocate the zero page. */
1530     zero_page = early_alloc(PAGE_SIZE);
1531    
1532     bootmem_init();
1533           
1534     empty_zero_page = virt_to_page(zero_page);
1535     __flush_dcache_page(NULL, empty_zero_page);
1536 }                                                                                            

从map_lowmem的函数名可以看出,这个函数会将lowmem部分的一级pgd页表填充初始化。1MB对齐部分的物理内存会被初始化pgd中,不足1MB的会再通过pte来映射。
到此,boot阶段初始化的页表就被覆盖掉了。且可能同时存在分段和分页两种形式的映射

Logo

更多推荐