$ /docs/mmu/ cat page-table-base-registers.md

三种 CPU 架构的 页表基址寄存器
位图 · 字段含义 · Linux 内核使用

三个寄存器、三种设计哲学、三个 Linux 内核子系统。本文把 x86 CR3ARM64 TTBR0/TTBR1RISC-V satp 摆在同一张桌面上,逐一拆解每个字段,并附上 Linux 6.x 内核中真实使用它们的代码路径。

x86_64 · CR3 · 64-bit · PCID 可选 ARM64 · TTBR0_EL1 / TTBR1_EL1 · 64-bit · 内置 ASID RISC-V · satp · 64-bit / 32-bit · MODE 字段

x86_64 · CR3

PAGE-DIRECTORY BASE · x86

CR3 在长模式(64-bit)下是 PML4(Page Map Level 4)表的物理基址寄存器。 写入 CR3 会隐含地触发一次非全局 TLB 刷新(除非开启 PCID + noflush)。 Linux 借助 CR4.PCIDEINVPCID 指令实现「进程 PCID + 内核 PCID」双标签,从而把 KPTI(Page Table Isolation,又称 PTI)开销压到最低。

CR3 64-bit · 长模式分页结构基址 · 写入即触发 TLB 刷新
bit 63 bit 32 bit 0

Linux 如何使用 CR3

arch/x86/mm/tlb.c · flush_tlb_mm_range()
 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
arch/x86/mm/cpu_entry_area.c · cr3_set_user_pcid()
 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
arch/x86/entry/entry_64.S · 中断返回切换 CR3
 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

流程:进程切换中 CR3 的写时序

schedule() 选定 next switch_to() → __switch_to switch_mm_irqs_off() load_new_mm_cr3 → __write_cr3() CR3 ← PGD_PA | PCID bit63=noflush? 1: 硬件 flush 非全局 MMU 重新走 PML4 新地址空间生效 • 无 PCID:每次写 CR3 → 全 TLB flush(昂贵) • 有 PCID:不同 PCID 的 TLB 条目互不干扰,回切进程时仅需 INVPCID type=1 刷掉旧 PCID 即可 • KPTI:用户/内核各自 PCID(0 / 1),切换位图但保留 TLB

ARM64 · TTBR0_EL1 / TTBR1_EL1

VMSAv8-64 · 双翻译表基址 · 内置 ASID

ARM64 用 两个翻译表基址寄存器 把虚拟地址空间劈成两半:bit 63 = 0 走 TTBR0_EL1(用户空间,per-process),bit 63 = 1 走 TTBR1_EL1(内核空间,global)。 两个寄存器都用 VMSAv8-64 长描述符格式把 16-bit ASID 与页表基址打包在一起, 不需要额外的 PCID 寄存器。KPTI 通过在异常进出时把 TTBR1_EL1swapper_pg_dirtramp_pg_dir 之间互换实现。

TTBR0_EL1 低虚拟地址空间 · 用户态 · 进程切换时改写
bit 63 bit 32 bit 0
TTBR1_EL1 高虚拟地址空间 · 内核 · 启动时一次写入(KPTI 下多次切换)
bit 63 bit 32 bit 0

Linux 如何使用 TTBR0/1

arch/arm64/include/asm/mmu_context.h · cpu_switch_mm()
 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
arch/arm64/mm/context.c · 新 ASID 分配与 flush
 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
arch/arm64/kernel/entry.S · KPTI TTBR1 切换
 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

流程:进程切换 + KPTI 进出时 TTBR0/1 的写时序

进程 A 运行中 TTBR0=PGD_A, ASID=5 schedule() → B cpu_switch_mm(PDG_B, mm_B) TTBR0 ← PGD_B | ASID=8 TLB 中 ASID=5 条目自然失效 进程 B 用户态运行 TTBR1 = swapper_pg_dir EL0 用户态 trampoline 返回前 TTBR1 ← tramp_pg_dir tramp_exit() / KPTI on 用户代码开始执行 只能访问用户地址空间 EL0 → EL1 异常 tramp_enter 恢复 swapper • EL0 硬件只读 TTBR0 → KPTI 把「不该看到的内核页」从 TTBR1 抹掉 • 软件 PAN:临时把 TTBR0 指向 reserved_pg_dir 防止内核误访用户 • ASID 16-bit → 65536 个进程,超过则 bump version 全部失效

