Linux内核分析之内存管理-01

This language version is unavailable; showing the other language.

19.1 多级页表结构 —— PGD 到 PTE

要管理页表,内核必须先回答一个抽象问题:页表长什么样? 每个架构的答案不同——x86_64 四或五级、ARM64 三到五级可配、RISC-V 三到五级运行时冻结——但内核的核心子系统(内存管理、缺页处理、页表复制)不能为每个架构写一套。于是 Linux 用五级固定命名的层级抽象(pgd/p4d/pud/pmd/pte)把"树多深"包装成编译期可折叠的宏系统:没有 p4d/pud 的架构会把对应宏退化为直通透传。本节结合 Linux 7.0.10 源码分析这套抽象的类型系统、遍历表达式与共享页表语义。


19.1.1 表项类型:一维包装的语义屏障

每个页表项在物理上都是一个 64 位(或 32 位)整数,但内核坚持用单字段结构体包装:

// arch/x86/include/asm/pgtable_types.h:293-368(节选)
typedef struct pgprot { pgprotval_t pgprot; } pgprot_t; /* :293 权限位集合 */
typedef struct { pgdval_t pgd; } pgd_t;         /* :295 顶级表项 */
typedef struct { p4dval_t p4d; } p4d_t;         /* :342 第四级 */
typedef struct { pudval_t pud; } pud_t;         /* :368 第三级 */
/* pmd_t/pte_t 同理 */

单字段结构体不是脱裤子放屁——它在类型系统层面建立了语义屏障:

  1. pmd_t 不能隐式赋给 pte_t:两者位布局不同(PMD 表项是"指向下一级表"或"块映射",PTE 才是页帧+权限),混用即 bug;
  2. 赋值必须经 __pmd(value)/pmd_val(pmd) 显式进出:#define __pgd(x) native_make_pgd(x)(arch/x86/include/asm/pgtable.h:100)、#define pgd_val(x) native_pgd_val(x)(pgtable.h:99),grep 点使代码审计可以精确找到所有"裸值进出页表"的位置;
  3. pgprot_t 把权限位与地址位分离:mk_pte(page, prot) 组装时才合并,权限策略(如 PAGE_KERNEL 变体)可以独立于物理地址演进。

19.1.2 五级抽象与宏折叠

内核固定命名五级:PGD → P4D → PUD → PMD → PTE。每级一张物理页大小的表(x86_64/ARM64/RISC-V 均为 4KB/512 项),表项数由 PTRS_PER_* 描述。架构不支持的层级通过 include/asm-generic/pgtable-nop*.h 折叠成"单元素表"——pud_offset() 直接返回 p4d 指针、pud_val() 直接透传,遍历代码无需感知。三级索引链构成遍历表达式:

// include/linux/pgtable.h:71
#define pgd_index(a)  (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))

// include/linux/pgtable.h:140-156
static inline pgd_t *pgd_offset_pgd(pgd_t *pgd, unsigned long address)
{ ... return pgd + pgd_index(address); }

#define pgd_offset(mm, address)     pgd_offset_pgd((mm)->pgd, (address))    /* :149 */
#define pgd_offset_k(address)       pgd_offset(&init_mm, (address))     /* :156 */

// include/linux/pgtable.h:95-100
static inline pte_t *pte_offset_kernel(pmd_t *pmd, unsigned long address)
{ return (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address); }

pgd_offset(mm, addr) 从 mm->pgd(进程顶级表基址,task_struct->mm)出发,按 PGDIR_SHIFT 取高位索引定位顶级表项;沿链下探由各级 _offset 宏接力,每级从当前表项中解出下一级表的物理页地址(x86 的 pmd_page_vaddr(),arch/x86/include/asm/pgtable.h:1022:(pmd_val(pmd) & PTE_PFN_MASK) >> PAGE_SHIFT 再 __va() 转虚拟地址)。无锁路径(GUP 快速遍历)使用 pud_offset_lockless()/pmd_offset_lockless()(include/linux/pgtable.h:2237-2240)——先取表项值再解引用,配合 RCU 与原子读保证不会访问已被释放的表页。

完整的软件遍历链 (以内核映射某地址为例)

pgd = pgd_offset_k(addr)                 ← init_mm.pgd 出发(内核地址)
  └─ p4d = p4d_offset(pgd, addr)         ← x86 四级时 p4d==pgd(折叠)
       └─ pud = pud_offset(p4d, addr)
            └─ pmd = pmd_offset(pud, addr)
                 ├─ pmd_large(*pmd) → 2MB 块映射终点
                 └─ pte = pte_offset_kernel(pmd, addr)
                      ├─ pte_none(*pte)      → 空表项(未映射)
                      └─ pte_page(*pte)      → struct page / 物理帧

为什么用多级而不是一级大表? 48 位虚拟地址、4KB 页的一级表需要 512GB 连续内存存放 2^36 个表项——比大多数机器的总内存还多。多级树把空间开销变成按需分配:只有实际映射的地址范围才分配下级表,一个只映射了 8MB 的进程只需 PGD+2×PMD+4×PTE 表(约 7 页)。代价是遍历时间(最多 5 次访存),由 TLB 缓存最热结果(19.5 节)。

19.1.3 内核表与用户表的关系

pgd_offset_k()(pgtable.h:156)揭示了一个关键事实:内核地址与用户地址共享同一张 PGD。init_mm.pgd(swapper_pg_dir)是内核自己的顶级表;普通进程的 mm->pgd 在创建时从 init_mm 拷贝内核半区(22 章展开 mm_alloc() 的细节)。这一共享带来两个推论:

  • 内核映射零成本共享:所有进程可见同一内核直接映射区,无需逐进程建立;
  • 切换进程只需换 PGD 一个指针:x86 写 CR3、ARM64 写 TTBR0、RISC-V 写 satp——下半区(内核半区)保持不变,因此内核页(全局页)的 TLB 项可以在切换后幸存(19.5 节 PG_GLOBAL 的意义)。
PGD 的上下两半 (x86_64 为例)

          用户半区 (低地址)              内核半区 (高地址)
   ┌─────────────────────────┬─────────────────────────┐
   │ 进程私有: 代码/堆/栈/mmap │ 所有进程共享: 直接映射/  │
   │ 进程切换时随 CR3 更换     │ vmalloc/vmemmap/内核映像 │
   └─────────────────────────┴─────────────────────────┘
     mm->pgd (每进程)              从 init_mm 拷贝, 生命周期
     pte 权限含 _PAGE_USER         与进程无关; 修改经 vmalloc/
                                   set_memory 等全局接口

19.1.4 表项的三种形态

每个中间级表项有两种形态,叶子级还有一种特殊形态,这套判别谓词是所有页表代码的基础:

形态 判别谓词(x86) 含义
空项 pud_none()/pmd_none()/pte_none() 未建立映射,下级表不存在
链接项 pud_present() && !pud_large() 指向下一级表(PFN 字段)
块/叶项 pud_large()/pmd_large()(PS 位) 直接映射 1GB/2MB 块,跳过下级
换出槽 pte_none() 且 swap 值非零 页在 swap(swp_entry_t 编码在 pte 值内,24 章)

块映射让"一棵树只需两级"成为可能:映射一段物理连续的 1GB 区域时,PUD 级直接写块项,省掉 PMD/PTE 两级表及其遍历开销。这也解释了第 23 章大页的意义——不仅是 TLB 效率,还有页表本身的空间与访问成本。

19.1.5 遍历代码的两种范式

