本文分析基于Linux-5.10.100,架构基于ARM64 V8.3,假设页表映射层级为4,即CONFIG_ARM64_PGTABLE_LEVELS=4,地址宽度为48,即CONFIG_ARM64_VA_BITS=48。
在Linux系统中,通过MMU进行虚实地址转换时,会依赖于TTBR1_EL1和TTBR0_EL0两个寄存器。其中TTBR1_EL1指向内核地址空间的页表基地址,TTBR0_EL0指向用户地址空间的页表基地址。
Linux中所有内核线程共享内核地址空间,因此TTBR1_EL1中的值为swapper_pg_dir, swapper_pg_dir在arm64/include/asm/pgtable.h中的定义为:
extern pgd_t swapper_pg_dir[PTRS_PER_PGD];
在使用有效虚拟地址长度为48位,四级页表映射(CONFIG_PGTABLE_LEVELS=4)的情况下,PTRS_PER_PGD为512,即pgd表项共512项。swapper_pg_dir挂载在内核线程共用的mm_struct init_mm中:
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
MMAP_LOCK_INITIALIZER(init_mm)
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.arg_lock = __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
.user_ns = &init_user_ns,
.cpu_bitmap = CPU_BITS_NONE,
INIT_MM_CONTEXT(init_mm)
};
进程地址空间相互隔离,因此TTBR0_EL0中的值为每个进程独有的,在进程描述符task_struct中存在对内存地址空间的定义:
struct task_struct {
...
struct mm_struct *mm;
struct mm_struct *active_mm;
...
}
其中对于用户进程来说,mm和active_mm均指向其用户地址空间,而对于内核线程,mm为NULL,active_mm指向其借用的那个进程的进程地址空间。地址空间的页表基址定义在mm_struct中:
struct mm_struct {
...
pgd_t * pgd;
...
};
在进程切换时,将task_struct->mm->pgd加载到TTBR0_EL0中以实现对进程地址空间的切换。
TTBR1寄存器的变化
在内核启动过程中,首先实现的是内核的恒等映射,内核此时使用的是一级页表init_idmap_pg_dir,放在TTBR0寄存器中,TTBR1寄存器中保存的是空白页表reserved_pg_dir,将整个kernel、FDT以及预留的swap区域的虚拟地址连续映射到相等的物理地址处,此处的内核仅具备读、执行权限。
随后,将设置好的页表init_pg_dir放入TTBR1寄存器,将page walk使用的寄存器由TTBR0转换为TTBR1,代码开始在EL1区域内运行。
最后,通过paging_init实现页初始化,将swapper_pg_dir加载到TTBR1寄存器并且使用swapper_pg_dir替换init_mm的pgd:
1
2
3
4
5
6
7
void __init paging_init(void)
{
...
cpu_replace_ttbr1(lm_alias(swapper_pg_dir));
init_mm.pgd = swapper_pg_dir;
...
}
Arm64支持ASID(Address Space ID),为每个进程分配一个ASID,标识自己的进程地址空间,以避免在进程切换时刷新TLB,性能。TLB entry的ASID来自于TTBRx_EL1寄存器,在TLB block在缓存的同时,将当前进程的ASID缓存到对应的TLB entry中。在进程运行的过程中,TTBR1寄存器的高位会加入当前进程的ASID的信息,验证代码如下:
static void switch_ttbr1() {
unsigned long orig_ttbr1;
asm volatile(
" mrs %0, ttbr1_el1\n"
" msr ttbr1_el1, %0\n"
: "=r"(orig_ttbr1)
:
);
printk("ttbr1_el1: 0x%llx\n", orig_ttbr1);
pgd_t *pgdp = lm_alias(swapper_pg_dir);
phys_addr_t ttbr1 = phys_to_ttbr(virt_to_phys(pgdp));
if (system_supports_cnp() && !WARN_ON(pgdp != lm_alias(swapper_pg_dir)))
ttbr1 |= TTBR_CNP_BIT;
unsigned long asid = ASID(current->active_mm);
ttbr1 &= ~TTBR_ASID_MASK;
ttbr1 |= FIELD_PREP(TTBR_ASID_MASK, asid);
printk("ttbr1 with ASID: 0x%llx\n", ttbr1);
write_sysreg(ttbr1, ttbr1_el1);
isb();
post_ttbr_update_workaround();
}
通过qemu启动内核,触发上述代码,通过dmesg查看得到:
其中swapper_pg_dir所对应的物理地址为0x0000417e2001,而该内核线程对应进程的ASID为0x0344,二者或运算得到完整的TTBR1_EL1寄存器的值。