跳转至

第1篇:CPU 页表深度解析 — 一个虚拟地址的物理之旅

源码:arch/arm64/mm/fault.c mm/memory.c | 对应头文件:arch/arm64/include/asm/pgtable-hwdef.h include/linux/mm_types.h

系列目录:SoC 统一内存架构深度解析


为什么要讲 CPU 页表?

因为 HMM 遍历的就是 CPU 页表,SMMU 把 CPU 页表翻译给 GPU 用。不懂页表就看不懂统一内存。

ARM64 四级页表拓扑

ARM64 在 48-bit VA 配置(CONFIG_ARM64_VA_BITS=48)下使用 4 级页表:

Virtual Address (48-bit)
│ [47:39] [38:30] [29:21] [20:12] [11:0]
│  L0       L1      L2      L3     offset
TTBR0_EL1 → PGD → PUD → PMD → PTE → Physical Page

页表级数由 ARM64_HW_PGTABLE_LEVELS 宏动态计算,位于 arch/arm64/include/asm/pgtable-hwdef.h:31-32

#define ARM64_HW_PGTABLE_LEVELS(va_bits) \
    (((va_bits) - PTDESC_ORDER - 1) / PTDESC_TABLE_SHIFT)

其中 PTDESC_TABLE_SHIFTPAGE_SHIFT - PTDESC_ORDERpage.h:13pgtable-hwdef.h:13),对于 4K 页表为 12 - 3 = 9。代入 48-bit VA:

  • levels = (48 - 3 - 1) / 9 = 44 / 9 ≈ 4.88levels = 4(天花板除法)

对于 39-bit VA 则是 3 级页表,所以 ARM64 的页表级数是运行时确定的。

各级页表 Shift 定义

各级页表映射的地址宽度定义在 arch/arm64/include/asm/pgtable-hwdef.h

级别 行号 条件 含义
PGD PGDIR_SHIFT 82 4 - CONFIG_PGTABLE_LEVELS 顶级页表入口覆盖的VA位数
PUD PUD_SHIFT 65 CONFIG_PGTABLE_LEVELS > 3 Level-1 入口覆盖的VA位数
PMD PMD_SHIFT 55 CONFIG_PGTABLE_LEVELS > 2 Level-2 入口覆盖的VA位数
PAGE PAGE_SHIFT 13 (include/vdso/page.h) 通用 页内偏移位数
// pgtable-hwdef.h:82
#define PGDIR_SHIFT     ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS)

其中 ARM64_HW_PGTABLE_LEVEL_SHIFT(n) = PTDESC_TABLE_SHIFT * (4 - n) + PTDESC_ORDERpgtable-hwdef.h:47)。

对于 48-bit / 4K 页表: - PGDIR_SHIFT = 9 * (4 - 2) + 3 = 9 * 2 + 3 = 21 — PGD 每个 entry 映射 2MB - PUD_SHIFT = 9 * (4 - 1) + 3 = 30 — PUD 每个 entry 映射 1GB - PMD_SHIFT = 9 * (4 - 0) + 3 = 39 — PMD 每个 entry 映射 512GB

Note: 虽然 PMD_SHIFT=39 理论上可以映射 512GB,但在常规 Walk 中,PMD 入口指向的是下一级 PTE 表,而非直接映射。Block mapping(PMD_TYPE_SECT)选项可以将 PMD 入口配置为直接映射 2MB 大页。

PTE 硬件位定义

每个 PTE 是一个 64-bit 的硬件描述符,位定义在 arch/arm64/include/asm/pgtable-hwdef.h

位域 行号 说明
bit[0] PTE_VALID 164 该 PTE 有效,MMU 可以将其用于翻译
bits[1:0]=0b11 PTE_TYPE_PAGE 166 这是一个 Page 类型的描述符(leaf entry)
bit[6] PTE_USER 167 AP[1],EL0 可以访问
bit[7] PTE_RDONLY 168 AP[2],只读
bits[9:8] PTE_SHARED 169 SH[1:0],Inner Shareable
bit[10] PTE_AF 170 Access Flag:MMU 硬件自动设置,OS 定期清除以跟踪页面访问
bit[11] PTE_NG 171 nG(non-Global),用于 ASID 区分
bit[50] PTE_GP 172 BTI Guarded Page
bit[51] PTE_DBM 173 Dirty Bit Management:硬件脏页跟踪(FEAT_HA/HD)
bit[52] PTE_CONT 174 Contiguous range,连续页合并 TLB 入口
bit[53] PTE_PXN 175 Privileged Execute Never
bit[54] PTE_UXN 176 User Execute Never