全内核的页表遍历收敛为两种范式,理解它们能速读任何 MM 代码:

范式一:持锁显式遍历(写侧/精确路径)。调用者持 mmap_lock(读或写)+ 页表锁(ptl),逐级 _offset 定位。pte_offset_kernel() 的返回值直接解引用——锁保证表页不会被释放。缺页处理(22.4 节)、mprotect、页表填充(20.3 节 vmalloc)都是此范式:

/* 典型形状 (伪码, 22.4 节 handle_pte_fault 的骨架) */
pud = pud_offset(p4d, addr);          /* 逐级 _offset */
pmd = pmd_offset(pud, addr);
ptl = pte_lockptr(mm, pmd);           /* 表页锁 */
spin_lock(ptl);
pte = pte_offset_kernel(pmd, addr);   /* 定位表项 */
...判读与修改 *pte...
spin_unlock(ptl);

范式二:无锁快照遍历(读侧热路径)。GUP(get_user_pages)快速路径、per-VMA 锁下的缺页(22.2.4 节)不持页表锁:先用 pmd_offset_lockless()/pud_offset_lockless()(include/linux/pgtable.h:2237-2240)按值读表项,再验证"值指向的表页仍活着"(RCU 宽限期保证表页释放延迟),成功后原子地处理。19.1.2 节"先值后链"的纪律即指此——先读表项值、按值解引用,而不是先拿指针再解引用(指针可能在两步之间失效)。

两范式的分野:

           持锁显式                无锁快照
 锁         mmap_lock + ptl        RCU + 原子读 + seq 比对
 表页生存   锁保护                  RCU 宽限期延迟释放
 性能      每次锁开销              零锁, 但需回退协议
 失败处理   不失败(锁保证)           值过期 → 慢路径重试
 典型      do_wp_page, zap_*      GUP-fast, per-VMA 缺页

19.1.6 表页自身的分配与释放

多级树的每一级表页都是一张普通 4KB 页(18 章伙伴系统供货),但其生命周期有一层 RCU 包裹:pte_alloc_one() 分配、pte_free_defer()(RCU 延迟版)释放——释放必须延迟到无锁读者全部退场(范式二的前提)。zap 路径(22.3.5 节 mmu_gather 的 tlb_remove_table())把表页挂批次、刷 TLB 后经 RCU 释放,与页帧的"先失效后释放"铁律同构。表页记账计入 mm_pgtables_bytes()——OOM 评分(24.4.2 节 oom_badness 的第三正项)把"页表膨胀"也计入嫌疑,防的是恶意进程用 mmap 造海量空表耗尽内存的攻击。


小结

本节建立了页表的通用抽象:单字段结构体(pgd_t…pte_t)用类型系统隔离各级位布局语义;五级命名(PGD/P4D/PUD/PMD/PTE)配合宏折叠让"树深"成为架构私有细节;pgd_offset → pte_offset_kernel 的遍历表达式是全内核共享的公共路径,无锁变体以"先值后链"的纪律保证 RCU 安全。内核与用户地址共享 PGD,使进程切换退化为一个指针的替换;表项的空/链接/块三形态由统一谓词判别。下一节进入 x86_64 的具体实现:PTE 位布局、四级/五级布局与 CR3。

19.2 x86_64 四级/五级页表

19.1 节的抽象落到 x86_64 上:四级布局(48 位虚拟地址)是默认,五级布局(LA57,57 位)在支持并可配置时启用。本节逐位解析 x86_64 的 PTE 格式——PRESENT/RW/USER/ACCESSED/DIRTY/PSE/GLOBAL/NX 这些位如何支撑缺页处理、COW、回收与安全机制——并用一个具体地址走一遍完整翻译,最后解释 CR3/PCID 与内核页表的全局属性。


19.2.1 布局参数:四级与五级

// arch/x86/include/asm/pgtable_64_types.h:48-86(节选)
#define PGDIR_SHIFT pgdir_shift     /* :50 四级=39, 五级=48 (运行期变量) */
#define PTRS_PER_PGD    512         /* :51 */

#define P4D_SHIFT       39      /* :56 */
#define MAX_PTRS_PER_P4D    512
#define PTRS_PER_P4D        ptrs_per_p4d    /* :58 四级=1(折叠), 五级=512 */

#define PUD_SHIFT   30          /* :67  1GB 块映射阶 */
#define PTRS_PER_PUD    512

#define PMD_SHIFT   21          /* :74  2MB 块映射阶 */
#define PTRS_PER_PMD    512

#define PTRS_PER_PTE    512         /* :80 */

四级布局的索引切分:

48 位虚拟地址 (4KB 页, 四级):

 47            39 38        30 29        21 20        12 11         0
┌───────────────┬─────────────┬─────────────┬─────────────┬────────────┐
│  PGD 索引(9)  │ PUD 索引(9) │ PMD 索引(9) │ PTE 索引(9) │ 页内偏移(12)│
└───────────────┴─────────────┴─────────────┴─────────────┴────────────┘
      512          512           512           512            4096B
   PGDIR_SHIFT=39  PUD_SHIFT=30  PMD_SHIFT=21  PAGE_SHIFT=12

  PGD 一项覆盖 512GB (PGDIR_SIZE)
  用户空间: 47 位以下 128TB ; 内核空间: 47 位以上 128TB
  五级 (LA57): PGD 索引 9 位提到 56-48 位, PGDIR_SHIFT=48,
               单项覆盖 256TB, 虚拟地址 57 位 (MAXMEM 同步放宽)

pgdir_shift/ptrs_per_p4d 是运行期变量(在 2.9 节的 early_cpu_init() 前由 probe_page_size_mask() 确定),五级机器上未启用 LA57 时保持四级布局——同一内核映像在两种机器上都能运行,代价是顶级索引计算多一次内存读。

19.2.2 一次完整翻译:具体地址走读

用一个真实感地址 0x00007f3a_1234_5678(典型 mmap 区地址)走四级翻译:

 翻译 0x00007f3a12345678 (48 位):

 二进制切分:
  000000000111 11110011 10100010 01000110 10001010110 011001011000
  └─ PGD idx ─┘└─ PUD ─┘ (十进制值:)
     0x7 = 7      0xF3=243   PMD=0xA2=162  PTE=0x46B=1131  off=0x678

 每级表项的内容 (假设映射 4KB 页):

 CR3 ──┐
      ▼
 PGD[7]    = 0x1AB001063  → 指向 PUD 表 (物理页 0x1AB001)
      ▼ (+0x1AB001000)
 PUD[243]  = 0x1AC011067  → 指向 PMD 表 (物理页 0x1AC011)
      ▼
 PMD[162]  = 0x80000001AD5087
              ││ │└──── PTE 表物理帧 = 0x1AD5  (低位是权限位)
              ││ └─ bit7 (PS)=0 → 是 PTE 表, 不是 2MB 大页
              │└── bit1 RW=1, bit0 P=1
              ▼
 PTE[1131] = 0x800000012345887
              │            └─ 页帧号 = 0x12345 → 物理地址 0x12345678
              └─ (A 位可能已被访问置 1, D 位=1 写过)

 最终: 物理 = 0x12345000 + 0x678 = 0x12345678 (假定恒等映射的页帧)

 访存代价: 4 次页表读 + 1 次数据读
 (TLB 命中时: 1 次数据读 — 19.5 节缓存的全部意义)

若 PMD[162] 的 PS 位=1,则是 2MB 大页(块映射):PMD 表项直接编码 2MB 对齐的物理帧,PTE 级整个消失——翻译少一跳,这就是 23 章大页的硬件基础(19.2.4 节)。

