前言
在 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) | 操作系统自定义使用 |
| 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 */
struct mm_struct {
pgd_t *pgd; /* 指向进程的页全局目录(页表根) */
/* ... 更多字段 ... */
};
/* 页表项类型定义 - 定义在 arch/x86/include/asm/pgtable_types.h */
//所有的页表项都是8字节的一个定义
typedef struct { unsigned long pgd; } pgd_t; /* 页全局目录项 */
typedef struct { unsigned long pud; } pud_t; /* 页上级目录项 */
typedef struct { unsigned long pmd; } pmd_t; /* 页中间目录项 */
typedef struct { unsigned long pte; } pte_t; /* 页表项 */
6.2.2. 关键管理函数
页表分配函数:
/* 分配各级页表 */
pgd_t *pgd_alloc(struct mm_struct *mm); /* 分配页全局目录 */
int __pud_alloc(struct mm_struct *mm,
pgd_t *pgd, unsigned long address);
int __pmd_alloc(struct mm_struct *mm,
pud_t *pud, unsigned long address);
int __pte_alloc(struct mm_struct *mm, pmd_t *pmd);
/* 页面分配 */
struct page *alloc_pages(gfp_t gfp_mask,
unsigned int order);
void __free_pages(struct page *page, unsigned int order);
页表遍历与查找函数:
/* 根据虚拟地址查找各级页表项 */
pgd_t *pgd_offset(struct mm_struct *mm, unsigned long address);
pud_t *pud_offset(pgd_t *pgd, unsigned long address);
pmd_t *pmd_offset(pud_t *pud, unsigned long address);
pte_t *pte_offset_map(pmd_t *pmd, unsigned long address);
/* 页表项状态检查 */
int pte_present(pte_t pte); /* 检查页是否在内存中 */
int pte_young(pte_t pte); /* 检查访问位 */
int pte_dirty(pte_t pte); /* 检查脏位 */
int pte_write(pte_t pte); /* 检查写权限 */
页表项修改函数:
/* 设置页表项 */
void set_pte(pte_t *ptep, pte_t pte);
void set_pmd(pmd_t *pmdp, pmd_t pmd);
void set_pud(pud_t *pudp, pud_t pud);
void set_pgd(pgd_t *pgdp, pgd_t pgd);
/* 页表项属性修改 */
pte_t pte_mkwrite(pte_t pte); /* 设置可写 */
pte_t pte_wrprotect(pte_t pte); /* 设置写保护 */
pte_t pte_mkdirty(pte_t pte); /* 设置脏位 */
pte_t pte_mkyoung(pte_t pte); /* 设置访问位 */
页表释放函数:
/* 释放各级页表 */
void pgd_free(struct mm_struct *mm, pgd_t *pgd);
void pud_free(struct mm_struct *mm, pud_t *pud);
void pmd_free(struct mm_struct *mm, pmd_t *pmd);
void pte_free(struct mm_struct *mm, pgtable_t pte);
TLB 管理函数:
/* TLB 刷新操作 */
/* 刷新所有 CPU 的 TLB */
void flush_tlb_all(void);
/* 刷新指定进程的 TLB */
void flush_tlb_mm(struct mm_struct *mm);
void flush_tlb_page(struct vm_area_struct *vma,
unsigned long addr);
void flush_tlb_range(struct vm_area_struct *vma,
unsigned long start, unsigned long end);
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
= 0x140BCD
7.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 缓存加速转换
- 支持大页面优化,实现进程隔离和内存保护
这套机制是现代操作系统内存管理的基石,为应用程序提供了统一、安全、高效的内存访问接口。
1196

被折叠的 条评论
为什么被折叠?



