三个寄存器、三种设计哲学、三个 Linux 内核子系统。本文把 x86 CR3、 ARM64 TTBR0/TTBR1 与 RISC-V satp 摆在同一张桌面上,逐一拆解每个字段,并附上 Linux 6.x 内核中真实使用它们的代码路径。
CR3 在长模式(64-bit)下是 PML4(Page Map Level 4)表的物理基址寄存器。
写入 CR3 会隐含地触发一次非全局 TLB 刷新(除非开启 PCID + noflush)。
Linux 借助 CR4.PCIDE 与 INVPCID 指令实现「进程 PCID + 内核 PCID」双标签,从而把
KPTI(Page Table Isolation,又称 PTI)开销压到最低。
1/* 进程退出 / munmap 触发 TLB shootdown:先取 mm->context.pcid, 2 再由 INVPCID type=1 精确刷新该 PCID 对应条目(不开 PCID 则降级为全刷) */ 3void flush_tlb_mm_range(struct mm_struct *mm, unsigned long start, 4 unsigned long end, unsigned int stride, 5 unsigned long vma __always_unused) 6{ 7 unsigned long pcids = pcid_set_all(); 8 if (cpu_feature_enabled(X86_FEATURE_INVPCID)) { 9 invpcid_flush_all(pcids); // INVPCID type=3 10 } else { 11 __flush_tlb_all(); // mov cr3, cr3 12 } 13} 14
1/* PTI 关键路径:syscall 入口 / 中断返回时切换到「用户页表 + 用户 PCID」 2 返回内核时切回「用户页表 + 内核 PCID」以减少 TLB 刷新 */ 3static inline void cr3_set_user_pcid(unsigned long pcid) 4{ 5 unsigned long cr3 = __pa(current->mm->pgd); 6 cr3 |= pcid; // bits 11:0 = PCID 7 if (static_cpu_has(X86_FEATURE_PCID)) 8 this_cpu_write(cpu_tlbstate.cr3, cr3); 9 __write_cr3(cr3); 10} 11
1/* 从中断返回到用户态:若当前 CR3 仍指向「内核 PCID」,切回「用户 PCID」 2 利用 noflush 标志(bit 63)避免再次全刷 TLB */ 3SYM_CODE_START_LOCAL(.Lparanoid_exit_restore) 4 movq %rsp, %rdi 5 call paranoid_exit_restore 6 ... 7 SWITCH_TO_USER_CR3_NOSPEC scratch_reg=%rdi // 关键切换宏 8 ... 9SYM_CODE_END(.Lparanoid_exit_restore) 10
ARM64 用 两个翻译表基址寄存器 把虚拟地址空间劈成两半:bit 63 = 0 走
TTBR0_EL1(用户空间,per-process),bit 63 = 1 走 TTBR1_EL1(内核空间,global)。
两个寄存器都用 VMSAv8-64 长描述符格式把 16-bit ASID 与页表基址打包在一起,
不需要额外的 PCID 寄存器。KPTI 通过在异常进出时把 TTBR1_EL1 在
swapper_pg_dir 与 tramp_pg_dir 之间互换实现。
1/* 进程切换:把 next->mm 的 PGD 物理地址 + ASID 写进 TTBR0_EL1。 2 BUG_ON 防止内核页表 swapper_pg_dir 被错误地塞到 TTBR0。 3 ASID 取自 mm->context.id 的低 16 位。 */ 4static inline void cpu_switch_mm(pgd_t *pgd, struct mm_struct *mm) 5{ 6 BUG_ON(pgd == swapper_pg_dir); 7 cpu_set_reserved_ttbr0(); 8 cpu_do_switch_mm(virt_to_phys(pgd), mm); 9} 10
1/* ASID allocator:版本号 + 位图。版本递增意味着「所有旧 ASID 已失效」 2 此时无需遍历,直接把所有 CPU 的 TTBR0.ASID 一起刷掉。 */ 3static void flush_context(void) 4{ 5 int cpu; u64 asid; 6 write_seqcount_begin(&asid_lock); 7 asid = ++asid_generation; // bump version 8 cpumask_clear(cpu_asid_map); 9 write_seqcount_end(&asid_lock); 10 smp_wmb(); 11 cpumask_call_function(asid_map, 12 tlbi_context_asid, void *arg); // local TLBI 13} 14
1/* KPTI:从 EL0 回到 EL1 时,把 TTBR1 切回 swapper_pg_dir(完整内核页表)。 2 进入 EL0 之前则切到 tramp_pg_dir(仅含 trampoline 代码段)。 */ 3SYM_CODE_START_LOCAL(tramp_exit) 4 msr ttbr1_el1, x1 // x1 = tramp_pg_dir 物理地址 5 isb 6 ... 7SYM_CODE_END(tramp_exit) 8/* 等价 C 路径在 mmu.c 的 kpti_install_ng_mappings() 里建表。 */ 9
RISC-V 只有一个 satp(Supervisor Address Translation and Protection)寄存器,
它把 翻译方案选择 (MODE)、ASID、根页表 PPN 三件事塞进同一个 64-bit 寄存器。
没有 TTBR 切换、没有 PCID noflush 标志位;TLB 维护全部由 sfence.vma 指令承担。
Linux RISC-V 因此实现相对干净——一个寄存器、一个汇编宏。
1/* 写入 satp: 2 satp = (mode << 60) | (asid << 44) | (ppn) 3 ppn = root_page_table / PAGE_SIZE */ 4static inline void csr_write(unsigned long val, unsigned long csr) 5{ 6 if (csr == CSR_SATP) 7 __asm__ volatile("csrw satp, %0" :: "rK"(val) : "memory"); 8 else 9 __asm__ volatile("csrw %0, %1" :: "i"(csr), "rK"(val) : "memory"); 10} 11
1/* 进程切换:把新 mm 的 PGD 物理页号 + ASID 写进 satp。 2 写入之后必须 sfence.vma 同步 TLB。 */ 3static inline void set_mm(struct mm_struct *prev, 4 struct mm_struct *next, 5 unsigned long cpu) 6{ 7 unsigned long satp = __pa(next->pgd); 8 satp |= (cntx2asid(atomic_long_read(&next->context.id)) << 44); 9 satp |= satp_mode; 10 csr_write(satp, CSR_SATP); 11 flush_tlb_mm(next); // 本地 sfence.vma 12} 13
1/* RISC-V 的 sfence.vma 只刷当前 hart。需要 SBI / IPI 通知其他 hart。 2 现代内核一律走 ASID 变体 sfence.vma 以保留全局 (G) 项。 */ 3void flush_tlb_mm(struct mm_struct *mm) 4{ 5 unsigned long asid = get_mm_asid(mm); 6 __sbi_tlb_flush_range(mm, 0, 0, PAGE_SIZE); 7 // 内部判断是否要广播 sbi_remote_sfence_vma_asid() 8} 9
同一个「保存根页表基址」的需求,三种实现各有侧重。把这张表和上面的位图对照看,
你就能理解 Linux 内核里那些看似冗余的宏(__write_cr3 / cpu_switch_mm /
csr_write(CSR_SATP))为什么会被写成现在这个样子。
| 维度 | x86_64 | ARM64 | RISC-V |
|---|---|---|---|
| 寄存器 | CR3 (64-bit) | TTBR0_EL1 + TTBR1_EL1 | satp (64-bit / 32-bit) |
| 用户/内核基址分离 | 单寄存器(KPTI 切换 PGD) | 天然分离(按 VA bit 63 选) | 单寄存器(Linux 自己分两套 satp) |
| ASID 字段 | CR3 隐式(CR3[11:0] = PCID) | TTBR0[63:48](16-bit) | satp[59:44](16-bit) |
| ASID 大小 | 12-bit (4096) | 16-bit (65536) | 16-bit (65536) |
| 页表基址字段 | CR3[51:12](40-bit 物理) | TTBRx[47:1](4KB 粒度) | satp[43:0](44-bit PPN) |
| 分页方案选择 | 由 CR4.PAE / CR4.LA57 等决定 | 由 TCR_EL1.TGx / TTxSZ 决定 | 由 satp.MODE 直接编码(0/8/9/10/11) |
| 切换指令 | MOV CR3, rax(mov-to-CR3) | MSR TTBR0_EL1, x0 | CSRRW satp, a0 |
| TLB 维护指令 | MOV CR3 / INVLPG / INVPCID | TLBI 系列 / 隐式于 TTBR 写 | SFENCE.VMA (rs1=VA, rs2=ASID) |
| 多核 TLB shootdown | IPI → 受影响 CPU INVPCID | IPI → TLBI 指令 + SMMU | IPI + SBI remote fence |
| KPTI / PTI 实现 | 额外维护 user-pgd & kernel-pgd,PCID 标签 0/1 | TTBR1 在 swapper/tramp 之间切 | 暂无强制 KPTI;单寄存器方案天然不易 |
| ASID 耗尽策略 | invpcid 全局 flush | asid_generation++ + 全 TLBI | flush_tlb_mm 走全局 sfence |
| Linux 内核入口 | __switch_to / cr3_set_* | cpu_switch_mm / __switch_to | switch_to / set_mm / csr_write |
本文技术细节均来自以下资料:Intel SDM Volume 3A、ARM Architecture Reference Manual ARMv8-A、 RISC-V Privileged ISA Manual 以及 Linux kernel 6.x 源码。