19.2.3 PTE 位布局:一个 64 位字的全部智慧

// arch/x86/include/asm/pgtable_types.h:10-30(节选)
#define _PAGE_BIT_PRESENT   0   /* is present */
#define _PAGE_BIT_RW        1   /* writeable */
#define _PAGE_BIT_USER      2   /* userspace addressable */
#define _PAGE_BIT_PWT       3   /* page write through */
#define _PAGE_BIT_PCD       4   /* page cache disabled */
#define _PAGE_BIT_ACCESSED  5   /* was accessed (raised by CPU) */
#define _PAGE_BIT_DIRTY     6   /* was written to (raised by CPU) */
#define _PAGE_BIT_PSE       7   /* 4 MB (or 2MB) page */
#define _PAGE_BIT_PAT       7   /* on 4KB pages */
#define _PAGE_BIT_GLOBAL    8   /* Global TLB entry PPro+ */
#define _PAGE_BIT_SOFTW1    9   /* available for programmer */
...
#define _PAGE_BIT_PAT_LARGE 12  /* On 2MB or 1GB pages */
#define _PAGE_BIT_SOFTW4    57  /* available for programmer */
...
#define _PAGE_BIT_NX        63  /* No execute: only valid after cpuid check */
 63 62 61 ... 59 58 57 56 ... 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌──┬──┬──┬───┬──┬──┬──┬────┬──┬──┬──┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│NX│ K│  │AVL│SW│SW│  │PFN [51:12]   │PAT│ G│PS│A│D│PC│PW│ U│RW│P│
│  │E │  │   │5 │4 │  │              │   │  │  │ │ │D │T │SR│  │ │
└──┴──┴──┴───┴──┴──┴──┴────┴──┴──┴──┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
 位12: PAT  位9-11/58-62: 软件可用          位7: PSE(中间级)=块映射
 位8:  GLOBAL(切换 CR3 不刷)                位6: DIRTY(CPU 硬件置位)
 位5:  ACCESSED(CPU 硬件置位)               位1: RW  位2: USER
 位0:  PRESENT                              位63: NX (EFER.NXE 启用)

各位的内核语义与消费点:

位 硬件行为 内核消费点
P (bit 0) 0 则触发 #PF 缺页入口区分"未映射"与"权限拒绝"(25.1 节)
RW (bit 1) 违反写触发 #PF COW 检测:pte 禁写但 VMA 可写 → 复制页(25.3 节)
USER (bit 2) Ring3 访问触发 #PF 用户/内核隔离;SMEP 在此之上再加执行侧限制
A (bit 5) CPU 硬件置位 回收扫描器的年龄判断(24.1 节),软件定期清零重新计龄
D (bit 6) CPU 硬件置位 回写决策:未脏页可直接丢弃、脏页必须先写回(24/36 章)
PS (bit 7) 中间级表项启用块映射 2MB/1GB 大页(23 章)
G (bit 8) CR3 切换不失效该 TLB 内核直接映射区避免逐进程刷新(19.5 节)
NX (bit 63) 取指违反触发 #PF W^X 强制、vmalloc 模块区不可执行;依赖 EFER.NXE(2.6 节引导设置)
SOFTW1-5 硬件不碰 软件私有:如 _PAGE_BIT_SOFTW1=special、soft-dirty(用 SOFTW2,脏位跟踪)

A/D 位由硬件置位是 x86 的珍贵特性:回收器无需访问计数即可知道页是否被用过。ARM64 的对应机制(AF 位 + DBM)与 RISC-V(A/D 位)语义一致但异常处理不同——ARM64 老硬件上 A 位缺失会触发 Access Flag fault 由软件补记(19.3 节)。

A/D 位驱动的"软缺页 vs 硬缺页" (22.4.3 节矩阵的硬件基础):

 场景: 应用写一页已映射、可写、但 CPU 未置 D 位的页
 硬件行为: 自动置 D=1, 正常写执行 — 无异常!
 (内核唯一的知情途径: 下次扫描页表/回收时看到 D=1)
 → "零成本脏位跟踪"

 场景: 写一页 RW=0 的页 (COW 预埋)
 硬件行为: 拒绝 + 触发 #PF (错误码 WRITE|PROT)
 → 内核介入判定 COW (25.3 节)

19.2.4 CR3:翻译树的根指针与 PCID

// arch/x86/include/asm/special_insns.h:41-47
static __always_inline void native_write_cr3(unsigned long val)
{
    asm volatile("mov %0,%%cr3": : "r" (val) : "memory");
}
CR3 寄存器布局 (4KB 对齐的 PGD + PCID 标志):

 63                    12 11          0
┌────────────────────────┬─────────────┐
│  PGD 物理地址 (高位)     │  PCID/标志   │
└────────────────────────┴─────────────┘
 bit 0-11: 启用 PCIDE 时为 PCID (12 位, 4096 个进程标签)
           未启用时: bit3 PWT / bit4 PCD 等

 写 CR3 的副作用 (19.5 节 TLB 的核心):
   非 G 标志的 TLB 项 → 全部失效
   启用 PCID: 失效范围收窄到"旧 PCID 的项"
   → 新进程的 TLB 项得以幸存 (切回零成本)

CR3 低 12 位是标志(其中大半是 PCID,进程上下文标识,19.5 节展开),高位是 PGD 物理地址(4KB 对齐天然低 12 位为零)。native_write_cr3() 是进程地址空间切换的最底层动作——switch_mm() → load_new_mm_cr3()(arch/x86/mm/tlb.c)最终都落到它。PGD 表项里存放的物理页来自 mm->pgd——它由 pgd_alloc() 在进程创建时分配(18.3 节伙伴系统一页),并把内核半区从 init_mm 拷入。

19.2.5 块映射与大页位

PS 位让中间级表项直接变成叶项:

PUD 级 PS=1: 1GB 页  (PUD 表项直接编码 1GB 对齐物理帧)
PMD 级 PS=1: 2MB 页  (PMD 表项直接编码 2MB 对齐物理帧)

遍历路径对比 (映射 2MB 区域):
四级+4KB 页:  PGD → PUD → PMD → PTE 表(512 项×8B=1页) → 512 个 4KB 项
四级+2MB 页:  PGD → PUD → PMD(PS=1)                   ← 一项搞定, 无 PTE 表
收益: TLB 项覆盖 2MB; 页表内存省 1 页; 遍历访存少 1 级

页表内存账 (映射 1TB 内存的 PTE 元数据):
 全 4KB:  PTE 表共 2GB (1TB/2MB×4KB)
 全 2MB:  PMD 表共 16MB   ← 500 倍差距
 全 1GB:  PUD 直达, 元数据仅数 MB

内核在哪些地方用块映射:内核映像本身(init_mem_mapping() 优先 1GB/2MB 映射)、直接映射区(大块映射减少 TLB 压力)、以及 23 章的 THP/hugetlb 对用户进程的透明大页。page_size_mask(2.9 节 init_mem_mapping 参数)正是 PS 能力的探测结果。

19.2.6 内核页表的全局属性

PAGE_KERNEL 等常量组装了内核映射的标准权限:

PAGE_KERNEL     = P | RW | N  (可写不可执行, NX 必开)
PAGE_KERNEL_RO  = P | N       (只读不可执行: 内核只读段)
PAGE_KERNEL_EXEC= P | RW      (可执行: 模块加载后部分区域)
PAGE_KERNEL_IO  = ... | PWT/PCD 变体 (MMIO 映射)