关键硬件机制详解

Access Flag (PTE_AF):当 MMU 遍历页表进行地址翻译时,会自动将对应表项的 AF 位设置为 1。OS 定期扫描并清除 AF 位,下一次访问又会触发 AF 硬件置位。OS 通过检查 AF 状态判断页面是否被最近访问过——这是 LRU 页面回收的基础。

Dirty Bit Management (PTE_DBM):传统的 ARM 架构没有硬件脏位,OS 需要用 PTE_RDONLY 模拟写保护来跟踪脏页(写时触发行保护 fault)。FEAT_HA + FEAT_HD 硬件扩展引入 DBM,让 MMU 在首次写入时自动设置 DBM 位(硬件等价于 PTE_WRITE),消除了额外的页错误中断。对应的 fault_info 表中有 access flag fault(fault.c:917-920)和 permission fault(fault.c:921-924)不同路径。

Block Mapping (PMD_TYPE_SECT):PMD 级别的 section/block 映射(pgtable-hwdef.h:138),可以直接将 2MB 的 PMD entry 指向物理内存,跳过 PTE 级。HMM 遍历时通过 pmd_trans_huge() 检测这一情况。

软件 PTE 位定义

软件层使用硬件未定义的位来存储额外信息,定义在 arch/arm64/include/asm/pgtable-prot.h

硬件位 行号 用途
PTE_WRITE =PTE_DBM (bit 51) 16 软件可写标志,直接复用硬件 DBM 位
PTE_DIRTY bit 55 18 软件脏位(当硬件 HD 不可用时)
PTE_SPECIAL bit 56 19 Special 映射(vDSO、gate area 等)
PTE_PRESENT_INVALID =PTE_NG (bit 11) 26 PTE 有效但不可被硬件访问,软件解释内容

其中 PTE_PRESENT_INVALID 是一个巧妙的设计:硬件侧 PTE_VALID=0(bit[0] 清零),MMU 遇到此 PTE 会触发 fault;但软件侧 pte_present() 返回 true,OS 可以直接读取 PTE 内容。这使得可以通过设置 PTE_NG 来标记一个 "对软件 present 但对硬件 invalid" 的状态。

// PTE_WRITE 复用硬件 DBM 位,意味着写权限和脏位是同一个物理位
#define PTE_WRITE   (PTE_DBM)         /* same as DBM (51) */

// PTE_DIRTY 使用 sw bit 55,当硬件不支持 HD 时用于软件脏页跟踪
#define PTE_DIRTY   (_AT(pteval_t, 1) << 55)

// PTE_PRESENT_INVALID = PTE_NG 且 V=0,硬件无法访问但软件读得到
#define PTE_PRESENT_INVALID (PTE_NG)  /* only when !PTE_VALID */

关键数据结构

PGD 指针 — mm_struct.pgd

// include/linux/mm_types.h:1199
pgd_t * pgd;

每个 mm_structpgd 字段指向进程页表的 PGD(Page Global Directory)基地址。在上下文切换时写入 TTBR0_EL1(用户态)或 TTBR1_EL1(内核态)。

VMA — vm_area_struct

// include/linux/mm_types.h:932
struct vm_area_struct {
    unsigned long vm_start;     // line 938
    unsigned long vm_end;       // line 939
    struct mm_struct *vm_mm;    // line 948
    pgprot_t vm_page_prot;      // line 949 — 访问权限
    vm_flags_t vm_flags;        // line 958 — VM_READ/WRITE/EXEC/SHARED 等
};

VMA 描述了进程虚拟地址空间的一个连续区间及其属性。HMM 的 test_walk 回调(hmm.c:597)会检查 vm_flags 中是否设置了 VM_IOVM_PFNMAP 来跳过无法处理的 VMA。

缺页中断调用链

fault_info 调度表

ARM64 通过 ESR_ELx 寄存器中的 Fault Status Code (FSC) 字段分发到对应的处理函数。调度表定义在 arch/arm64/mm/fault.c:908-972

