深入理解 Linux 四级页表架构:PGD、P4D、PUD、PMD 到 PTE 的映射全过程

在 Linux 操作系统中,虚拟内存管理是整个系统稳定运行与进程隔离的基石。无论是用户态进程通过 malloc 分配堆内存,还是内核态通过 vmalloc 映射非连续物理页,本质上都是在操作一套由硬件 MMU(Memory Management Unit)和操作系统内核共同维护的**多级页表(Multi-level Page Tables)**结构。
随着 64 位体系结构(如 x86_64 与 ARM64)的普及,虚拟地址空间从 32 位的 4GB 暴增到 48 位(256TB)乃至 57 位(128PB)。为了兼顾寻址灵活性与页表自身的物理内存开销,Linux 内核设计了一套高度统一且可向前兼容的五级页表抽象,而在绝大多数现代 x86-64 架构下,默认启用的是经典的四级页表映射模式。
本文从硬件 MMU 寻址原理与 Linux 内核源码实现两个维度,彻底剖析从虚拟地址(VA)到物理地址(PA)的逐级转换过程。
一、 虚拟地址结构与多级页表层级
在标准的 48 位虚拟地址模式下(4 级页表,页面大小为 4KB),一个 64 位的虚拟地址有效位为 48 位(低 48 位),其结构被划分为 5 个逻辑切片:
63 47 38 29 20 11 0
+-----------+----------+----------+----------+----------+-----------+
| 符号扩展位 | PGD Index| PUD Index| PMD Index| PTE Index| Offset |
| (16 bits) | (9 bits) | (9 bits) | (9 bits) | (9 bits) | (12 bits) |
+-----------+----------+----------+----------+----------+-----------+
- PGD (Page Global Directory):全局页目录,占 9 位($2^9 = 512$ 项)。
- P4D (Page Level 4 Directory):第 4 级页目录。在 4 级页表架构下,P4D 在内核中被折叠(folded),等同于 PGD。
- PUD (Page Upper Directory):上层页目录,占 9 位($2^9 = 512$ 项)。
- PMD (Page Middle Directory):中间页目录,占 9 位($2^9 = 512$ 项)。
- PTE (Page Table Entry):页表项,占 9 位($2^9 = 512$ 项)。
- Offset:页内偏移量,占 12 位($2^{12} = 4096 = 4KB$)。
每个页表项本身占用 8 字节(64 位),因此无论是 PGD、PUD、PMD 还是 PTE,一个完整的页表单级刚好占用一个标准 4KB 物理页($512 \times 8 \text{ Bytes} = 4096 \text{ Bytes}$),这一精妙的设计使得内核在为页表分配内存时可以直接使用伙伴系统(Buddy System)的单个 Page。
二、 MMU 硬件层级的逐级转换全景
当 CPU 执行一条访存指令(如 mov (%rax), %rbx)时,硬件 MMU 的翻译逻辑如下:
CR3 寄存器 (存储当前进程 PGD 物理基地址)
│
▼
┌───────────────┐
│ PGD 页表 │ ── (PGD Index 寻址)
└───────┬───────┘
│ 包含下一个 PUD 的物理基地址
▼
┌───────────────┐
│ PUD 页表 │ ── (PUD Index 寻址)
└───────┬───────┘
│ 包含下一个 PMD 的物理基地址
▼
┌───────────────┐
│ PMD 页表 │ ── (PMD Index 寻址)
└───────┬───────┘
│ 包含下一个 PTE 的物理基地址
▼
┌───────────────┐
│ PTE 页表 │ ── (PTE Index 寻址)
└───────┬───────┘
│ 包含目标 4KB 物理页帧基地址 (PFN)
▼
┌───────────────┐
│ 真实物理内存页 │ ── + Offset (页内偏移) ──► 目标物理数据
└───────────────┘
三、 PTE 页表项标志位与内存属性
PTE 不仅保存了物理页帧号(PFN, Page Frame Number),其低 12 位还编码了极其关键的硬件访问权限与状态控制位:
| 标志位 (Flag) | 位位置 | 作用与含义 |
|---|---|---|
_PAGE_PRESENT | Bit 0 | 存在位。为 1 表示物理页在内存中;为 0 则触发硬件缺页异常(Page Fault)。 |
_PAGE_RW | Bit 1 | 读写权限。0 为只读(Read-only),1 为可读写(Read/Write)。写保护与写时复制(COW)核心依赖此位。 |
_PAGE_USER | Bit 2 | 用户/特权级。1 允许用户态访问;0 仅限 Ring 0 内核态访问。 |
_PAGE_ACCESSED | Bit 5 | 访问标记。页面被读或写后硬件自动置 1,内核内存回收(LRU)靠此判断活跃度。 |
_PAGE_DIRTY | Bit 6 | 脏页标记。物理页被写入后硬件自动置 1,用于判断回写磁盘(Writeback)。 |
_PAGE_NX | Bit 63 | 禁止执行位(No-Execute)。为 1 时禁止在该内存页执行指令,防御栈溢出攻击。 |
四、 内核源码实战:手写虚拟地址到物理地址遍历函数
在内核驱动或调试模块中,如果我们想手动解析一个进程虚拟地址对应的物理地址,可以使用内核提供的一组标准宏与函数:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <asm/pgtable.h>
/**
* 遍历指定进程虚拟地址的多级页表,获取对应的物理地址
*/
unsigned long manual_virt_to_phys(struct mm_struct *mm, unsigned long vaddr)
{
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
spinlock_t *ptl;
unsigned long paddr = 0;
unsigned long page_addr = 0;
unsigned long page_offset = vaddr & ~PAGE_MASK;
if (!mm) {
pr_err("mm_struct is NULL\n");
return 0;
}
// 1. 获取 PGD
pgd = pgd_offset(mm, vaddr);
if (pgd_none(*pgd) || pgd_bad(*pgd)) {
pr_warn("Invalid PGD entry\n");
return 0;
}
// 2. 获取 P4D (在 4 级页表中直接返回 pgd)
p4d = p4d_offset(pgd, vaddr);
if (p4d_none(*p4d) || p4d_bad(*p4d)) {
pr_warn("Invalid P4D entry\n");
return 0;
}
// 3. 获取 PUD
pud = pud_offset(p4d, vaddr);
if (pud_none(*pud) || pud_bad(*pud)) {
pr_warn("Invalid PUD entry\n");
return 0;
}
// 4. 获取 PMD (若支持 HugePage 2MB,可在此处直接解析)
pmd = pmd_offset(pud, vaddr);
if (pmd_none(*pmd) || pmd_bad(*pmd)) {
pr_warn("Invalid PMD entry\n");
return 0;
}
// 5. 获取 PTE 并加锁 (PTE 映射可能会被并发修改)
pte = pte_offset_map_lock(mm, pmd, vaddr, &ptl);
if (!pte) {
pr_warn("Failed to map PTE\n");
return 0;
}
if (pte_present(*pte)) {
// 从 PTE 中提取物理页帧号 (PFN) 并左移 12 位
page_addr = pte_pfn(*pte) << PAGE_SHIFT;
paddr = page_addr | page_offset;
pr_info("VA: 0x%lx -> PA: 0x%lx (Flags: 0x%lx)\n", vaddr, paddr, pte_val(*pte) & 0xFFF);
} else {
pr_warn("Page not present in RAM (swapped or lazy alloc)\n");
}
// 释放 PTE 锁
pte_unmap_unlock(pte, ptl);
return paddr;
}
五、 核心设计思想与工程启示
为什么 Linux 不直接使用单级大数组记录映射?
- 内存节约(稀疏寻址):进程的虚拟地址空间绝大部分是未映射的“空洞”。多级页表允许只为已分配的地址区间创建子页表。一个典型的 10MB 进程,在 4 级页表下只需消耗几十 KB 的页表内存,而如果是单级平坦页表,无论进程多小,都必须占用数 GB 的连续空间来存放索引。
- 硬件 TLB 缓存的权衡:四级页表的最大代价是访存放大——每次未命中的内存访问需要连续查询 4 次内存。为了弥补这个开销,现代 CPU 依赖大容量 TLB(Translation Lookaside Buffer)以及透明大页(Transparent Huge Pages, 2MB / 1GB,跳过 PMD/PTE 直接寻址),在高性能计算与数据库场景中大幅削减 TLB Miss 开销。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/jiang_style/article/details/164464616