7.0 内核已全面 W^X:可执行映射必须显式声明且运行期经 set_memory_ro/set_memory_x 严格切换(安全加固机制见 1.6 节与 20.5 节 KASLR 的配合)。_PAGE_BIT_SOFTW4(bit 57)等高位软件位因 52 位物理地址扩展而仍留有余量。


小结

x86_64 用 9+9+9+9+12 的五段切分实现四级翻译(一次具体地址走读验证了每级索引与 PFN 拼接),LA57 启用时顶级索引上移;PTE 的 64 位字里,硬件消费 P/RW/USER/A/D/PS/G/NX 八位,其余留给软件——A/D 由 CPU 自动置位支撑回收年龄跟踪与零成本脏位跟踪,RW/USER/NX 支撑 COW、特权隔离与 W^X。CR3 写入完成地址空间切换并依 G 位与 PCID 决定 TLB 存活范围;页表内存账说明大页在元数据上的 500 倍收益。下一节看 ARM64 如何用对偶 TTBR 与 Block Mapping 达成同样的翻译目标。

19.3 ARM64 页表与 Block Mapping

ARM64 的翻译硬件与 x86 有三点根本差异:用户与内核各用一根独立的基址寄存器(TTBR0/TTBR1)而非共享 PGD;页表层级数量是编译期可配的(2-5 级,随 VA_BITS 变化);表项格式由两个类型位决定(块映射/表映射),并额外提供 contiguous hint 让连续表项共享一个 TLB 项。5.2 节从体系结构层面介绍过 TTBR 分割,本节结合 Linux 7.0.10 源码进入内核实现细节。


19.3.1 层级计算:从 VA_BITS 推出树深

ARM64 不像 x86 那样固定层级,而是由虚拟地址位宽反推:

// arch/arm64/include/asm/pgtable-hwdef.h:20-31(公式注释与定义)
 * levels = DIV_ROUND_UP((va_bits - PAGE_SHIFT), PTDESC_TABLE_SHIFT)
#define ARM64_HW_PGTABLE_LEVELS(va_bits) \
    DIV_ROUND_UP(((va_bits) - PAGE_SHIFT), PTDESC_TABLE_SHIFT)

PTDESC_TABLE_SHIFT = 9(4KB 页表 512 项)。于是 39 位 VA → 3 级、48 位 → 4 级、52 位(LPA2)→ 5 级:

// arch/arm64/include/asm/pgtable-hwdef.h:49-96(节选)
#define PTRS_PER_PTE        (1 << PTDESC_TABLE_SHIFT)   /* :49 = 512 */
#define PMD_SHIFT       ARM64_HW_PGTABLE_LEVEL_SHIFT(2) /* :55 */
#define PUD_SHIFT       ARM64_HW_PGTABLE_LEVEL_SHIFT(1) /* :65 */
#define P4D_SHIFT       ARM64_HW_PGTABLE_LEVEL_SHIFT(0) /* :72 */
#define PGDIR_SHIFT     ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS) /* :82 */
#define PTRS_PER_PGD        (1 << (VA_BITS - PGDIR_SHIFT))  /* :85 */

PTRS_PER_PGD 不再固定 512:39 位 VA、3 级布局时 PGD 只需 8 项(1 << (39-36)),页表本身不满一页——这正是 5.2 节"39 位模式省内存"的实现层来源。VA_BITS(arch/arm64/include/asm/memory.h:43,取 CONFIG_ARM64_VA_BITS)与 PAGE_OFFSET(memory.h:45,_PAGE_OFFSET(VA_BITS))决定了用户/内核半区的切分点。

ARM64 三档布局对照 (4KB 页):

 VA_BITS=39 (3 级):          VA_BITS=48 (4 级):          VA_BITS=52 (5 级, LPA2):
 [38:30] PGD (8 项!)         [47:39] PGD (512 项)        [51:42] PGD
 [29:21] PUD (512)           [38:30] PUD                 [41:30] PUD
 [20:12] PTE                 [29:21] PMD                 [29:21] PMD
 [11:0]  off                 [20:12] PTE                 [20:12] PTE
                             [11:0]  off                 [11:0]  off
 PGDIR_SHIFT=36              PGDIR_SHIFT=39              PGDIR_SHIFT=42
 每进程顶级表仅 1 项可用空间  512 项满表                   内核半区更大

 内核选择: VA_BITS=48 + 52 位能力共存时用
 "双 T0SZ" 技巧 (5.2 节): 用户 52 位, 内核 48 位
 (内核半区不用那么大 — 省一级遍历)

19.3.2 TTBR0/TTBR1:对偶基址寄存器

ARM64 双寄存器翻译 (48 位 VA):

        用户地址 < 2^48            内核地址 >= 2^64-2^48
            │                            │
      TTBR0_EL1 ── 用户页表树         TTBR1_EL1 ── 内核页表树
      (每进程切换)                    (init_mm.pgd, 常驻)

  ASID 放在 TTBR0_EL1 低 16 位 → 上下文感知 TLB
  每进程切换: 写 TTBR0 = pgd | (ASID << 48), 随后 TLBI 必要时

对比 x86"一张 PGD 上下两半"的做法,ARM64 用两根寄存器 + 硬件按地址位选择实现同样的分割。后果有二:其一,内核页表(TTBR1 侧)与用户页表(TTBR0 侧)物理上独立成树,内核映射变更无需触碰进程页表;其二,进程切换时 TTBR0 换指针即完成(cpu_switch_mm() → __cpu_set_tcr_t0sz() 调整 T0SZ 以适配 5.2 节的 52/48 位切换,arch/arm64/include/asm/mmu_context.h:67-98),reserved_pg_dir(mmu_context.h:41)用于"暂不希望任何用户翻译生效"的窗口(如上下文切换间隙、KPTI 收窄时)。

TTBR0_EL1 寄存器布局:

 63      48 47                     0
┌──────────┬──────────────────────────┐
│ ASID(16) │  PGD 物理基址 (按 TCR 对齐)│
└──────────┴──────────────────────────┘
 TCR_EL1 还同时配置: T0SZ/T1SZ (两个半区的位宽)
 /TG0/TG1 (页粒度 4K/16K/64K) / 各级 cache 属性
 — x86 用 MSR 族散装配置的东西, ARM64 收敛在
 TTBR0/1 + TCR 三根寄存器里

 ASID 的 generation 管理 (19.5.3 节对照):
 65536 个 ASID 用尽 → 全局换代 → 所有 mm 标
 "需重建" → 下次调度时懒分配新 ASID
 (mm->context.id 存 ASID+代际)

ASID 是 ARM64 版的 PCID:cpu_set_asid() 写入 TTBR0 低 16 位,TLB 项携带 ASID 标签,进程间互不干扰——避免了 x86 无 PCID 时代"切进程必刷全 TLB"的灾难(19.5 节对照)。

19.3.3 描述符格式:两个类型位决定一切

// arch/arm64/include/asm/pgtable-hwdef.h:127-138
#define PUD_TYPE_SECT       (_AT(pudval_t, 1) << 0)     /* 1GB 块 */
#define PMD_TYPE_TABLE      (_AT(pmdval_t, 3) << 0)     /* 指向下一级 */
#define PMD_TYPE_SECT       (_AT(pmdval_t, 1) << 0)     /* 2MB 块 */

PMD/PUD 级表项的 bit[1:0] 是类型标签:0b01 = Block(块映射叶项)、0b11 = Table(指向下一级)、0b0x = 无效。PTE 级另有细节更丰富的位布局:

