内存管理(三):内核上电启动阶段的页表映射

Linux版本:4.14.74

目录

  • 1 启动阶段所需页表
  • 2 创建过程
    • 2.1启动阶段的页表大小
    • 2.2创建描述符函数
    • 2.3 创建中间页表
    • 2.4 section mapping
    • 2.5 映射的整体流程

1 启动阶段所需页表

在kernel启动阶段,会创建两次地址映射

  • Identity mapping
  • Kernel image mapping

在BootLoader以及uboot中,mmu功能是关闭的,操作的都是物理地址。为了提高性能,加快初始化速度,我们必须某个阶段(越早越好)打开MMU和cache,打开MMU之后操作的就是虚拟地址,为了从物理地址(Physical Address,简称PA)转换到虚拟地址(Virtual Address,简称VA)的平滑过渡,ARM推荐创建VA和PA相等的一段映射(例如:虚拟地址addr通过页表查询映射的物理地址也是addr)。这段映射在linux中称为identity mapping。
而为了执行kernel image,自然需要映射kernel image。
内存管理(三):内核上电启动阶段的页表映射_第1张图片
turn on MMU相关的代码被放入到一个特别的section,名字是.idmap.text,实际上对应上图中物理地址空间的IDMAP_TEXT这个block。这个区域的代码被mapping了两次,做为kernel image的一部分,它被映射到了__idmap_text_start开始的虚拟地址上去,此外,假设IDMAP_TEXT block的物理地址是A地址,那么它还被映射到了A地址开始的虚拟地址上去。
通过System.map就可以查看哪些函数被放在".idmap.text"段。

内存管理(三):内核上电启动阶段的页表映射_第2张图片

2 创建过程

2.1启动阶段的页表大小

在内存管理(二):页表映射过程我们描述了虚拟地址到物理地址的映射过程,我们仍然以虚拟地址宽度为39bit,页大小为4K来描述。有3级页表,PGD–>PMD–>PTE,PTE映射的大小是4K。但是在启动阶段,系统并不会以4K为单位进行映射,因为在启动阶段的目的就是能尽快的读取到kernel image,而以4K进行映射,假如kernel的大小为16M,映射kernel image就需要4096次。所以在启动流程中会使用section map,页表减少一级,变为PGD–>PMD–>offset,PMD映射大小是2M,对于16M大小的kernel image,最少只需要8次就能映射完成。
现在已经明确在启动阶段需要两级页表,PGD和PMD,并且需要identify mapping和kernel mapping两份页表,也就是需要4个页大小来存储启动阶段的页表,这块地址空间是什么分配的呢?是在链接脚本中vmlinux.lds.S

 1. BSS_SECTION(0, 0, 0)  
 2.   
 3. . = ALIGN(PAGE_SIZE);  
 4. idmap_pg_dir = .;  
 5. . += IDMAP_DIR_SIZE;  
 6. swapper_pg_dir = .;  
 7. . += SWAPPER_DIR_SIZE;  
 8. idmap_full_pg_dir = .;  
 9. . += IDMAP_FULL_DIR_SIZE; 

从链接脚本中可以看到预留了两份地址存储页表项。紧跟在bss段后面。idmap_pg_dir是identity mapping使用的页表。swapper_pg_dir是kernel image mapping初始阶段使用的页表。请注意,这里的内存是一段连续内存。也就是说页表(PGD/ PMD)都是连在一起的,地址相差PAGE_SIZE(4k)
接下来就是对两份页表进行填充,这部分用到三个函数

  1. create_table_entry
  2. create_pgd_entry
  3. create_block_map

下面我们依次来看看这三个函数

2.2创建描述符函数

create_table_entry这个宏定义主要是用来创建一个中间level的translation table中的描述符。如果用linux的术语,就是创建PGD、PUD或者PMD的描述符。如果用ARM64术语,就是创建L0、L1或者L2的描述符。具体创建哪一个level的Translation table descriptor是由tbl参数指定的

