在ARM64处理器中,MMU(Memory Management Unit)包括TLB和一个页表遍历单元(Table Walk Unit, TWU)。其中TLB是一个高速缓存,用于缓存页表转换的结果,从而缩短页表查询的时间。一个完整的页表翻译和查找过程叫做页表查询。当TLB未命中时,MMU会进行内存的页表查询,从而得到一个虚拟地址(VA)对应的翻译后物理地址(PA)。
这里需要对MMU做一个模型上的抽象:
MMU的输入是两个东西:
1、MMU需要输入一段内存片段,用来保存页表描述符。
2、MMU需要输入一个具体的虚拟地址,用来给出对应的物理地址
因此,MMU在软件上需要根据MMU允许的查表模式(比如颗粒度设置为4KB、16KB或者64KB,页表查询设置为3级或者4级)设置符合要求的页表描述符,MMU是一个硬件协议规范的需要软件配置的东东。在软件上(或者kernel中),将页表描述符设置正确,并由正确的TTBR寄存器指向页表描述符基地址,就可以完成对MMU的初始化,kernel无需提供预先的针对虚拟地址的解析。
页表查询过程
在AArch64体系结构中,如果CPU想要访问一个虚拟地址,且虚拟地址没有在TLB命中时,会触发一次页表查询。此时,CPU首先根据虚拟地址头VA[63]位的值来确定调用TTBR0(用户态)还是TTBR1(内核态)寄存器中的基地址进行查表。
由TTBR找到一级页表(PGD表,也叫页全局目录,unix概念)基址后,根据虚拟地址的Bit[47:39]查找PGD表中的具体条目,找到二级页表(PUD页上级目录)对应基址,然后根据虚拟地址的Bit[38:30]查找三级页表(PMD页中间目录)对应基址,然后根据虚拟地址的Bit[29:21]找到具体的页表(PT)基地址,此时根据Bit[20:12]找到从页表基地址触发的具体的页表地址,最后根据Bit[11:0]找到页表内部的偏移。
下图是一个虚拟地址索引关系示意图:

根据虚拟地址的索引关系,MMU会通过多级查表的方式去获取物理地址(如果TLB缓存不命中的话):

