一、分页机制的起源与目标
现代操作系统(如 Linux)采用虚拟内存技术,将程序使用的 “虚拟地址”(Virtual Address, VA)与物理内存的 “物理地址”(Physical Address, PA)分离。这种分离带来两大好处:
- 内存隔离:不同程序的虚拟地址空间互不干扰,避免越界访问。
- 内存扩展:程序可用的虚拟地址空间可远大于物理内存(通过磁盘交换空间补充)。
而分页(Paging)是实现虚拟内存的核心机制之一。它将虚拟地址和物理地址划分为固定大小的 “页”(Page,通常为 4KB),通过 “页表” 记录虚拟页到物理页的映射关系。对于 64 位架构(如 x86-64),由于虚拟地址空间极大(理论上 2⁶⁴字节,约 16EB),单级页表会导致内存浪费(即使只使用少量内存,也需创建完整页表),因此引入多级分页模型,通过分层逐级查找,实现按需加载页表,减少内存占用。
二、x86-64 架构的四级分页结构
x86-64 架构采用四级分页模型,虚拟地址被划分为 5 个部分(包括页内偏移),各级页表逐级索引,最终找到物理页帧(Page Frame)。以下是详细结构:
1. 地址空间基础参数
- 虚拟地址长度:x86-64 当前常用48 位(Linux 默认,通过
CONFIG_PAGE_OFFSET
配置,最高可扩展至 57 位),剩余高位用于符号扩展(全 0 或全 1,避免地址空间浪费)。 - 页大小:默认4KB(2¹² 字节),因此页内偏移占12 位(VA 的最低 12 位)。
- 各级页表索引位:剩余 48-12=36 位,分为 4 级,每级9 位(早期 x86-64 曾用 10 位,随架构演进调整,当前 Linux 内核源码定义为 9 位)。
2. 四级页表的层级与作用
虚拟地址从高位到低位依次拆分为 4 个索引字段,对应四级页表:
层级 | 名称 | 缩写 | 索引位范围(48 位 VA) | 作用 | 数据结构(Linux 内核) |
---|---|---|---|---|---|
第 1 级 | 全局页目录(Page Global Directory) | PGD | 47-39 位 | 指向 “上层页目录表” 的基地址。每个进程对应一个 PGD,存储全局页目录项。 | pgd_t (本质是指针类型) |
第 2 级 | 上层页目录(Page Upper Directory) | PUD | 38-30 位 | 指向 “中间页目录表” 的基地址。在大地址空间中进一步细分。 | pud_t |
第 3 级 | 中间页目录(Page Middle Directory) | PMD | 29-21 位 | 指向 “页表” 的基地址。 | pmd_t |
第 4 级 | 页表(Page Table) | PTE | 20-12 位 | 指向物理页帧的基地址,包含访问权限、缓存策略等标志位。 | pte_t |
页内偏移 | 页内偏移(Page Offset) | - | 11-0 位 | 物理页内的具体偏移地址。 | - |
3. 虚拟地址到物理地址的转换过程
假设虚拟地址为VA = [PGD_INDEX][PUD_INDEX][PMD_INDEX][PTE_INDEX][OFFSET]
,转换步骤如下:
-
第 1 级:全局页目录(PGD)
- 从当前进程的页目录基址寄存器(CR3,存储 PGD 的物理地址)获取 PGD 的基地址。
- 用
PGD_INDEX
(9 位)索引 PGD,找到对应的PGD 项。若 PGD 项有效(存在上层页目录表),获取 PUD 基地址。 - 若 PGD 项无效(如未分配内存),触发缺页中断(Page Fault),由内核分配物理页并创建页表。
-
第 2 级:上层页目录(PUD)
- 用
PUD_INDEX
(9 位)索引 PUD 表,找到对应的PUD 项,获取 PMD 基地址。
- 用
-
第 3 级:中间页目录(PMD)
- 用
PMD_INDEX
(9 位)索引 PMD 表,找到对应的PMD 项,获取页表(PTE 表)基地址。
- 用
-
第 4 级:页表(PTE)
- 用
PTE_INDEX
(9 位)索引 PTE 表,找到对应的PTE 项。若 PTE 项有效,获取物理页帧基地址(PA 的高位部分)。 - 物理地址 = 物理页帧基地址 + 页内偏移(OFFSET)。
- 用
4. 页表项(PTE)的关键标志位
每个 PTE 存储物理页的元数据,重要标志位包括:
- PRESENT(P):1 位,表示该页是否在物理内存中(0 表示在磁盘交换空间)。
- RW(读写权限):1 位,0 表示只读,1 表示可写。
- USER(用户态访问权限):1 位,0 表示仅内核态可访问,1 表示用户态可访问。
- ACCESSED(A):1 位,页被访问时由硬件自动置 1,用于页面置换算法(如 LRU)。
- DIRTY(D):1 位,页被写入时置 1,用于脏页回写磁盘。
- GLOBAL(G):1 位,标记全局页(所有进程可见,如内核代码段),TLB 会缓存此类页。
三、四级分页的内存布局与数据结构
在 Linux 内核中,四级分页通过嵌套的指针结构实现,各级页表本质是数组,每个元素对应一个索引项。
1. 页目录基址寄存器(CR3)
- 每个进程有独立的 CR3 寄存器,指向其 ** 页目录根(PGD)** 的物理地址。
- 进程切换时,内核需更新 CR3,切换到新进程的 PGD,实现地址空间隔离。
2. 各级页表的存储
- PGD:每个进程一个 PGD,通常存储在进程的
mm_struct
结构体中(mm->pgd
)。 - PUD/PMD/PTE 表:按需动态分配。例如,当访问某个虚拟地址时,若对应页表未创建,内核会分配新的页表(通常为一个物理页,4KB,可存储
4KB/8B=512
个项,对应 9 位索引,2⁹=512 项)。
3. 内核中的数据结构定义
以下是 Linux 内核源码(arch/x86/include/asm/pgtable_types.h
)中的关键定义:
// 页目录项(PGD)、上层页目录项(PUD)、中间页目录项(PMD)本质是指针类型
typedef u64 pgd_t;
typedef u64 pud_t;
typedef u64 pmd_t;
typedef u64 pte_t;
// 页表层级数,x86-64为4级
#define PAGETABLE_LEVELS 4
各级页表的查询函数(arch/x86/include/asm/pgtable.h
):
pgd_t pgd_index(struct mm_struct *mm, unsigned long addr); // 获取PGD索引
pud_t pud_index(pgd_t pgd, unsigned long addr); // 获取PUD索引
pmd_t pmd_index(pud_t pud, unsigned long addr); // 获取PMD索引
pte_t pte_index(pmd_t pmd, unsigned long addr); // 获取PTE索引
四、四级分页的性能优化:TLB 与大页
虽然四级分页需要四次内存访问(查四级页表),但硬件通过 **TLB(Translation Lookaside Buffer,转换旁路缓冲)** 大幅提升效率,软件也可通过 “大页” 减少页表层级。
1. TLB:页表缓存
- 作用:缓存最近使用的虚拟地址到物理地址的映射,避免每次访问都查页表。
- 结构:TLB 是硬件缓存,分为指令 TLB和数据 TLB,按页大小分为 “小页(4KB)” 和 “大页(2MB/1GB)” 缓存项。
- 命中流程:访问虚拟地址时,先查 TLB,若命中,直接获取物理地址;若未命中(TLB 缺失),再查四级页表,并将结果写入 TLB。
2. 大页(Huge Page)
- 原理:使用更大的页(如 2MB 或 1GB),减少页表层级。例如,2MB 大页的页内偏移为 21 位(2²¹=2MB),虚拟地址可跳过 PMD 和 PTE 层级,直接通过 PGD 和 PUD 找到大页。
- 优势:
- 减少页表数量:大页的页表项直接存储在 PMD 中,无需创建 PTE 表。
- 提升 TLB 命中率:大页占用 TLB 的一个条目,可映射更大的内存区域,减少 TLB 缺失。
- Linux 支持:通过
hugetlbfs
文件系统分配大页,内核配置CONFIG_HUGETLB_PAGES
开启。
五、四级分页在 Linux 中的实现细节
1. 页表的创建与销毁
- 进程创建:
fork()
时复制父进程的 PGD,PUD/PMD/PTE 表按需共享(写时复制)。 - 内存分配:调用
mmap()
时,内核通过pgd_alloc()
分配 PGD,逐层检查并创建 PUD/PMD/PTE 表(函数链:handle_pte_fault()
→pmd_alloc()
→pud_alloc()
→pgd_alloc()
)。 - 内存释放:
munmap()
时反向销毁页表,释放空闲页表的物理内存。
2. 内核地址空间与用户地址空间
- 用户地址空间(0-3GB,32 位)/(0-128TB,48 位 x86-64):由进程独立的 PGD 管理,不同进程地址空间隔离。
- 内核地址空间(3-4GB,32 位)/(128TB-16EB,48 位 x86-64):所有进程共享同一套内核页表,PGD 中内核部分固定映射,通过
kmap()
等函数访问。
3. 页表遍历的内核函数
Linux 提供一组函数用于遍历页表,例如:
// 从虚拟地址获取各级页表项
pgd_t *pgd_offset(struct mm_struct *mm, unsigned long addr); // 获取PGD项指针
pud_t *pud_offset(pgd_t *pgd, unsigned long addr); // 获取PUD项指针
pmd_t *pmd_offset(pud_t *pud, unsigned long addr); // 获取PMD项指针
pte_t *pte_offset_map(pmd_t *pmd, unsigned long addr); // 获取PTE项指针(并映射到内核空间)
六、四级分页的挑战与优化
1. 页表内存占用
- 若每个进程使用 4KB 小页,四级页表的理论内存占用为:
- 每级页表项:8B(64 位指针)
- 总项数:512(PGD) + 512×512(PUD) + 512×512×512(PMD) + 512⁴(PTE) → 显然不可能全部分配!
- 优化:按需分配(惰性创建),未使用的页表不分配物理内存;大页减少页表层级。
2. TLB 缺失开销
- 每次 TLB 缺失需访问 4 次内存(查四级页表),耗时约数十纳秒,远高于 TLB 命中(约 1 纳秒)。
- 优化:利用 TLB 的全局页(G 标志位),减少进程切换时的 TLB 刷新;使用大页提升 TLB 命中率。
3. 地址空间布局随机化(ASLR)
- 通过随机化 PGD 的基址,增加攻击者预测虚拟地址的难度,提升安全性。Linux 通过
/proc/sys/kernel/randomize_va_space
控制。
七、对比其他架构:分页级数的差异
x86-64 的四级分页并非唯一方案,不同架构根据地址空间需求设计分页级数:
- ARMv8(64 位):默认三级分页(虚拟地址 48 位,页大小 4KB,三级索引各 12 位),可扩展至四级(针对更大地址空间)。
- PowerPC(64 位):支持三级或四级分页,取决于页大小和地址空间配置。
- x86-32(32 位):早期使用两级分页(PGD 和 PTE),页大小 4KB,虚拟地址空间 4GB。
八、总结:四级分页的核心价值
- 分层管理:将超大虚拟地址空间拆解为四级索引,避免单级页表的内存爆炸问题。
- 按需分配:仅为已使用的虚拟地址创建页表,节省内存。
- 兼容性与扩展性:支持 48 位 / 57 位虚拟地址,适应 64 位架构的长期演进。
- 性能平衡:通过 TLB 和大页技术,在地址转换效率与内存占用间找到平衡。
形象比喻:把四级分页想象成 “快递分拣四件套”
你可以把计算机的 “虚拟地址到物理地址转换” 想象成一次快递配送过程,而四级分页模型就是快递从 “虚拟收件地址” 到 “真实房间地址” 的四级分拣系统。
1. 场景设定
- 你的快递地址(虚拟地址):广东省广州市天河区珠江新城 1 栋 1001 室
- 真实仓库地址(物理地址):仓库货架第 8 区第 3 排第 5 层第 2 号格子
快递员看不懂 “虚拟地址”,需要通过四级分拣中心,层层拆解地址,最终找到真实位置。
2. 四级分拣中心(对应四级分页)
分拣层级 | 对应分页术语 | 作用 | 类比地址拆解 |
---|---|---|---|
第 1 级:市级分拣站 | 全局页目录(PGD, Page Global Directory) | 确定 “广东省”,缩小到某个大区域。每个省对应一个 “分拣账本”(页目录表)。 | 从 “广东省” 找到对应的第一级账本索引 |
第 2 级:区级中转站 | 上层页目录(PUD, Page Upper Directory) | 确定 “广州市天河区”,在大区域内进一步细分。每个区对应账本中的一条记录。 | 从 “广州市天河区” 找到第二级索引 |
第 3 级:街道分拨点 | 中间页目录(PMD, Page Middle Directory) | 确定 “珠江新城”,在区内缩小到具体街道。每个街道对应一条记录。 | 从 “珠江新城” 找到第三级索引 |
第 4 级:小区快递柜 | 页表项(PTE, Page Table Entry) | 确定 “1 栋 1001 室”,最终定位到具体房间。房间号对应物理地址的 “偏移量”。 | 从 “1 栋 1001 室” 找到物理地址的具体位置 |
3. 核心原理
- 虚拟地址是 “快递面单上的假地址”:程序看到的地址是虚拟的,方便管理(比如多个程序可以共用 “1001 室”,互不干扰)。
- 四级分页是 “地址翻译器”:每一级都像一本字典,通过 “索引”(类似地址中的区、街道、门牌号)层层查找,最终找到物理内存的真实位置。
- 为什么需要四级?:如果地址空间太大(比如 64 位系统的内存地址长达 16EB),一级分拣根本管不过来,必须分层管理,就像大城市需要区→街道→小区→房间四级地址,否则快递员会疯掉!
4. 一句话总结
四级分页就是把一个超长的虚拟地址拆成四段,每一段对应一个 “地址字典”,通过四次翻字典,把 “假地址” 翻译成 “真实内存地址”,就像快递员按地址四级分拣找到你家一样。