文章目录
- 重要:本系列文章内容摘自
<Linux内核深度解析>
基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明
1 页表缓存
处理器的内存管理单元(Memory Management Unit,MMU)负责把虚拟地址转换成物理地址,为了改进虚拟地址到物理地址的转换速度,避免每次转换都需要查询内存中的页表,处理器厂商在内存管理单元里面增加了一个称为TLB(Translation Lookaside Buffer)的高速缓存,TLB直译为转换后备缓冲区,意译为页表缓存。
页表缓存用来缓存最近使用过的页表项,有些处理器使用两级页表缓存:第一级TLB分为指令TLB和数据TLB,好处是取指令和取数据可以并行执行;第二级TLB是统一TLB(Unified TLB),即指令和数据共用的TLB。
1.1 TLB表项格式
不同处理器架构的TLB表项的格式不同。ARM64处理器的每条TLB表项不仅包含虚拟地址和物理地址,也包含属性:内存类型、缓存策略、访问权限、地址空间标识符(Address Space Identifier,ASID)和虚拟机标识符(Virtual Machine Identifier,VMID)。地址空间标识符区分不同进程的页表项,虚拟机标识符区分不同虚拟机的页表项。
1.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是虚拟内存区域,start是起始地址,end是结束地址(不包括) |
void flush_tlb_page(struct vm_area_struct *vma, unsigned long uaddr); | 使指定用户地址空间里面的指定虚拟页的TLB表项失效参数vma是虚拟内存区域,uaddr是一个虚拟页中的任意虚拟地址 |
void flush_tlb_kernel_range(unsigned long start, unsigned long end); | 使内核的某个虚拟地址范围的TLB表项失效参数start是起始地址,end是结束地址(不包括) |
void update_mmu_cache(struct vm_area_struct *vma, unsigned long address, pte_t *ptep); | 修改页表项以后把页表项设置到页表缓存由软件管理页表缓存的处理器必须实现该函数,例如MIPS处理器ARM64处理器的内存管理单元可以访问内存中的页表,把页表项复制到页表缓存,所以ARM64架构的函数update_mmu_cache什么都不用做 |
void tlb_migrate_finish(struct mm_struct *mm); | 内核把进程从一个处理器迁移到另一个处理器以后,调用该函数以更新页表缓存或上下文特定信息 |
当TLB没有命中的时候,ARM64处理器的内存管理单元自动遍历内存中的页表,把页表项复制到TLB,不需要软件把页表项写到TLB,所以ARM64架构没有提供写TLB的指令。
ARM64架构提供了一条TLB失效指令:
TLBI <type><level>{IS} {, <Xt>}
(1)字段的常见选项如下:
1)ALL:所有表项。
2)VMALL:当前虚拟机的阶段1的所有表项,即表项的VMID是当前虚拟机的VMID。虚拟机里面运行的客户操作系统的虚拟地址转换成物理地址分两个阶段:第1阶段把虚拟地址转换成中间物理地址,第2阶段把中间物理地址转换成物理地址。
3)VMALLS12:当前虚拟机的阶段1和阶段2的所有表项。
4)ASID:匹配寄存器Xt指定的ASID的表项。
5)VA:匹配寄存器Xt指定的虚拟地址和ASID的表项。
6)VAA:匹配寄存器Xt指定的虚拟地址并且ASID可以是任意值的表项。
(2)字段指定异常级别,取值如下:
1)E1:异常级别1。
2)E2:异常级别2。
3)E3:异常级别3。
(3)字段IS表示内部共享(Inner Shareable),即多个核共享。如果不使用字段IS,表示非共享,只被一个核使用。在SMP系统中,如果指令TLBI不携带字段IS,仅仅使当前核的TLB表项失效;如果指令TLBI携带字段IS,表示使所有核的TLB表项失效。
(4)字段Xt是X0~X30中的任何一个寄存器。
例如ARM64内核实现了函数flush_tlb_all,用来使所有核的所有TLB表项失效,其代码如下:
arch/arm64/include/asm/tlbflush.h
static inline void flush_tlb_all(void)
{
dsb(ishst);
__tlbi(vmalle1is);
dsb(ish);
isb();
}
把宏展开以后是:
static inline void flush_tlb_all(void)
{
asm volatile("dsb ishst" : : : "memory");
asm ("tlbi vmalle1is" : :);
asm volatile("dsb ish" : : : "memory");
asm volatile("isb" : : : "memory");
}
dsb ishst:确保屏障前面的存储指令执行完。dsb是数据同步屏障(Data Synchronization Barrier),ishst中的ish表示共享域是内部共享(inner shareable),st表示存储(store),ishst表示数据同步屏障指令对所有核的存储指令起作用。
tlbi vmalle1is:使所有核上匹配当前VMID、阶段1和异常级别1的所有TLB表项失效。
dsb ish:确保前面的TLB失效指令执行完。ish表示数据同步屏障指令对所有核起作用。
isb:isb是指令同步屏障(Instruction Synchronization Barrier),这条指令冲刷处理器的流水线,重新读取屏障指令后面的所有指令。
可以对比一下,ARM64内核实现了函数local_flush_tlb_all,用来使当前核的所有TLB表项失效,其代码如下:
arch/arm64/include/asm/tlbflush.h
static inline void local_flush_tlb_all(void)
{
dsb(nshst);
__tlbi(vmalle1);
dsb(nsh);
isb();
}
和函数flush_tlb_all的区别如下:
(1)指令dsb中的字段ish换成了nsh,nsh是非共享(non-shareable),表示数据同步屏障指令仅仅在当前核起作用。
(2)指令tlbi没有携带字段is,表示仅仅使当前核的TLB表项失效。
1.3 地址空间标识符
为了减少在进程切换时清空页表缓存的需要,ARM64处理器的页表缓存使用非全局(not global,nG)位区分内核和进程的页表项(nG位为0表示内核的页表项),使用地址空间标识符(Address Space Identifier,ASID)区分不同进程的页表项。
ARM64处理器的ASID长度是由具体实现定义的,可以选择8位或者16位,寄存器ID_AA64MMFR0_EL1(AArch64内存模型特性寄存器0,AArch64 Memory Model Feature Register 0)的字段ASIDBits存放处理器支持的ASID长度。如果具体实现支持16位ASID,那么可以使用寄存器TCR_EL1(转换控制寄存器,Translation Control Register)的AS(ASID Size)位控制实际使用的ASID长度。如果把AS位设置成0,表示使用8位ASID,否则表示使用16位ASID。
寄存器TTBR0_EL1(转换表基准寄存器0,Translation Table Base Register 0)或TTBR1_EL1都可以用来存放当前进程的ASID,寄存器TCR_EL1的A1位决定使用哪个寄存器存放当前进程的ASID,通常使用寄存器TTBR0_EL1。寄存器TTBR0_EL1的位[63:48]存放当前进程的ASID(如果使用8位ASID,那么寄存器TTBR0_EL1的位[63:56]是保留位),位[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。
1)如果存在空闲的硬件ASID,那么选择一个分配给进程。
2)如果没有空闲的硬件ASID,那么把全局ASID版本号加1,重新从1开始分配硬件ASID,即硬件ASID从255回绕到1。因为刚分配的硬件ASID可能和某个进程的硬件ASID相同,只是ASID版本号不同,页表缓存可能包含了这个进程的页表项,所以必须把所有处理器的页表缓存清空。
引入ASID版本号的好处是:避免每次进程切换都需要清空页表缓存,只需要在硬件ASID回绕时把处理器的页表缓存清空。
内存描述符的成员context存放架构特定的内存管理上下文,数据类型是结构体mm_context_t,ARM64架构定义的结构体如下所示,成员id存放内核给进程分配的软件ASID:
arch/arm64/include/asm/mmu.h
typedef struct {
atomic64_t id;
…
} mm_context_t;
全局变量asid_bits保存ASID长度,全局变量asid_generation的高56位保存全局ASID版本号,位图asid_map记录哪些ASID被分配。
每处理器变量active_asids保存处理器正在使用的ASID,即处理器正在执行的进程的ASID;每处理器变量reserved_asids存放保留的ASID,用来在全局ASID版本号加1时保存处理器正在执行的进程的ASID。处理器给进程分配ASID时,如果ASID分配完了,那么把全局ASID版本号加1,重新从1开始分配ASID,针对每个处理器,使用该处理器的reserved_asids保存该处理器正在执行的进程的ASID,并且把该处理器的active_asids设置为0。
active_asids为0具有特殊含义,说明全局ASID版本号变化,ASID从255回绕到1。
当全局ASID版本号加1时,每个处理器需要清空页表缓存,位图tlb_flush_pending保存需要清空页表缓存的处理器集合:
arch/arm64/mm/context.c
static u32 asid_bits;
static atomic64_t asid_generation;
static unsigned long *asid_map;
static DEFINE_PER_CPU(atomic64_t, active_asids);
static DEFINE_PER_CPU(u64, reserved_asids);
static cpumask_t tlb_flush_pending;
当进程被调度时,函数check_and_switch_context负责检查是否需要给进程重新分配ASID,其代码如下:
__schedule() -> context_switch() -> switch_mm_irqs_off() -> switch_mm() -> check_and_switch_context()arch/arm64/mm/context.c
1 void check_and_switch_context(struct mm_struct *mm, unsigned int cpu)
{
unsigned long flags;
u64 asid;
asid = atomic64_read(&mm->context.id);
if (!((asid ^ atomic64_read(&asid_generation)) >> asid_bits)
&& atomic64_xchg_relaxed(&per_cpu(active_asids, cpu), asid))
goto switch_mm_fastpath;
raw_spin_lock_irqsave(&cpu_asid_lock, flags);
asid = atomic64_read(&mm->context.id);
if ((asid ^ atomic64_read(&asid_generation)) >> asid_bits) {
asid = new_context(mm, cpu);
atomic64_set(&mm->context.id, asid);
}
if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending))
local_flush_tlb_all();
atomic64_set(&per_cpu(active_asids, cpu), asid);
raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);
switch_mm_fastpath:
if (!system_uses_ttbr0_pan())
cpu_switch_mm(mm->pgd, mm);
}
1.4 虚拟机标识符
虚拟机里面运行的客户操作系统的虚拟地址转换成物理地址分两个阶段:第 1 阶段把虚拟地址转换成中间物理地址,第 2 阶段把中间物理地址转换成物理地址。第 1 阶段转换由客户操作系统的内核控制,和非虚拟化的转换过程相同。第 2 阶段转换由虚拟机监控器控制,虚拟机监控器为每个虚拟机维护一个转换表,分配一个虚拟机标识符(Virtual Machine Identifier,VMID),寄存器VTTBR_EL2(虚拟化转换表基准寄存器,Virtualization Translation Table Base Register)存放当前虚拟机的阶段2转换表的物理地址。每个虚拟机有独立的ASID空间,页表缓存使用虚拟机标识符区分不同虚拟机的转换表项,可以避免每次虚拟机切换都要清空页表缓存,只需要在虚拟机标识符回绕时把处理器的页表缓存清空。
ARM64处理器的VMID长度是具体实现定义的,可以选择8位或者16位,寄存器ID_AA64MMFR0_EL1的字段VMIDBits存放处理器支持的VMID长度。如果具体实现支持16位VMID,可以使用寄存器VTCR_EL2(虚拟化转换控制寄存器,Virtualization Translation Control Register)的VS(VMID Size)位控制实际使用的VMID长度。如果把VS位设置成0,表示使用8位VMID,否则表示使用16位VMID。
寄存器VTTBR_EL2的位[63:48]存放正在运行的虚拟机的标识符,如果使用8位VMID,那么寄存器VTTBR_EL2的位[63:56]是保留位。