1.	[arch/arm64/kernel/head.S]  
2.	   
3.	/* 
4.	 * Macro to create a table entry to the next page. 
5.	 * 
6.	 *      tbl:    page table address 
7.	 *      virt:   virtual address 
8.	 *      shift:  #imm page table shift 
9.	 *      ptrs:   #imm pointers per table page 
10.	 * 
11.	 * Preserves:   virt 
12.	 * Corrupts:    tmp1, tmp2 
13.	 * Returns:     tbl -> next level table page address 
14.	 */  
15.	        .macro  create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2  
16.	        lsr     \tmp1, \virt, #\shift                  /****1****/
17.	        and     \tmp1, \tmp1, #\ptrs - 1        /****1****/ // table index  
18.	        add     \tmp2, \tbl, #PAGE_SIZE     /*****2***/
19.	        orr     \tmp2, \tmp2, #PMD_TYPE_TABLE   /****3***/// address of next table and entry type  
20.	        str     \tmp2, [\tbl, \tmp1, lsl #3]         /*****4******/
21.	        add     \tbl, \tbl, #PAGE_SIZE         /*****5****/ // next level table page  
22.	        .endm 
  • 参数tbl表示页表的地址,virt表示要映射的虚拟地址,shift表示这一级页表的在虚拟地址中的偏移,ptr表示这一级页表是几位的。tmp1和tmp2是两个临时变量
  • (1)取出虚拟地址中对应的本级页表的那几位
  • (2)取出下一级页表的地址,初始阶段的页表(PGD/PUD/PMD/PTE)都是排列在一起的,每一个占用一个page。也就是说,如果create_table_entry当前操作的是PGD,那么tmp2这时候保存了下一个level的页表,也就是PUD了
  • (3)页表描述符的前两个bit表示该描述符是否有效,这里或上0x11,到这里,页表项的数据填充完毕
  • (4)把页表项内容放到指定的页表项当中
  • (5)结束的时候tbl会加上一个PAGE_SIZE,也就是tbl变成了下一级页表的地址
  • 2.3 创建中间页表

下面的代码是create_pgd_entry,名称有点迷惑性,这个宏的作用并不仅仅是创建pgd,除了最末级页表PMD外,其他页表都会创建。当然在39bit情况下,只会创建PGD一个表项。

1.	/* 
2.	 * Macro to populate the PGD (and possibily PUD) for the corresponding 
3.	 * block entry in the next level (tbl) for the given virtual address. 
4.	 * 
5.	 * Preserves:   tbl, next, virt 
6.	 * Corrupts:    tmp1, tmp2 
7.	 */  
8.	        .macro  create_pgd_entry, tbl, virt, tmp1, tmp2  
9.	        create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2  
10.	#if SWAPPER_PGTABLE_LEVELS > 3  
11.	        create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2  
12.	#endif  
13.	#if SWAPPER_PGTABLE_LEVELS > 2  
14.	        create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2  
15.	#endif  
16.	        .endm  
17.	   

  • 参数tbl表示页表的地址,virt表示要映射的虚拟地址。tmp1和tmp2是两个临时变量
  • create_table_entry前面已经介绍过了,就是填充该页表的页表项(只填充一个)
  • SWAPPER_PGTABLE_LEVELS:对于页大小为4K的情况,由于使用了section mapping,SWAPPER_PGTABLE_LEVELS会比页表LEVEL小1,其他页大小这个值与页表LEVEL一致

例子1:当虚拟地址是48个bit,4k page size,这时候page level等于4,映射关系是PGD(L0)—>PUD(L1)—>PMD(L2)—>Page table(L3)—>page,但是如果采用了section mapping(4k的page一定会采用section mapping),映射关系是PGD(L0)—>PUD(L1)—>PMD(L2)—>section。在create_pgd_entry函数中将创建PGD和PUD这两个中间level。

例子2:当虚拟地址是48个bit,16k page size(不能采用section mapping),这时候page level等于4,映射关系是PGD(L0)—>PUD(L1)—>PMD(L2)—>Page table(L3)—>page。在create_pgd_entry函数中将创建PGD、PUD和PMD这三个中间level。

例子3:当虚拟地址是39个bit,4k page size,这时候page level等于3,映射关系是PGD(L1)—>PMD(L2)—>Page table(L3)—>page。由于是4k page,因此采用section mapping,映射关系是PGD(L1)—>PMD(L2)—>section。在create_pgd_entry函数中将创建PGD这一个中间level。

2.4 section mapping

create_block_map则是以2M为大小进行映射

1.	.macro    create_block_map, tbl, flags, phys, start, end   
2.	lsr    \phys, \phys, #SWAPPER_BLOCK_SHIFT   
3.	lsr    \start, \start, #SWAPPER_BLOCK_SHIFT   
4.	and    \start, \start, #PTRS_PER_PTE - 1    // table index   
5.	orr    \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT    // table entry   
6.	lsr    \end, \end, #SWAPPER_BLOCK_SHIFT   
7.	and    \end, \end, #PTRS_PER_PTE - 1        // table end index   
8.	:    str    \phys, [\tbl, \start, lsl #3]        // store the entry   
9.	add    \start, \start, #1            // next entry   
10.	add    \phys, \phys, #SWAPPER_BLOCK_SIZE        // next block   
11.	cmp    \start, \end   
12.	b.ls    9999b   
13.	.endm 
  • 参数tbl页表地址,参数flags表示当前页表项指示的是block还是page。phys表示要映射的物理地址的起始地址,start和end分表表示物理地址要映射到的虚拟地址的开始和结束
  • 前6行当中,phys和flag计算得到页表项的内容,通过start得到页表的index开始,通过end得到页表的计数。
  • 9999循环映射从phys开始的地址映射到start—end的区域

2.5 映射的整体流程

现在我们来看看创建启动阶段页表的整体流程

14.	__create_page_tables:  
15.	        mov     x28, lr  
16.	  
17.	        /* 
18.	         * Invalidate the idmap and swapper page tables to avoid potential 
19.	         * dirty cache lines being evicted. 
20.	         */  
21.          /*******1*******/	
22.	        adrp    x0, idmap_pg_dir  
23.	        ldr     x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)  
24.	        bl      __inval_dcache_area  
25.	  
26.	        /* 
27.	         * Clear the idmap and swapper page tables. 
28.	         */  
29.	       /***********2***********/
30.	        adrp    x0, idmap_pg_dir  
31.	        ldr     x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)  
32.	1:      stp     xzr, xzr, [x0], #16  
33.	        stp     xzr, xzr, [x0], #16  
34.	        stp     xzr, xzr, [x0], #16  
35.	        stp     xzr, xzr, [x0], #16  
36.	        subs    x1, x1, #64  
37.	        b.ne    1b  
38.	  
39.	        mov     x7, SWAPPER_MM_MMUFLAGS  
40.	  
41.	        /* 
42.	         * Create the identity mapping. 
43.	         */  
44.		/******3*****/
45.	        adrp    x0, idmap_pg_dir  
46.	        adrp    x3, __idmap_text_start          // __pa(__idmap_text_start)  
47.	  
48.	        create_pgd_entry x0, x3, x5, x6  
49.	        mov     x5, x3                          // __pa(__idmap_text_start)  
50.	        adr_l   x6, __idmap_text_end            // __pa(__idmap_text_end)  
51.	        create_block_map x0, x7, x3, x5, x6  
52.	  
53.	        /* 
54.	         * Map the kernel image (starting with PHYS_OFFSET). 
55.	         */ 
56.	       /*****4******/ 
57.	        adrp    x0, swapper_pg_dir  
58.	        mov_q   x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)  
59.	        add     x5, x5, x23                     // add KASLR displacement  
60.	        create_pgd_entry x0, x5, x3, x6  
61.	        adrp    x6, _end                        // runtime __pa(_end)  
62.	        adrp    x3, _text                       // runtime __pa(_text)  
63.	        sub     x6, x6, x3                      // _end - _text  
64.	        add     x6, x6, x5                      // runtime __va(_end)  
65.	        create_block_map x0, x7, x3, x5, x6  
66.	  
67.	        /* 
68.	         * Since the page tables have been populated with non-cacheable 
69.	         * accesses (MMU disabled), invalidate the idmap and swapper page 
70.	         * tables again to remove any speculatively loaded cache lines. 
71.	         */  
72.	        adrp    x0, idmap_pg_dir  
73.	        ldr     x1, =(IDMAP_DIR_SIZE + SWAPPER_DIR_SIZE + RESERVED_TTBR0_SIZE)  
74.	        dmb     sy  
75.	        bl      __inval_dcache_area  
76.	  
77.	        ret     x28  
78.	ENDPROC(__create_page_tables)

  • (1)对identity mapping和kernel mapping对应的区域invalid cache操作
  • (2)请identity和swapper的页表区域清零操作
  • (3)对identity区域进行映射,映射的过程就是调用create_pgd_entry和create_block_map的过程
  • (4)对kernel区域进行映射,映射的过程就是调用create_pgd_entry和create_block_map的过程
    上电映射页表的过程可简单总结为下图
    内存管理(三):内核上电启动阶段的页表映射_第3张图片
    通过create_pgd_entry创建中间页表,对于39bit虚拟地址宽度的情况,只用创建PGD中一个页表项。
    根据identity mapping和kernel mappig的区域,以2M为单位进行section mapping的映射,直到把两个区域映射完毕。
    好了,到这个阶段,我们就可以读取kerne image的数据了,可以进行kernel了。
    内存管理(三):内核上电启动阶段的页表映射_第4张图片

你可能感兴趣的:(Linux内存管理)