Linux版本:4.14.74
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。
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.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)
接下来就是对两份页表进行填充,这部分用到三个函数
- create_table_entry
- create_pgd_entry
- 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的过程
上电映射页表的过程可简单总结为下图
通过create_pgd_entry创建中间页表,对于39bit虚拟地址宽度的情况,只用创建PGD中一个页表项。
根据identity mapping和kernel mappig的区域,以2M为单位进行section mapping的映射,直到把两个区域映射完毕。
好了,到这个阶段,我们就可以读取kerne image的数据了,可以进行kernel了。
来源:oschina
链接:https://my.oschina.net/u/4271891/blog/4268041