// fault.c:908
static const struct fault_info fault_info[] = {
    { do_bad,               SIGKILL, SI_KERNEL, "ttbr address size fault"   },  // 0
    ...
    { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 0 translation fault" }, // 4
    { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" }, // 5
    { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" }, // 6
    { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 3 translation fault" }, // 7
    { do_page_fault,        SIGSEGV, SEGV_ACCERR, "level 0 access flag fault"  }, // 8
    { do_page_fault,        SIGSEGV, SEGV_ACCERR, "level 1 access flag fault"  }, // 9
    { do_page_fault,        SIGSEGV, SEGV_ACCERR, "level 2 access flag fault"  }, // 10
    { do_page_fault,        SIGSEGV, SEGV_ACCERR, "level 3 access flag fault"  }, // 11
    { do_page_fault,        SIGSEGV, SEGV_ACCERR, "level 0 permission fault"   }, // 12
    { do_page_fault,        SIGSEGV, SEGV_ACCERR, "level 1 permission fault"   }, // 13
    { do_page_fault,        SIGSEGV, SEGV_ACCERR, "level 2 permission fault"   }, // 14
    { do_page_fault,        SIGSEGV, SEGV_ACCERR, "level 3 permission fault"   }, // 15
    ...
};

关键路径的 FSC 编码与处理: - FSC 0x04-0x07 → translation fault → do_translation_faultdo_page_fault(无 PTE 存在) - FSC 0x08-0x0B → access flag fault → do_page_fault(PTE 存在但 AF=0,MMU 硬件未置位因为 OS 清过) - FSC 0x0C-0x0F → permission fault → do_page_fault(权限不匹配:写只读页、执行 NX 页等)

do_translation_fault vs do_page_fault — 本质区别

这两个 handler 是 ARM64 缺页处理中最容易混淆的概念:

// fault.c:833-844 — do_translation_fault 是一个轻量包装
static int __kprobes do_translation_fault(unsigned long far,
                                          unsigned long esr,
                                          struct pt_regs *regs)
{
    unsigned long addr = untagged_addr(far);

    if (is_ttbr0_addr(addr))           // ← 用户态地址?
        return do_page_fault(far, esr, regs);  // 转给 do_page_fault

    do_bad_area(far, esr, regs);       // 内核地址+translation fault→kill
    return 0;
}

do_translation_fault 只是一个门卫,它不是在处理 translation fault,而是在判断 谁发的: - 用户态地址(TTBR0,is_ttbr0_addr)→ 转给 do_page_fault,正常缺页 - 内核态地址(TTBR1)→ 直接 do_bad_area,kill 进程

do_page_faultfault.c:596)才是真正干活的那个——分配 vm_flags、查 VMA、调用 handle_mm_fault

// fault.c:654-662 — kernel uaccess 检查
if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
    if (is_el1_instruction_abort(esr))
        die_kernel_fault("execution of user memory", addr, esr, regs);
    if (!insn_may_access_user(regs->pc, esr))
        die_kernel_fault("access to user memory outside uaccess routines",
                         addr, esr, regs);
}

为什么需要这种区分?ARM64 的 TTBR0_EL1 指向用户态页表,TTBR1_EL1 指向内核态页表。如果内核代码访问用户态地址时没走 copy_from_user 等安全路径,就需要 die_kernel_fault 而不是静默处理。

signal code 的区别也值得注意(fault.c:908-924): - translation fault → SEGV_MAPERR页根本不存在fault.c:913-916) - access flag / permission fault → SEGV_ACCERR页存在但不允许访问fault.c:917-924

这两个信号码和 VMA 的存在性检测形成闭环(fault.c:687-735):

// fault.c:687 — 查 VMA 权限
if (!(vma->vm_flags & vm_flags)) {
    si_code = SEGV_ACCERR;      // VMA 存在但权限不匹配
    ...
}
// fault.c:728 — VMA 不存在
si_code = SEGV_MAPERR;          // 根本没有 VMA

调用链详解

异常入口(硬件)
do_mem_abort(far, esr, regs)                      // fault.c:975
  │ esr_to_fault_info(esr) → fault_info[index]     // fault.c:58-60
  │ inf->fn(far, esr, regs)                        // 分发到具体 handler
  ├── do_translation_fault (FSC=4-7)               // fault.c:833
  │     └── do_page_fault                          // fault.c:596
  └── do_page_fault (FSC=8-15)                     // fault.c:596
        │ 1. 根据 ESR 解析 fault 类型:
        │    - is_el0_instruction_abort → vm_flags=VM_EXEC
        │    - is_write_abort           → vm_flags=VM_WRITE
        │    - else                     → vm_flags=VM_READ
        │ 2. user_mode? → FAULT_FLAG_USER
        │ 3. lock_vma_under_rcu() or lock_mm_and_find_vma()
        ├── handle_mm_fault(vma, addr, flags, regs)   // memory.c:6699
        │     │
        │     ├── sanitize_fault_flags()
        │     ├── arch_vma_access_permitted() 检查
        │     ├── hugetlb? → hugetlb_fault()
        │     └── else → __handle_mm_fault(vma, addr, flags)  // memory.c:6465
        │           │
        │           ├── pgd_offset(mm, addr)  — 从 mm->pgd 计算 PGD 入口
        │           ├── p4d_alloc(mm, pgd, addr)  — 分配 P4D(如果折叠则为 no-op)
        │           ├── pud_alloc(mm, p4d, addr)  — 分配 PUD
        │           ├── 检查 PUD 大页 (create_huge_pud/pud_trans_huge)
        │           ├── pmd_alloc(mm, pud, addr)  — 分配 PMD
        │           ├── 检查 PMD 大页 (create_huge_pmd/pmd_trans_huge)
        │           └── handle_pte_fault(vmf)          // memory.c:6383
        │                 │
        │                 ├── pte_none? → do_pte_missing(vmf)     // 缺页分配
        │                 ├── !pte_present? → do_swap_page(vmf)   // Swap in
        │                 ├── pte_protnone? → do_numa_page(vmf)   // NUMA 迁移
        │                 └── FAULT_FLAG_WRITE && !pte_write?
        │                       → do_wp_page(vmf)                  // 写时复制
        └── 返回后: 检查 VM_FAULT_RETRY,处理信号

完整页表遍历示意

                    ┌──────────────────────────────────────────────────────┐
                    │               Virtual Address (48-bit)                │
                    │   [47:39]  [38:30]  [29:21]  [20:12]    [11:0]      │
                    │    L0 idx   L1 idx   L2 idx   L3 idx    offset       │
                    │    9 bits   9 bits   9 bits   9 bits    12 bits      │
                    └──────┬───────┬───────┬───────┬───────────┬───────────┘
                           │       │       │       │           │
    ┌──────────────────────┘       │       │       │           │
    ▼                              │       │       │           │
┌─────────┐  TTBR0_EL1            │       │       │           │
│ mm->pgd │──────────────────────┐ │       │       │           │
│ (line   │    PGD[L0] ──► ┌─────▼─┐     │       │           │
│  1199)  │                │  PUD  │     │       │           │
└─────────┘                │table  │     │       │           │
                           │       │     │       │           │
                           │PUD    │     │       │           │
                           │[L1]───┼──┐  │       │           │
                           └───────┘ │  │       │           │
                                     │  │       │           │
                                     ▼  │       │           │
                                 ┌───────┐     │           │
                                 │  PMD  │     │           │
                                 │ table │     │           │
                                 │       │     │           │
                                 │PMD    │     │           │
                                 │[L2]───┼──┐  │           │
                                 └───────┘ │  │           │
                                           │  │           │
                                           ▼  │           │
                                       ┌───────┐         │
                                       │  PTE  │         │
                                       │ table │         │
                                       │       │         │
                                       │PTE    │         │
                                       │[L3]───┼──┐      │
                                       └───────┘ │      │
                                                 │      │
                                                 ▼      ▼
                                            ┌──────────────┐
                                            │  Physical    │
                                            │  Page Frame  │
                                            │  (4KB)       │
                                            └──────────────┘

每条 PGD/PUD/PMD/PTE entry 占用 64 bits。低 PTDESC_TABLE_SHIFT(9 或与页大小相关)位作为 flags,高位存放下一级页表的物理地址。

块映射快捷路径:当 PMD 入口配置为 PMD_TYPE_SECT 时,跳过 PTE 级,直接映射 2MB 大页。

总结

CPU 页表的核心是一个树形数据结构,根在 mm->pgdmm_types.h:1199),通过 ARM64_HW_PGTABLE_LEVELSpgtable-hwdef.h:31-32)确定级数。页错误通过 fault_info[] 表(fault.c:908)分派,最后到达 handle_pte_faultmemory.c:6383)进行 4 路分支处理。

理解这套机制后,HMM 就是在此之上建立一个只读的页表遍历器——不修改 PTE,只读取 PFN 并把结果填回 hmm_range.pfns[] 数组。

下一篇文章

第2篇:HMM 如何遍历 CPU 页表 — hmm_range_fault 内部全解


💬 评论