MMU寄存器设置
在kernel中,会通过设置ARMv8架构下的MMU寄存器,来对MMU的页表基地址、页表颗粒度、地址空间大小、内存类型与属性、缓存使能等功能进行配置。具体来说,可以参考A-Profile Architecture中章节"D8 The AArch64 Virtual Memory System Architecture"中的描述。下面列举的寄存器在不同EL等级各不相同,只是列举了一些理论上应该存在的寄存器用以说明MMU模型:
页表描述符设置
在根据虚拟地址中对应位段与页表基地址,找到在某一级页表中对应的页表项后,需要根据页表项的格式来进行下级页表基址寻找、表项属性描述。
/arch/arm64/include/asm/pgtable.h:定义软件层面的描述符操作宏
/arch/arm64/include/asm/pgtable-hwdef.h:定义页表描述符的硬件位域宏
/arch/arm64/include/asm/pgtable-types.h:定义页表项的类型抽象
/arch/arm64/include/asm/pgtable-prot.h:页表项中的软件权限位和内存属性组合
页表描述符本身是一种遵循硬件MMU规范的内存片段,其本身也是由kernel这样的软件准备好后籍由TTBR导给MMU使用的。下面是文件pgtable-hwdef.h中的片段:
/*
* PGDIR_SHIFT determines the size a top-level page table entry can map
* (depending on the configuration, this level can be 0, 1 or 2).
*/
#define PGDIR_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)
#define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK (~(PGDIR_SIZE-1))
#define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT))
......
/*
* Hardware page table definitions.
*
* Level 1 descriptor (PUD).
*/
#define PUD_TYPE_TABLE (_AT(pudval_t, 3) << 0)
#define PUD_TABLE_BIT (_AT(pudval_t, 1) << 1)
#define PUD_TYPE_MASK (_AT(pudval_t, 3) << 0)
#define PUD_TYPE_SECT (_AT(pudval_t, 1) << 0)
#define PUD_SECT_RDONLY (_AT(pudval_t, 1) << 7) /* AP[2] */
......
/*
* Level 3 descriptor (PTE).
*/
#define PTE_VALID (_AT(pteval_t, 1) << 0)
#define PTE_TYPE_MASK (_AT(pteval_t, 3) << 0)
#define PTE_TYPE_PAGE (_AT(pteval_t, 3) << 0)
#define PTE_TABLE_BIT (_AT(pteval_t, 1) << 1)
#define PTE_USER (_AT(pteval_t, 1) << 6) /* AP[1] */
#define PTE_RDONLY (_AT(pteval_t, 1) << 7) /* AP[2] */
#define PTE_SHARED (_AT(pteval_t, 3) << 8) /* SH[1:0], inner shareable */
#define PTE_AF (_AT(pteval_t, 1) << 10) /* Access Flag */
#define PTE_NG (_AT(pteval_t, 1) << 11) /* nG */
#define PTE_GP (_AT(pteval_t, 1) << 50) /* BTI guarded */
#define PTE_DBM (_AT(pteval_t, 1) << 51) /* Dirty Bit Management */
#define PTE_CONT (_AT(pteval_t, 1) << 52) /* Contiguous range */
#define PTE_PXN (_AT(pteval_t, 1) << 53) /* Privileged XN */
#define PTE_UXN (_AT(pteval_t, 1) << 54) /* User XN */可以看出,对于kernel来说,不是很关心PGD、PUD、PMD这三个层级的页表类型的页表描述符的属性,比较关心具体的PTE类型的属性。因为PTE类型直接对应到具体的物理内存。
PGD、PUD、PMD这三者的描述符属性基本只关心:有效标志位bit[0](1有效,0无效)、类型标志位bit[1](1页表类型,0块类型)、块的输出地址、页表的下一级基地址。
PTE则需要关心更具体的页表属性。对于具体的页表属性来说,一般会从如下几个维度考量:
1、页表属性——共享性域缓存性
2、页表属性——访问权限
3、页表属性——执行权限
4、页表属性——访问标志位
5、页表属性——全局和进程特有TLB
页表描述符示意图:

在Arm64中,“块”是一种直接映射打断物理内存的页表描述符类型,它让硬件在地址转换时不必走到最后一级页表,直接就能完成虚拟地址到物理地址的映射,是一种用于TLB提效的手段。这个映射关系存在,但还是需要使用kmalloc或者vmalloc或者malloc的方式将内存分配出去。块映射纯粹是为了内核访问物理内存时,能用一个TLB条目覆盖更大的范围,从而降低TLBmiss。
恒等映射的页表案例
由于MMU不能依靠直接输入物理地址来给你一个物理地址和对应物理地址的返回值,所以在kernel的初始化阶段,为了确保kernel不出问题,会在内存范围内建立一种恒等映射。
恒等映射的本质,是一种将虚拟地址本身映射到物理地址的一种页表结构。从BootLoader/BIOS跳转到kernel时,MMU是关闭的。关闭了的MMU意味着不能利用高速缓存的性能,因此,我们需要在初始化的某个阶段吧MMU打开并使能数据高速缓存。在MMU关闭的情况下,处理器访问的地址都是物理地址,在MMU打开的一刻起,处理器访问的地址都是虚拟地址。
目前处理器大多是多级流水线体系结构处理器,处理器会预先取多条指令到流水线中。当打开MMU时,处理器的预取指令依然会存在,因此在执行MMU开启的过程中,需要创建一种恒等映射的页表关系,以避免处理器因启用虚拟地址而跑飞。
恒等映射页表的创建将遵循如下步骤:
1、设置4级页表属性与PTE物理页表属性
2、根据属性定义页表struct
3、在kernel文件数据段中预留4KB大小的内存空间给PGD
4、使用__create_pgd_mapping函数,在内核启动最早期的汇编代码阶段,建立代码段、数据段、PBASE段的恒等映射,并将页表基地址加载到TTBR1中
5、后续会开启MMU,并在MMU中进行非恒等映射的其他种类的页表映射
