1. 页表
1.1 统一的页表框架
页表用来把虚拟页映射到物理页,并且存放页的保护位(即访问权限)。
在Linux4.11版本以前,Linux内核把页表分为4级:
页全局目录表(PGD)、页上层目录(PUD)、页中间目录(PMD)、直接页表(PT)。
4.11版本把页表扩展到5级,在页全局目录和页上层目录之间增加了页四级目录(P4D)。
各处处理器架构可以选择使用5级,4级,3级或者2级页表,同一种处理器在页长度不同的情况可能选择不同的页表级数。可以使用配置宏CONFIG_PGTABLE_LEVELS配置页表的级数,一般使用默认值。
如果选择4级页表,那么使用PGD,PUD,PMD,PT;如果使用3级页表,那么使用PGD,PMD,PT;如果选择2级页表,那么使用PGD和PT。如果不使用页中间目录,那么内核模拟页中间目录,调用函数pmd_offset根据页上层目录表项和虚拟地址获取页中间目录表项时,直接把页上层目录表项指针强制转换成页中间目录表项。
static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr)
{
return (pmd_t *)pud_page_vaddr(*pud) + pmd_index(addr);
}
每个进程有独立的页表,进程的mm_struct实例的成员pgd指向页全局目录,前面四级页表的表项存放下一级页表的起始地址,直接页表的页表项存放页帧号(PFN)。
内核也有一个页表,0号内核线程的进程描述符init_task的成员active_mm指向内存描述符init_mm,内存描述符init_mm的成员pgd指向内核的页全局目录swapper_pg_dir。
1.2 ARM64处理器的页表
ARM64处理器把页表称为转换表,最多4级。ARM64处理器支持三种页长度:4KB,16KB,64KB。页长度和虚拟地址的宽度决定了转换表的级数,在虚拟地址的宽度为48位的条件下,页长度和转换表级数的关系如下所示:
页长度 | 转换表级数 |
---|---|
4KB | 4级页表 |
16KB | 4级页表 |
64KB | 3级页表 |
ARM64处理器把表项称为描述符,使用64位的长描述符格式。描述符的0bit指示描述符是不是有效的:0表示无效,1表示有效。第1位指定描述符类型。
在块描述符和页描述符中,内存属性被拆分为一个高属性和一个低属性块。
(1)[59,63):基于页的硬件属性。
(2)[55,59):保留给软件使用。
(3)54:在异常级别0,表示UXN,即不运行异常级别0执行内核代码;在其他异常级别,表示XN,不允许执行。
(4)53:PXN不运行在特权级别(el1,el2,el3)执行。
(5)52:连续,指示这条转换表项属于一个连续表项集合,一个连续表项集合可以被换存在一条TLB表项里面。
(6)51:脏位修饰符(DBM),指示页或内存块是否被修改过。
(7)11:非全局(nG)。表示不是全局,是进程私有的,有一个关联的ASID;nG位是0,表示转换是全局的,是所有进程共享的,内核的页或内存块是所有进程共享的。
(8)10:访问标志,指示页或内存块自从相应的转换表描述符中的访问标志被设置为0后是否被访问过。
(9)[8,10):可共享性,00表示不共享,01保留值,10表示外部共享,11表示内部共享。
(10)[6,8): AP[2:1](数据访问权限)。
(11)5:非安全。对于安全状态的内存访问,指定输出地址在安全地址映射还是非安全地址映射。
(12)[2,5):内存属性索引,指定寄存器MAIR_ELx中内存属性字段的索引,内存属性间接寄存器有8个8位内存属性字段:Attr<n>,n等于0~7。
2. TLB
处理器的MMU负责把虚拟地址转换成物理地址,为了改进虚拟地址到物理地址的转换速度,避免每次转换都需要查询内存中的页表,处理器厂商在管理单元里加了称为TLB的高速缓存,TLB直译为转换后备缓冲区,意译为页表缓存。
页表缓存用来缓存最近使用过的页表项,有些处理器使用两级页表缓存:第一级TLB分为指令TLB和数据TLB,好处是取指令和取数据可以并行;第二级TLB是统一TLB,即指令和数据共用的TLB。
2.1TLB表项格式
不同处理器架构的TLB表项的格式不同。ARM64处理器的每条TLB表项不仅包含虚拟地址和物理地址,也包含属性:内存类型、缓存策略、访问权限、地址空间标识符(ASID)和虚拟机标识符(VMID)。地址空间标识符区分不同进程的页表项,虚拟机标识符区分不同虚拟机的页表项。
2.2 TLB管理
如果内核修改了可能缓存在TLB里面的页表项,那么内核必须负责使旧的TLB表项失效,内核定义了每种处理器架构必须实现的函数。
函数 | 说明 |
---|---|
void flush_tlb_all(void); | 使所有TLB表项失效 |
void flush_tlb_mm(struct mm_struct *mm); | 使指定用户地址空间的所有TLB表项失效,参数mm是进程的内存描述符 |
void flush_tlb_range(struct vm_area_struct *vma, unsigned long start,unsigned long end); | 使指定用户地址空间的指定虚拟页的TLB表项失效,参数vma是虚拟内存区域,uaddr是一个虚拟页中的任意虚拟地址 |
void flush_tlb_kernel_range(unsigned long start, unsigned long end); | 使内核的某个虚拟地址范围的TLB表项失效,参数start是起始地址,end是结束地址 |
当TLB没有命中的时候,ARM64处理器的MMU自动遍历内存中的页表,把页表项复制到TLB,不需要软件把页表项写到TLB,所以ARM64架构没有提供写TLB的指令。
2.3 地址空间标识符
为了减少在进程切换时清空页表缓存的需要,ARM64处理器的页表缓存使用非全局位区分内核和进程的页表项(nG位为0表示内核的页表项),使用地址空间标识符(ASID)区分不同进程的页表项。
ARM64处理器的ASID长度是由具体实现定义的,可以选择8位或者16位。寄存器TTBR0_EL1或者TTBR1_EL1都可以用来存放当前进程的ASID,通常使用寄存器TCR_EL1的A1位决定使用哪个寄存器存放当前进程的ASID,通常使用寄存器TTBR0_EL1。寄存器TTBR0_EL1的位[63:48]或者[63:56]存放当前进程的ASID,位[47:1]存放当前进程的页全局目录的物理地址。
在SMP系统中,ARM64架构要求ASID在处理器的所有核是唯一的。假设ASID为8位,ASID只有256个值,其中0是保留值,可分配的ASID范围1~255,进程的数量可能超过255,两个进程的ASID可能相同,内核引入ASID版本号解决这个问题。
(1)每个进程有一个64位的软件ASID,低8位存放硬件ASID,高56位存放ASID版本号。
(2)64位全局变量asid_generation的高56位保存全局ASID版本号。
(3)当进程被调度时,比较进程的ASID版本号和全局版本号。如果版本号相同,那么直接使用上次分配的ASID,否则需要给进程重新分配硬件ASID。
存在空闲ASID,那么选择一个分配给进程。不存在空闲ASID时,把全局ASID版本号加1,重新从1开始分配硬件ASID,即硬件ASID从255回绕到1。因为刚分配的硬件ASID可能和某个进程的ASID相同,只是ASID版本号不同,页表缓存可能包含了这个进程的页表项,所以必须把所有处理器的页表缓存清空。
引入ASID版本号的好处是:避免每次进程切换都需要清空页表缓存,只需要在硬件ASID回环时把处理器的页表缓存清空。
2.4 虚拟机标识符
虚拟机里面运行的客户操作系统的虚拟地址转物理地址分两个阶段:
(1)把虚拟地址转换成中间物理地址,由客户操作系统的内核控制,和非虚拟化的转换过程相同。
(2)把中间物理地址转换成物理地址,由虚拟机监控器控制,虚拟机监控器为每个虚拟机维护一个转换表,分配一个虚拟机标识符,寄存器VTTBR_EL2存放当前虚拟机的阶段2转换表的物理地址。
每个虚拟机有独立的ASID空间,页表缓存使用虚拟机标识符区分不同虚拟机的转换表项,避免每次虚拟机切换都要清空页表缓存,在虚拟机标识符回绕时把处理器的页表缓存清空。