// arch/arm64/include/asm/pgtable-hwdef.h:164-196(节选)
#define PTE_VALID       (_AT(pteval_t, 1) << 0)     /* 有效位 */
#define PTE_TYPE_MASK       (_AT(pteval_t, 3) << 0)
#define PTE_TYPE_PAGE       (_AT(pteval_t, 3) << 0)     /* 4KB 叶项 */
#define PTE_RDONLY      (_AT(pteval_t, 1) << 7)     /* AP[2]: 只读 */
#define PTE_SHARED      (_AT(pteval_t, 3) << 8)     /* SH[1:0] 内部共享 */
#define PTE_AF          (_AT(pteval_t, 1) << 10)    /* Access Flag */
#define PTE_DBM         (_AT(pteval_t, 1) << 51)    /* Dirty Bit Modifier */
#define PTE_PXN         (_AT(pteval_t, 1) << 53)    /* Privileged XN */
#define PTE_UXN         (_AT(pteval_t, 1) << 54)    /* User XN */
#define PTE_ATTRINDX(t)     (_AT(pteval_t, (t)) << 2)   /* MAIR 索引 */
ARM64 PTE (4KB 页) 位图:

 63 62 ... 54 53 52 51 50...        11 10  9 8-7 6-2  1-0
┌──┬────┬───┬───┬───┬───────────┬───┬──┬─┬───┬────┬────┐
│NS│... │UXN│PXN│   │ DBM │ PFN [47:12] │nG│AF│SH│ATTR│TYPE│V│
└──┴────┴───┴───┴───┴─────┴───────────┴───┴──┴─┴───┴────┴───┘
 bit0   V: Valid (对应 x86 PRESENT)
 bit6   AF: Access Flag —— 无它则访问触发 Access Flag fault
 bit51  DBM: Dirty Bit Modifier —— AF+DBM 时硬件写置脏
 bit53  PXN: 内核态禁执行   bit54: UXN 用户态禁执行 (W^X 基础)
 bit2-4 ATTR: MAIR 寄存器索引 (内存属性: Normal/Device/...)
 bit7   RDONLY (AP[2]) ; bit9 nG: 非 global (对应 x86 G 取反!)

与 x86 最大的语义错位有三处,读 ARM64 页表代码时必须警觉:

  1. AF 位是"访问许可"而非"已访问统计":AF=0 时任何访问触发 Access Flag fault,由软件(do_page_fault → set_pte 补 AF)处理——老内核用它实现"首次访问陷阱",如今主要用于大页/contiguous 的惰性权限;
  2. 脏位跟踪用 AF+DBM 组合模拟:硬件在 AF=1 且 DBM=1 的页上写操作自动置脏等效状态,软件用 PTE_RDONLY+DBM 组合实现"硬件管理脏位"(pte_dirty 判别依赖 DBM 位,__clean_pte 族配合);
  3. nG 语义与 x86 的 G 相反:ARM64 bit9=1 表示非全局(切换 ASID 即失效),x86 bit8=1 表示全局——两边内核常量 PTE_NG/_PAGE_GLOBAL 的组装方向相反。

19.3.4 Block Mapping 与 contiguous hint

Block Mapping 让 2MB(PMD_TYPE_SECT)与 1GB(PUD_TYPE_SECT)区域跳过下级表,语义与 x86 PS 位等价。ARM64 额外的 contiguous hint 是 TLB 效率的独门武器:

// arch/arm64/include/asm/pgtable-hwdef.h:90-96
#define CONT_PTE_SHIFT      (CONFIG_ARM64_CONT_PTE_SHIFT + PAGE_SHIFT)
#define CONT_PTES       (1 << (CONT_PTE_SHIFT - PAGE_SHIFT))
#define CONT_PMD_SHIFT      (CONFIG_ARM64_CONT_PMD_SHIFT + PMD_SHIFT)
#define CONT_PMDS       (1 << (CONT_PMD_SHIFT - PMD_SHIFT))

默认配置下 CONT_PTES = 16:PTE 表中 16 个连续表项(对齐 64KB)标注 cont 位后,硬件允许用单个 TLB 项覆盖这 64KB——TLB miss 成本摊薄 16 倍。THP/大块连续映射路径(23 章)会尽力按 cont 对齐设置表项;contpte_try_fold/unfold(arch/arm64/mm/contpte.c)在运行期合并/拆分。

contiguous hint 合并示意 (16×4KB = 64KB):

 PTE 表 (512 项):
 ┌──┬──┬──┬──┬───────────────┬──────────────────────┐
 │..│P0│P1│..│P15(cont=1 ×16)│ 未对齐区: 逐项映射    │
 └──┴──┴──┴──┴───────────────┴──────────────────────┘
      └────── 1 个 TLB 项覆盖 ──────┘
 约束: 16 项必须同权限/同属性/物理连续/64KB 对齐 —— 违反则拆散

 与 2MB 大页的对比:
   2MB 块映射: 1 项/2MB, 需物理连续+对齐 (碎片敏感)
   cont 4KB:   1 项/64KB, 无物理连续要求 (碎片免疫)
   → 细粒度页保留灵活性的同时拿 16 倍 TLB 收益

19.3.5 与 x86 的实现层对照

维度 x86_64 ARM64
基址寄存器 CR3 一根(上下半共享 PGD) TTBR0/TTBR1 两根(独立树)
层级 4/5 级,运行期探 LA57 2-5 级,编译期按 VA_BITS
块映射 PS 位(PUD/PMD) TYPE 位(PUD/PMD TYPE_SECT)
TLB 标签 PCID(CR3[11:0],软件切换) ASID(TTBR0[63:48],硬件比对)
访问/脏位 A/D 硬件置位 AF 许可 + DBM 组合模拟
全局页 G=1 跨 CR3 幸存 nG=0 项跨 ASID 幸存(内核侧)
特殊能力 LA57 运行期切换 cont hint / LPA2 / MTE 标签位

5.8 节的架构对比表在这里获得了页表层的具体支撑:两套硬件哲学(单根切换 vs 对偶基址)最终收敛到同一套 pgd/pud/pmd/pte 抽象,19.1 节的宏折叠功不可没。


小结

ARM64 用 VA_BITS 反推层级数(3-5 级),TTBR0/TTBR1 对偶寄存器分割用户/内核两棵独立树并以 ASID 标记上下文(TTBR0 内嵌 ASID + TCR 双 T0SZ);表项由两个类型位区分块映射与表链接,PTE 的 AF/DBM 组合模拟 x86 的 A/D 硬件位,nG/PXN/UXN 承担全局页与 W^X 职责。contiguous hint 以 16 项一组共享 TLB 项,是 ARM64 在大内存场景的独有优化。下一节看 RISC-V 如何以极简位布局实现同一目标,并在引导期冻结层级选择。

19.4 RISC-V Sv39/Sv48/Sv57 页表

RISC-V 的翻译规格不是一个,而是一族:Sv39(39 位)、Sv48、Sv57 由不同位宽的 MMU 模式实现。内核面临的独特问题是同一内核映像要兼容三种硬件——层级数不能像 ARM64 那样编译期定死,也不能像 x86 那样每级独立探测,而必须引导期一次性探测并永久冻结(页表结构中途改型等于重建所有进程的地址空间)。5.2 节与 6.4 节分别从体系结构与引导流程介绍过 SATP 与 set_satp_mode(),本节聚焦页表本体:三档布局、PTE 位布局与 satp 的运行期语义。