RISC-V · satp

S-MODE · 单寄存器 · MODE 字段选翻译方案

RISC-V 只有一个 satp(Supervisor Address Translation and Protection)寄存器, 它把 翻译方案选择 (MODE)ASID根页表 PPN 三件事塞进同一个 64-bit 寄存器。 没有 TTBR 切换、没有 PCID noflush 标志位;TLB 维护全部由 sfence.vma 指令承担。 Linux RISC-V 因此实现相对干净——一个寄存器、一个汇编宏。

satp (RV64 · Sv39 / Sv48 / Sv57) 64-bit · MODE [63:60] / ASID [59:44] / PPN [43:0]
bit 63 bit 32 bit 0
satp (RV32 · Sv32) 32-bit · MODE [31] / ASID [30:22] / PPN [21:0]
bit 31 bit 16 bit 0
MODE = 0
Bare · 关闭分页 · VA = PA
MODE = 1
Reserved (Sv32 仅 RV32)
MODE = 8
Sv39 · 39-bit VA · 3 级页表
MODE = 9
Sv48 · 48-bit VA · 4 级页表
MODE = 10
Sv57 · 57-bit VA · 5 级页表
MODE = 11
Sv64 · 64-bit VA · 6 级页表

Linux 如何使用 satp

arch/riscv/include/asm/csr.h · CSR_SATP 写宏
 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
arch/riscv/mm/context.c · 切换 mm 时写 satp
 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
arch/riscv/mm/tlbflush.c · 多核远程 TLB 刷新
 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

流程:RISC-V satp 写 + sfence.vma 序列

构造 satp 值 PPN | (ASID << 44) | (MODE << 60) csrw satp, value 硬件立即切换根页表 sfence.vma rs1, rs2 旧 ASID 条目失效(保留全局) 新地址空间生效 MMU 用新 satp 重走页表 • sfence.vma x0, x0 → 刷所有非全局条目(最重) • sfence.vma x0, asid → 刷指定 ASID 的所有条目(推荐) • sfence.vma va, asid → 精确刷单个 VA 的某 ASID 条目 • 多核:写完本地 satp 后通过 SBI_REMOTE_SFENCE 通知其他 hart

三架构横向对比

同一个「保存根页表基址」的需求,三种实现各有侧重。把这张表和上面的位图对照看, 你就能理解 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 源码。

Intel® 64 and IA-32 Architectures Software Developer's Manual, Vol. 3A
Chapter 4 — Paging · Section 4.10 PCID / Section 4.10.2 INVPCID
ARM Architecture Reference Manual ARMv8-A
Chapter D8 — The AArch64 Virtual Memory System Architecture (VMSAv8-64) · TTBR0_EL1 / TTBR1_EL1
The RISC-V Instruction Set Manual, Volume II: Privileged Architecture
Chapter 4 — Supervisor-Level ISA · §4.1.1 satp · §4.2 SFENCE.VMA
Linux kernel — arch/x86/mm/, arch/arm64/mm/, arch/riscv/mm/
cr3_set_user_pcid · cpu_switch_mm · kpti_install_ng_mappings · set_mm · flush_tlb_mm_range
Documentation/x86/pti.rst
Page Table Isolation (KPTI / PTI) on x86
Documentation/arm64/memory.rst
AArch64 Linux memory layout & TTBR0/1 selection by VA[63]
AArch64 Kernel Page Tables (wenboshen.org)
swapper_pg_dir / idmap_pg_dir / tramp_pg_dir 详解
kernel-internals.org — ARM64 Page Tables
TTBR0/1, translation granules, PTE format, ASID, Stage 2