前言
在 AI 计算和推理加速的内核驱动开发中,高效的内存管理是构建高性能计算系统的核心基础。无论是 GPU 显存映射、DMA 缓冲区管理,还是大规模张量数据的虚拟地址分配,都离不开对页表机制的深入理解。本文系统梳理 x86_64 架构下 Linux 的四级页表机制,为 AI 计算相关的内核驱动开发提供理论基础和实践参考。
1. 概述
在 x86_64 架构的 Linux 系统中,页表采用四级分页模型来实现虚拟地址到物理地址的转换。虽然现代硬件支持五级页表,但 Linux 默认启用四级分页,以多级页表项的形式分层存储在物理内存中。
2. 四级页表结构
Linux 对 x86_64 的虚拟地址(默认 48 位有效地址)进行分段,依次索引四级页表, 页表层级说明如下:
| 页表层级 | 全称 | 索引位宽 | 虚拟地址位段 | 作用 |
|---|---|---|---|---|
| PGD | 页全局目录 (Page Global Directory) | 9 位 | 47~39 | 索引 PGD 项,指向 PUD 页的物理地址 |
| PUD | 页上级目录 (Page Upper Directory) | 9 位 | 38~30 | 索引 PUD 项,指向 PMD 页的物理地址 |
| PMD | 页中间目录 (Page Middle Directory) | 9 位 | 29~21 | 索引 PMD 项,指向 PT 页或 2MB 大页面 |
| PT | 页表 (Page Table) | 9 位 | 20~12 | 索引 PT 项,指向 4KB 物理页面 |
| 页内偏移 | - | 12 位 | 11~0 | 物理页面内的字节偏移 |
2.1. 各级页表详解
PGD(页全局目录)
- 进程的
mm_struct中保存 PGD 物理地址 - 虚拟地址最高 9 位索引 PGD 项
- 指向 PUD 页的物理地址
- 进程的
PUD(页上级目录)
- 中间层级,9 位索引 PUD 项
- 指向 PMD 页物理地址
- 在 4KB 页面且地址范围较小时常与 PGD 合并
PMD(页中间目录)
- 9 位索引 PMD 项
- 可指向 PT 页物理地址
- 或直接映射 2MB 大页面(此时跳过 PT 层级)
PT(页表)
- 最低层级,9 位索引 PT 项
- 直接指向 4KB 物理页面的起始地址
- 虚拟地址最低 12 位为页内偏移量
3. 页表项(PTE)存储格式
每个页表项占8 字节(64 位),包含两部分核心信息:
页表项(64 位 / 8 字节)结构: ┌────────────────────────────────────────────────────────────────┐ │ 63 0 │ ├──────────────────────────────────────────┬─────────────────────┤ │ 物理页面基地址 (52 位) │ 标志位 (12 位) │ │ [63:12] │ [11:0] │ └──────────────────────────────────────────┴─────────────────────┘ 详细位段分布: ┌──┬──┬──┬──┬──────────────────────────────────┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │N │PK│PK│PK│ │ │P │ │ │P │P │U │R │ │ │ │P │ │X │3 │2 │1 │ 物理页面基地址 │G │A │D │A │C │W │/ │/ │ │ │ │ │ │ │ │ │ │ (Physical Page Base Address) │ │T │ │ │D │T │S │W │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├──┼──┼──┼──┼──────────────────────────────────┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ │63│62│61│60│59 12│11│10│ 9│ 8│ 7│ 6│ 5│ 4│ 3│ 2│ 1│ 0│ └──┴──┴──┴──┴──────────────────────────────────┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘3.1. 物理地址部分(位 63-12)
- 高 52 位:目标物理页的基地址
- 物理地址仅低 52 位有效,由 CPU 架构决定
- 由于页面按 4KB 对齐,低 12 位始终为 0,因此不需要存储
3.2. 权限与状态标志位(位 11-0)
| 位 | 标志位 | 名称 | 作用 |
|---|---|---|---|
| 0 | P | 存在位 (Present) | 标记页是否在物理内存中 |
| 1 | R/W | 读写权限 (Read/Write) | 0=只读,1=可读写 |
| 2 | U/S | 用户/内核空间 (User/Supervisor) | 0=仅内核态,1=用户态可访问 |
| 3 | PWT | 写通 (Page Write-Through) | 控制写缓存策略 |
| 4 | PCD | 禁用缓存 (Page Cache Disable) | 1=禁用页面缓存 |
| 5 | A | 访问位 (Accessed) | 标记页是否被访问过 |
| 6 | D | 脏位 (Dirty) | 标记页是否被修改过 |
| 7 | PAT | 页面属性 (Page Attribute Table) | 与 PWT/PCD 配合控制缓存 |
| 8 | G | 全局位 (Global) | 1=全局页,TLB 刷新时保留 |
| 9-11 | AVL | 可用位 (Available) | 操作系统自定义使用 |
| 62 | RSV | 保留位 | 保留给未来扩展 |
| 63 | NX | 禁止执行 (No eXecute) | 1=禁止从该页执行代码 |
4. 页表空间占用
在 x86_64 架构的 Linux 四级分页模型中,每级页表默认占用 4KB 物理内存。
4.1. 空间计算逻辑
- 每个页表项(PTE/PMD/PUD/PGD 项)占8 字节
- 一个 4KB 页面可容纳:4096 ÷ 8 = 512 个页表项
- 虚拟地址中每级页表的索引位宽为9 位
- 2^9 = 512,刚好匹配一个 4KB 页面能存储的页表项数量
因此,PGD、PUD、PMD、PT 各级页表只要独立存在,单级占用空间都是4KB。
4.2. 特殊情况
2MB 大页面
- PMD 项直接映射物理大页
- 跳过 PT 层级,节省 4KB 的 PT 页空间
1GB 超大页面
- PUD 项直接映射物理超大页
- 跳过 PMD 和 PT 层级,节省两级页表空间
地址范围较小时
- PUD 层会与 PGD 层合并
- PUD 无需单独占用 4KB 空间
5. 内核与用户页表隔离
5.1. 用户进程页表
- 每个进程有独立的 PGD
- 用户空间(
0x0000000000000000~0x00007FFFFFFFFFFF)的页表项由进程私有 - 进程切换时只需切换CR3 寄存器指向新进程的 PGD 物理地址
5.2. 内核页表
- 内核空间(
0xFFFF800000000000及以上)的页表项在所有进程的 PGD/PUD/PMD 中共享 - 实现内核地址空间的全局映射
- 避免每次进程切换时重新映射内核空间
6. 硬件关联与管理
6.1. 硬件支持
- CR3 寄存器:存储当前进程 PGD 的物理地址
- CPU 地址转换:自动遍历四级页表完成虚拟地址到物理地址的转换
- TLB(Translation Lookaside Buffer):缓存虚拟地址到物理地址的映射,减少页表遍历开销
6.2. 内核管理
6.2.1. 关键数据结构
/* 进程内存描述符 - 定义在 include/linux/mm_types.h */structmm_struct{pgd_t*pgd;/* 指向进程的页全局目录(页表根) *//* ... 更多字段 ... */};/* 页表项类型定义 - 定义在 arch/x86/include/asm/pgtable_types.h *///所有的页表项都是8字节的一个定义typedefstruct{unsignedlongpgd;}pgd_t;/* 页全局目录项 */typedefstruct{unsignedlongpud;}pud_t;/* 页上级目录项 */typedefstruct{unsignedlongpmd;}pmd_t;/* 页中间目录项 */typedefstruct{unsignedlongpte;}pte_t;/* 页表项 */6.2.2. 关键管理函数
页表分配函数:
/* 分配各级页表 */pgd_t*pgd_alloc(structmm_struct*mm);/* 分配页全局目录 */int__pud_alloc(structmm_struct*mm,pgd_t*pgd,unsignedlongaddress);int__pmd_alloc(structmm_struct*mm,pud_t*pud,unsignedlongaddress);int__pte_alloc(structmm_struct*mm,pmd_t*pmd);/* 页面分配 */structpage*alloc_pages(gfp_tgfp_mask,unsignedintorder);void__free_pages(structpage*page,unsignedintorder);页表遍历与查找函数:
/* 根据虚拟地址查找各级页表项 */pgd_t*pgd_offset(structmm_struct*mm,unsignedlongaddress);pud_t*pud_offset(pgd_t*pgd,unsignedlongaddress);pmd_t*pmd_offset(pud_t*pud,unsignedlongaddress);pte_t*pte_offset_map(pmd_t*pmd,unsignedlongaddress);/* 页表项状态检查 */intpte_present(pte_tpte);/* 检查页是否在内存中 */intpte_young(pte_tpte);/* 检查访问位 */intpte_dirty(pte_tpte);/* 检查脏位 */intpte_write(pte_tpte);/* 检查写权限 */页表项修改函数:
/* 设置页表项 */voidset_pte(pte_t*ptep,pte_tpte);voidset_pmd(pmd_t*pmdp,pmd_tpmd);voidset_pud(pud_t*pudp,pud_tpud);voidset_pgd(pgd_t*pgdp,pgd_tpgd);/* 页表项属性修改 */pte_tpte_mkwrite(pte_tpte);/* 设置可写 */pte_tpte_wrprotect(pte_tpte);/* 设置写保护 */pte_tpte_mkdirty(pte_tpte);/* 设置脏位 */pte_tpte_mkyoung(pte_tpte);/* 设置访问位 */页表释放函数:
/* 释放各级页表 */voidpgd_free(structmm_struct*mm,pgd_t*pgd);voidpud_free(structmm_struct*mm,pud_t*pud);voidpmd_free(structmm_struct*mm,pmd_t*pmd);voidpte_free(structmm_struct*mm,pgtable_tpte);TLB 管理函数:
/* TLB 刷新操作 *//* 刷新所有 CPU 的 TLB */voidflush_tlb_all(void);/* 刷新指定进程的 TLB */voidflush_tlb_mm(structmm_struct*mm);voidflush_tlb_page(structvm_area_struct*vma,unsignedlongaddr);voidflush_tlb_range(structvm_area_struct*vma,unsignedlongstart,unsignedlongend);7. 地址转换实例
7.1. 示例参数设定
假设需要转换虚拟地址:0x0000 1234 5678 ABCD
已知条件:
- 进程 PGD 物理地址:
0x100000 - 页表项标志位:P=1(存在)、RW=1(可读写)
- 高 52 位为物理地址基址
7.2. 步骤 1:拆分虚拟地址位段
将 48 位虚拟地址0x000012345678ABCD按位段拆分:
虚拟地址:0x0000 1234 5678 ABCD ↓ ↓ ↓ ↓ 二进制: 0000 0000 0000 0001 0010 0011 0100 0101 0110 0111 1000 1010 1011 1100 1101 位段拆分: ├─ PGD 索引 [47:39]:0x000 (十进制: 0) ├─ PUD 索引 [38:30]:0x024 (十进制: 36) ├─ PMD 索引 [29:21]:0x08D (十进制: 141) ├─ PT 索引 [20:12]:0x171 (十进制: 369) └─ 页内偏移 [11:0] :0xBCD (十进制: 3021)7.3. 步骤 2:四级页表逐级寻址
第一级:寻址 PGD 项
PGD 物理地址 = 0x100000 PGD 索引 = 0x000 PGD 项物理地址 = 0x100000 + (0x000 × 8) = 0x100000 假设 PGD 项值 = 0x110000_000000067 └─ 高 52 位 0x110000 → PUD 页的物理地址第二级:寻址 PUD 项
PUD 页物理地址 = 0x110000 PUD 索引 = 0x024 PUD 项物理地址 = 0x110000 + (0x024 × 8) = 0x110120 假设 PUD 项值 = 0x120000_000000067 └─ 高 52 位 0x120000 → PMD 页的物理地址第三级:寻址 PMD 项
PMD 页物理地址 = 0x120000 PMD 索引 = 0x08D PMD 项物理地址 = 0x120000 + (0x08D × 8) = 0x120438 假设 PMD 项值 = 0x130000_000000067 └─ 高 52 位 0x130000 → PT 页的物理地址第四级:寻址 PT 项
PT 页物理地址 = 0x130000 PT 索引 = 0x171 PT 项物理地址 = 0x130000 + (0x171 × 8) = 0x130888 假设 PT 项值 = 0x140000_000000067 └─ 高 52 位 0x140000 → 最终物理页面的基地址7.4. 步骤 3:计算最终物理地址
物理地址 = 物理页面基地址 + 页内偏移 = 0x140000 + 0xBCD = 0x140BCD7.5. 转换流程图
虚拟地址: 0x000012345678ABCD ↓ ┌─────────────────────────────────────┐ │ CR3 寄存器: 0x100000 (PGD 基址) │ └─────────────────────────────────────┘ ↓ [PGD 索引: 0x000] ┌─────────────────────────────────────┐ │ PGD[0] = 0x110000_000000067 │ │ → PUD 基址: 0x110000 │ └─────────────────────────────────────┘ ↓ [PUD 索引: 0x024] ┌─────────────────────────────────────┐ │ PUD[36] = 0x120000_000000067 │ │ → PMD 基址: 0x120000 │ └─────────────────────────────────────┘ ↓ [PMD 索引: 0x08D] ┌─────────────────────────────────────┐ │ PMD[141] = 0x130000_000000067 │ │ → PT 基址: 0x130000 │ └─────────────────────────────────────┘ ↓ [PT 索引: 0x171] ┌─────────────────────────────────────┐ │ PT[369] = 0x140000_000000067 │ │ → 物理页基址: 0x140000 │ └─────────────────────────────────────┘ ↓ [页内偏移: 0xBCD] ┌─────────────────────────────────────┐ │ 最终物理地址: 0x140BCD │ └─────────────────────────────────────┘8. 性能优化机制
8.1. TLB 缓存
- TLB 是 CPU 内部的高速缓存,存储最近使用的虚拟地址到物理地址的映射
- 命中 TLB 时无需遍历四级页表,大幅提升地址转换速度
- TLB 失效时才触发页表遍历(Page Table Walk)
8.2. 大页面支持
- 2MB 大页面:减少页表层级,降低 TLB 压力
- 1GB 超大页面:适用于大内存应用,进一步减少页表开销
- 通过
hugetlbfs文件系统或mmap的MAP_HUGETLB标志使用
8.3. 页表缓存
- 内核维护页表缓存池,避免频繁分配和释放页表页
- 使用 slab 分配器管理页表项
9. 总结
Linux 的四级页表机制通过分层索引的方式,实现了灵活、高效的虚拟地址到物理地址的转换。整个过程由 CPU 硬件自动完成,配合 TLB 缓存和大页面支持,在保证内存隔离和保护的同时,提供了出色的性能表现。
关键要点:
- 四级页表结构:PGD → PUD → PMD → PT → 物理页
- 每级页表占用 4KB 物理内存,每个页表项 8 字节
- 48 位虚拟地址分段索引,12 位页内偏移
- CR3 寄存器存储 PGD 基址,TLB 缓存加速转换
- 支持大页面优化,实现进程隔离和内存保护
这套机制是现代操作系统内存管理的基石,为应用程序提供了统一、安全、高效的内存访问接口。