19.4.1 三档布局与运行期冻结

// arch/riscv/include/asm/pgtable-64.h:16-20
#define PGDIR_SHIFT_L3  30  /* Sv39: 顶级索引 9 位, 1GB/PUD */
#define PGDIR_SHIFT_L4  39  /* Sv48: 顶级索引 9 位, 512GB */
#define PGDIR_SHIFT_L5  48  /* Sv57: 顶级索引 9 位, 256TB */
#define PGDIR_SHIFT     (pgtable_l5_enabled ? PGDIR_SHIFT_L5 : \
        (pgtable_l4_enabled ? PGDIR_SHIFT_L4 : PGDIR_SHIFT_L3))

三档共享同一个规律:每级 9 位索引、512 项表,档位只差"最顶级占几段":

Sv39 (39 位): [38:30] PGD │ [29:21] PMD │ [20:12] PTE │ [11:0] off   3 级
Sv48 (48 位): [47:39] PGD │ [38:30] PUD │ [29:21] PMD │ [20:12] PTE │ off  4 级
Sv57 (57 位): [56:48] PGD │ [47:39] P4D │ [38:30] PUD │ [29:21] PMD │ PTE │ off  5 级

  PMD_SHIFT = 21 (2MB) 对三档恒定          pgtable-64.h:39
  PTRS_PER_PGD/PTE = 4096/8 = 512          pgtable.h:33/35
  差异只在 PGD 索引位宽 → pgtable_l4/l5_enabled 引导期冻结

冻结动作在 set_satp_mode()(arch/riscv/mm/init.c:858):先用一个临时两级页表(只映射内核映像)在 57 位模式试探启动 MMU,失败(非法指令或功能探测不通过)则退回 48 位、再退 39 位(init.c:820/827 的 satp_mode = SATP_MODE_48/39 回退序列)。一旦 MMU 以某档位成功点亮,pgtable_l4_enabled/pgtable_l5_enabled 就永远成立,所有进程页表按该档位建立。satp_mode 全局变量(init.c:49-53)记录最终档位,__pi_set_satp_mode_from_cmdline/fdt(init.c:813-814)允许设备树/命令行降档。

探测与冻结流程:

  setup_vm() 建临时页表(两级, 覆盖内核+DTB)
       │
       v
  set_satp_mode(dtb_pa)                       arch/riscv/mm/init.c:858
       │  写 satp = (SATP_MODE_57 << 60) | 临时表基址
       │  sfence.vma + 验证 MMU 生效
       ├─ 57 位成功 → satp_mode = SATP_MODE_57
       ├─ 失败     → satp_mode = SATP_MODE_48    init.c:820
       └─ 再失败   → satp_mode = SATP_MODE_39    init.c:827
       v
  pgtable_l4_enabled / pgtable_l5_enabled 冻结
  此后 PGDIR_SHIFT/PTRS_PER_PGD 恒定, 正式页表按此建立

19.4.2 PTE 位布局:RISC-V 的极简主义

// arch/riscv/include/asm/pgtable-bits.h:11-76(节选)
#define _PAGE_PRESENT   (1 << 0)    /* V: Valid —— 与 ARM64 的 V 同名同位 */
#define _PAGE_READ      (1 << 1)    /* R: Readable */
#define _PAGE_WRITE     (1 << 2)    /* W: Writable */
#define _PAGE_EXEC      (1 << 3)    /* X: Executable */
#define _PAGE_USER      (1 << 4)    /* U: User */
#define _PAGE_GLOBAL    (1 << 5)    /* G: Global —— 与 x86 同语义 */
#define _PAGE_ACCESSED  (1 << 6)    /* A: Set by hardware on any access */
#define _PAGE_DIRTY     (1 << 7)    /* D: Set by hardware on any write */
#define _PAGE_SOFT      (3 << 8)    /* RSW: 保留给软件 */
#define _PAGE_SPECIAL   (1 << 8)    /* RSW: 0x1 */
...
#define _PAGE_TABLE     _PAGE_PRESENT   /* :59 中间级表项只需 V */
#define _PAGE_PROT_NONE _PAGE_GLOBAL    /* :65 PTE 无效但作权限-none 用 */
#define _PAGE_LEAF (_PAGE_READ | _PAGE_WRITE | _PAGE_EXEC)  /* :76 */
RISC-V PTE (64 位):

 63                     10 9  8 7 6 5 4 3 2 1 0
┌─────────────────────────┬──┬──┬─┬─┬─┬─┬─┬─┬─┬─┐
│      PFN [55:10]        │RS│RS│D│A│G│U│X│W│R│V│
│   (56 位物理地址支持)      │W2│W1│ │ │ │ │ │ │ │ │
└─────────────────────────┴──┴──┴─┴─┴─┴─┴─┴─┴─┴─┘
 RSW[9:8] 留给软件: SPECIAL/soft-dirty/UFFD-WP 各占其一
 叶项判别: R|W|X 任一为 1 (_PAGE_LEAF), 与 x86 PS 位哲学不同 ——
 中间级表项若意外带 RWX 位会被硬件视为叶项(规范规定组合行为)

与 x86 的对照颇能说明架构设计权衡:

功能 x86 位 RISC-V 位 语义差异
存在 P (bit 0) V (bit 0) RISC-V 的中间级只需 V=1
可读 无(P 即可读) R (bit 1) 显式 RISC-V 有显式不可读页
写 RW (bit 1) W (bit 2) 相同
执行 NX=1 反向 X (bit 3) 正向 RISC-V 默认不可执行(Sv 上无 X 即拒)
用户 USER (bit 2) U (bit 4) 相同
访问 A 硬件置位 A 硬件置位 相同(回收器福音)
脏 D 硬件置位 D 硬件置位 相同
全局 G (bit 8) G (bit 5) 相同语义

_PAGE_PROT_NONE = _PAGE_GLOBAL(:65)是个巧妙的复用:V=0、但保留其他位的表项表示"页存在但暂不可访问"(Sv 提案中的 NAPOT/权限-none 用法),内核用它实现 mprotect(PROT_NONE)——不触发常规缺页路径的"不存在",而进入权限拒绝处理。_PAGE_SWP_EXCLUSIVE = _PAGE_ACCESSED(:68)则在 swap 槽位 PTE 里借 A 位做迁移位标记(24 章 swap 编码展开)。

19.4.3 satp 与进程切换

satp(Supervisor Address Translation and Protection)寄存器同时承担"模式 + ASID + 根指针":

satp 寄存器 (64 位):
┌────────┬──────────────┬────────────────────────────┐
│ MODE[63:60] │ ASID[59:44] │      PPN[43:0] 根表物理页    │
│ 8=Sv57 │ 16 位标签      │    4KB 对齐                 │
│ 9=Sv48 │               │                            │
│10=Sv39 │               │                            │
└────────┴──────────────┴────────────────────────────┘
进程切换: csr_write(satp, mode|asid|pgd>>12) + sfence.vma
(与 x86 写 CR3 / ARM64 写 TTBR0 对应)

MODE 字段是三档共存的硬件基础:同一份内核在探测期可以反复改写 MODE 重启 MMU(19.4.1 节),运行期则只读。ASID 16 位与 ARM64 同位宽,内核以 generation 算法回收 ASID(arch/riscv/mm/context.c,19.5 节对照 PCID/ASID 管理)。

19.4.4 用户地址空间大小与合法位模式

// arch/riscv/include/asm/pgtable.h:1236-1247(注释与定义)
 * -      0x4000000000  ( 256GB) for RV64 using SV39 mmu
 * -    0x800000000000  ( 128TB) for RV64 using SV48 mmu
 * - 0x100000000000000  (  64PB) for RV64 using SV57 mmu
#define TASK_SIZE_64    (PGDIR_SIZE * PTRS_PER_PGD / 2)

Sv 规范要求高位必须等于 bit 63(位模式合法性检查,canonical address):非法位模式(如 Sv39 下 bit 62 ≠ bit 63)触发非法地址异常而非翻译。TASK_SIZE_64 取半空间正是对这条规范的实现——用户半区从 0 增长到 2^(VA_BITS-1),内核半区从 2^63 向下。MMAP_VA_BITS_64(pgtable.h:127-128)进一步限制 mmap 区按 48 位以下布局(除非显式请求),保证旧 39 位程序兼容。


小结

RISC-V 以"每级 9 位、512 项"的统一结构实现 Sv39/Sv48/Sv57 三档翻译,档位由引导期 set_satp_mode() 用临时页表探测后一次性冻结(pgtable_l4/l5_enabled),satp 的 MODE/ASID/PPN 三字段承担模式、标签与根指针。PTE 位布局比 x86 更显式——独立的 R 与 X 位、硬件自动置位的 A/D、RSW 软件区——并以 _PAGE_PROT_NONE、_PAGE_LEAF 等组合语义补齐权限模型。至此三大架构的翻译机制全部展开;它们修改页表之后面临的共同问题是 TLB 缓存一致性,这是下一节的主题。

19.5 TLB 刷新与延迟策略

页表是内存里的数据结构,CPU 却只为它维护一份每核私有的缓存——TLB。于是每次修改页表都埋下一颗雷:其他(或本)CPU 可能还在用旧翻译访问新页表已改变的地址。TLB 刷新就是排雷动作,而刷新的代价(IPI 打断所有相关 CPU)让它成为内存子系统最贵的同步操作之一。本节结合 Linux 7.0.10 源码,分析三大架构的刷新指令、x86 的 tlb_gen/PCID 延迟刷新协议、ARM64/RISC-V 的 ASID 代际管理,以及 mmu_gather 的批量收集协议。


19.5.1 为什么必须刷新:缓存的私有性与标签

无标签 TLB (无 PCID/ASID)              有标签 TLB (PCID/ASID)

  TLB: {VA → PA}                        TLB: {VA, tag → PA}
  换进程 = 全量失效!                     换进程 = 换 tag, 旧项幸存
  每次切换后: 冷 TLB 遍历                切回旧进程: 项仍在, 零成本
  刷页表 = 无差别 IPI                    刷页表 = 按 tag 定点失效

x86: CR3 低 12 位 PCID (CR4.PCIDE=1 时)   arch/x86 tlbstate 每核跟踪
ARM64: TTBR0 ASID 字段                    generation 回收复用
RISC-V: satp ASID 字段                    同上

标签 TLB 的出现没有消除刷新的必要性——同一进程内修改页表(munmap、mprotect、页迁移)仍需失效旧项——但它把"跨进程"的失效从每切换必刷降为按代际批刷。理解刷新代码的关键常是理解它在为哪一层延迟买单。

19.5.2 x86:tlb_gen 与 flush_tlb_mm_range

x86 用单调递增的 mm->context.tlb_gen 计数器给每次页表修改编号,各 CPU 记住"我见过第几代",刷新即"把计数器推到新值并让落后者失效":

// arch/x86/mm/tlb.c:1449-1489(节选)
void flush_tlb_mm_range(struct mm_struct *mm, unsigned long start,
                unsigned long end, unsigned int stride_shift,
                bool freed_tables)
{
    struct flush_tlb_info *info;
    int cpu = get_cpu();
    u64 new_tlb_gen;

    /* This is also a barrier that synchronizes with switch_mm(). */
    new_tlb_gen = inc_mm_tlb_gen(mm);           /* :1457 代际推进 */

    info = get_flush_tlb_info(mm, start, end, stride_shift, freed_tables,
                  new_tlb_gen);

    if (mm_global_asid(mm)) {               /* :1471 全局 ASID 分支 */
        broadcast_tlb_flush(info);          /* 硬件广播 INVLPGB */
    } else if (cpumask_any_but(mm_cpumask(mm), cpu) < nr_cpu_ids) {
        info->trim_cpumask = should_trim_cpumask(mm);
        flush_tlb_multi(mm_cpumask(mm), info);      /* IPI 广播 */
        consider_global_asid(mm);
    } else if (mm == this_cpu_read(cpu_tlbstate.loaded_mm)) {
        lockdep_assert_irqs_enabled();
        local_irq_disable();
        flush_tlb_func(info);               /* 仅本核: 直接失效 */
        local_irq_enable();
    }
    ...
    mmu_notifier_arch_invalidate_secondary_tlbs(mm, start, end);
}

三条路径的择优体现了延迟思想:

  • 仅本核持有 mm 时,连 IPI 都省了,本地 invlpg 循环完事;
  • IPI 广播 flush_tlb_multi()(tlb.c:1379)给 mm_cpumask 里每核发函数调用,目标核比对 info->new_tlb_gen 与本核记录,落后才刷——若某核已因调度切换走(见到更新的 gen),无事可做;
  • mm_global_asid 是 7.0 新路径:AMD INVLPGB 指令允许不发起 IPI 的硬件广播失效(invlpgb_flush_addr_nosync + __tlbsync,tlb.c:1505-1521 的 invlpgb_kernel_range_flush() 展示了分块上限处理),对宽机器把刷新成本从"逐核中断"降为"一次总线广播"。

范围参数 stride_shift 决定刷新粒度:PMD_SHIFT 表示"大页级刷"(一次 invlpg 覆盖 2MB 对齐区),PAGE_SHIFT 逐页刷,TLB_FLUSH_ALL(tlbflush.h:23,值 -1UL)整表刷。freed_tables=true(页目录被释放)时必须全刷,因为 invlpg 逐项失效无法覆盖"中间级表项本身失效"的语义。

PCID 与 CR3 切换的配合:load_new_mm_cr3() 写 CR3 时把当前 mm 的 tlb_gen 打包进 PCID/低 12 位标志,使"本核正在运行的 mm"自动被视为最新代——刚切进来的进程无需任何补刷。flush_tlb_one_user()(tlb.c:1649)是单地址失效的最终执行者(要求仅用户地址——内核地址失效走 flush_tlb_one_kernel(),语义严格分开),flush_tlb_local()(tlb.c:1702)刷本核全部非全局项。

19.5.3 ARM64:TLBI 与 RANGE 批量失效

ARM64 的失效指令 TLBI <op> 按目标域组合:VAE1(按 VA,EL1)、VAAE1(按 VA,忽略 ASID 即所有 ASID)、ALLE1(全失效)、VMALLE1(本 VMID 全失效)等;IS 后缀表示 Inner Shareable(广播到共享域):

// arch/arm64/include/asm/tlbflush.h:42
#define __tlbi(op, ...)     __TLBI_N(op, ##__VA_ARGS__, 1, 0)

范围失效走 FEAT_TLB 指令 TLBI RANGE——一条指令携带 num/scale/tlb_level 编码,一次失效最多 2^24 页:

// arch/arm64/include/asm/tlbflush.h:430-465(节选)
#define __flush_tlb_range_op(op, start, pages, stride,          \
                asid, tlb_level, tlbi_user, lpa2)   \
do {                                    \
    ...
    while (__flush_pages > 0) {                 \
        if (!system_supports_tlb_range() ||         \
            __flush_pages == 1 ||               \
            (lpa2 && __flush_start != ALIGN(__flush_start, SZ_64K))) {  \
            addr = __TLBI_VADDR(__flush_start, asid);   \
            __tlbi_level(op, addr, tlb_level);      /* 逐项 */    \
            ...                     \
            continue;                   \
        }                           \
                                    \
        num = __TLBI_RANGE_NUM(__flush_pages, scale);       \
        if (num >= 0) {                     \
            addr = __TLBI_VADDR_RANGE(__flush_start >> shift, asid, \
                        scale, num, tlb_level); /* 范围 */    \
            __tlbi(r##op, addr);                \
            ...                     \
        }                           \
        scale--;    /* 大粒度失败降一级重试 */        \
    }                               \
} while (0)

循环从 scale=3(最大范围)逐级降档,单页兜底回退到普通 TLBI <op>;tlb_level 参数利用页表层级信息只失效对应层级的 TLB 项(如仅 PMD 级块映射项),比全级失效更省。全量刷新用 __tlbi(vmalle1)(tlbflush.h:293,本核)或 vmalle1is(:301,共享域广播)。

ARM64 无 tlb_gen 计数器,延迟刷新由 ASID generation 实现:mm->context.id 存 ASID+代际,asids 用尽(65536 项)时全局换代、所有 mm 标记失效、下次切换时懒重建——代价被摊到"低频的代际翻转"上。

19.5.4 RISC-V:sfence.vma 的单指令语义

// arch/riscv/include/asm/tlbflush.h:18-43(节选)
static inline void local_flush_tlb_all(void)
{
    __asm__ __volatile__ ("sfence.vma" : : : "memory"); /* 全失效(当前 ASID) */
}

static inline void local_flush_tlb_all_asid(unsigned long asid) /* :23 */
{ ... /* sfence.vma zero/ASID */ ... }

static inline void local_flush_tlb_page(unsigned long addr) /* :32 */
static inline void local_flush_tlb_page_asid(unsigned long addr, ...)   /* :37 */

sfence.vma 是 RISC-V 唯一的 TLB 管理指令,rs1(地址)/rs2(ASID)的取舍形成四种组合——rs1=x0 则全地址、rs2=x0 则全 ASID。语义上它同时是内存屏障(保证之前的页表写全局可见后才失效),因此 RISC-V 的页表修改天然比 x86/ARM64 少一道显式屏障。多核广播由 flush_tlb_range() 等函数经 on_each_cpu IPI 完成与 x86 同构;内核同样用 ASID generation 懒失效(tlbflush.h:23-43 的 asid 变体即为此设计)。

19.5.5 mmu_gather:批量收集与延迟释放

页取消映射(munmap/mremap/进程退出)是 TLB 刷新最密集的场景,mmu_gather 把"刷 TLB"与"还页/还表"解耦成先全刷、后释放的两阶段协议:

// include/asm-generic/tlb.h:321-351(节选)
struct mmu_gather {
    struct mm_struct    *mm;
#ifdef CONFIG_MMU_GATHER_TABLE_FREE
    struct mmu_table_batch  *batch;     /* 待释放页目录批次 */
#endif
    unsigned long       start;
    unsigned long       end;
    unsigned int        fullmm : 1; /* 正在整个 mm 上操作 */
    unsigned int        need_flush_all : 1; /* 需要全量刷 */
    unsigned int        freed_tables : 1;/* 已解除页目录 */
    unsigned int        delayed_rmap : 1;/* 挂起的延迟 rmap 移除 */
    ...
};
munmap 的两阶段协议 (tlb.h:49-94 注释归纳):

 tlb_gather_mmu(&tlb, mm, start, end)        收集器就位
   │ unmap 路径逐 VMA:
   │   zap_pte_range(): pte 清零 + __tlb_remove_page() 挂入收集器
   │                    页不立即释放! 引用由 tlb 暂持
   │   clear_pmd/pud 时 tlb_remove_table() 挂入表批次
   v
 tlb_finish_mmu(&tlb, start, end)
   ├─ flush_tlb_range() → 架构刷新 (此时所有旧翻译已失效)
   └─ tlb_remove_table_free() / 页交还伙伴系统   ← 刷新之后才动手

 顺序保证: 若先还页再刷 TLB, 另一 CPU 可能在旧 TLB 命中后
          访问已被伙伴系统重新分配的页 → 数据损坏

为什么不能边解映射边释放:另一 CPU 可能持有旧 TLB 项继续访问该页。两阶段协议以"暂持整批引用"换"只刷一次 TLB",batch 上限(MMU_GATHER_BUNDLE)控制在合理内存占用。fullmm(进程退出)时直接 TLB_FLUSH_ALL 一把刷完。

19.5.6 刷新成本的三层延迟策略总览

层 手段 效果 代价
不刷 PCID/ASID 标签 + 代际 跨进程切换零失效 ASID 空间耗尽时代际翻转
少刷 mmu_gather 批量、tlb_level/stride 粒度 N 次 unmap 合并为 1 次刷新 暂持引用的内存
便宜地刷 INVLPGB 硬件广播(x86)、TLBI RANGE(ARM64)、sfence 语义屏障(RISC-V) 单指令多地址/免 IPI 需硬件新特性,逐级降档

19.5.7 mm_cpumask 的维护与失效边界

x86 协议的隐含前提是 mm_cpumask 准确——"哪些 CPU 可能缓存了本 mm 的翻译"。它由两处维护:调度器在 switch_mm() 时置位本 CPU(cpumask_set_cpu),缺页 IPI 命中后对不再运行本 mm 的 CPU 清位(should_trim_cpumask 的收缩逻辑)。误收窄的风险:某 CPU 实际缓存了翻译却被 mask 掉——防护同样是"代际比对":目标核发现 info->new_tlb_gen 大于自己见过的代才行动,错过一次刷新的 CPU 会在下次 switch_mm 的 lazy 检查中补课。这三个机制(mask 收缩、代际比对、lazy 补课)层层互备,是"乐观广播 + 确定性兜底"的又一实例。

一条指令的完整旅程 (munmap 触发的单页刷新):

 CPU 3 munmap(addr)
  ├─ pte 清除 + inc_mm_tlb_gen → gen=105
  ├─ flush_tlb_multi(mm_cpumask, info{gen=105})
  │    ├─ CPU 0: 正运行本 mm, gen 落后 → invlpg addr ✓
  │    ├─ CPU 1: 已切走 (lazy, gen=104<105) → 补课+无事 ✓
  │    └─ CPU 2: 从未运行 → mask 外, 不打扰 ✓
  └─ 完成 (无锁, IPI 即同步屏障)

小结

TLB 刷新是页表修改的必然后手,三架构给出了同一问题的三种答案:x86 以 tlb_gen 代际 + mm_cpumask 落后者检测把 IPI 压到最少,7.0 又加入 INVLPGB 硬件广播路径;ARM64 以 TLBI RANGE 单指令批量失效并按页表层级定点清理;RISC-V 用 sfence.vma 四种参数组合承担全部职责。PCID/ASID 标签层把跨进程失效延迟到代际翻转,mmu_gather 两阶段协议把密集取消映射合并为单次刷新——所有策略共同服从"先失效后释放"的铁律。至此页表机制(建表、翻译、缓存一致性)完整闭环,下一章进入内核自己的虚拟地址空间布局。