页表是操作系统与 CPU 协同实现虚拟地址到物理地址映射的核心数据结构,本质是一份 “虚拟页→物理页” 的映射表,记录了虚拟地址空间中每个虚拟页(Virtual Page, VP)对应的物理页(Physical Page, 也称页框,Page Frame, PF),同时存储内存权限、页面状态等控制信息。它是虚拟内存机制的基石,解决了 “虚拟地址如何精准定位物理内存” 的核心问题。
本文将从 “本质→设计逻辑→结构→工作流程→优化机制→实际影响” 六个维度,结合 Linux 内核实现和 C++/Linux 编程场景,详细拆解页表的核心原理。
一、页表的核心本质:虚拟地址的 “翻译字典”
在分页机制中,操作系统将虚拟地址空间和物理内存均划分为固定大小的 “页”(通常为 4KB,也支持 8KB、16KB、2MB、1GB 等)。虚拟地址和物理地址的映射,本质是 “虚拟页号→物理页号” 的对应关系,而页表就是存储这份对应关系的 “字典”。
核心作用:
- 地址翻译:将进程操作的虚拟地址(如
0x0804a008)拆分为 “虚拟页号 + 页内偏移”,通过页表查询到对应的物理页号,再与页内偏移拼接,得到最终的物理地址(如0x12345008)。 - 内存控制:每页的映射项(页表项)中包含权限(读 / 写 / 执行)、存在状态(是否加载到物理内存)、脏位(是否被修改)等控制位,实现内存保护和页面管理。
- 隔离与共享:每个进程有独立的页表(用户空间部分),确保虚拟地址隔离;内核空间页表或共享内存的页表项可被多个进程共享,实现物理内存复用。
页表的核心优化是「按页映射」:
- 虚拟地址 = 虚拟页号(VPN) + 页内偏移(Offset)(4KB 页 → 偏移占 12 位);
- 物理地址 = 物理页号(PPN) + 页内偏移(Offset)(偏移完全复用,无需映射);
- 页表只需存储「VPN→PPN」的对应关系,一个 4GB 虚拟地址空间仅需 1024*1024 个映射项(4GB/4KB=1M),内存开销从 4GB 降至 4MB(每个映射项 4 字节)。
二、为什么需要页表?(设计初衷与演进)
在页表出现前,操作系统曾使用 “分段机制”(直接将虚拟地址按段划分映射到物理内存),但存在两大缺陷:
- 物理内存碎片化:进程需要连续的物理内存段,大块内存需求易因碎片无法满足;
- 映射效率低:分段机制需存储段基址、段长度等信息,地址转换时需多次边界检查,且无法高效实现按需加载。
页表通过 “分页 + 映射” 解决了这些问题,但单级页表又带来新的问题:内存占用过大。以 32 位系统为例,若页大小为 4KB(虚拟地址拆分为 10 位页号 + 12 位偏移),单级页表需2^10=1024个页表项;若为 64 位系统(假设页大小 4KB,虚拟页号占 52 位),单级页表需2^52个页表项,每个页表项 8 字节,仅页表就需2^52 × 8B = 4PB内存,完全不可行。
因此,现代操作系统均采用多级页表(如 Linux 32 位用二级页表,64 位用四级页表),核心思想是 “按需创建页表”—— 仅为存在映射的虚拟页创建对应的下级页表,大幅减少页表占用的物理内存。
三、页表的结构:从单级到多级(Linux 实现)
早期系统用「单级页表」(一张大表存储所有 VPN→PPN 映射),但存在 “内存浪费” 问题 —— 即使进程只用到 100MB 虚拟内存,也需要分配 4MB 页表(4GB/4KB*4 字节)。
Linux 采用「多级页表」(x86-64 是 4 级),核心思想是「按需创建子页表」,仅为已使用的虚拟地址范围分配页表,大幅节省内存。
页表的结构由 CPU 架构和操作系统共同定义,Linux 的页表结构严格遵循 CPU 的地址转换规范(如 x86-32、x86-64、ARM 架构)。以下分 “单级页表”(基础原理)和 “多级页表”(实际实现)详细说明。
| 页表级别 | 名称 | 作用 | 存储位置(物理内存) |
|---|---|---|---|
| 第 1 级 | PGD(页全局目录) | 页表根节点,每个进程 1 份 | 独立物理页,mm_struct->pgd 指向其物理地址 |
| 第 2 级 | PUD(页上级目录) | 从 PGD 索引到 PUD 表项 | 按需创建(仅当对应虚拟地址范围被使用时分配) |
| 第 3 级 | PMD(页中间目录) | 从 PUD 索引到 PMD 表项 | 按需创建 |
| 第 4 级 | PTE(页表项) | 最终映射:VPN→PPN | 按需创建 |
1. 单级页表(基础模型)
单级页表是最简单的页表结构,本质是一个数组,数组索引 = 虚拟页号(VPN),数组元素 = 页表项(Page Table Entry, PTE)。
核心结构:
- 虚拟地址拆分:
虚拟地址 = 虚拟页号(VPN) + 页内偏移(Offset)- 例:32 位系统,页大小 4KB(
2^12)→ 虚拟页号 10 位(2^10=1024个页表项),页内偏移 12 位。
- 例:32 位系统,页大小 4KB(
- 页表项(PTE):存储物理页号(PFN)+ 控制位(权限、存在位等),通常占 4 字节(32 位)或 8 字节(64 位)。
地址转换流程(单级):
- CPU 将虚拟地址拆分为 VPN 和 Offset;
- 以 VPN 为索引,查询单级页表数组,得到对应的 PTE;
- 从 PTE 中提取物理页号(PFN),与 Offset 拼接,得到物理地址;
- 若 PTE 中 “存在位” 为 0(页面未加载到物理内存),触发缺页中断。
缺陷:
- 页表占用内存固定,与虚拟地址空间大小成正比,64 位系统完全不可用。
2. 多级页表(Linux 实际实现)
多级页表将虚拟页号拆分为多个层级的 “索引”,每个索引对应一级页表,仅当某一级索引对应的页表存在时,才创建下级页表。Linux 根据 CPU 位数和页大小,采用不同的页表层级:
- 32 位系统(页大小 4KB):二级页表(页目录表→页表);
- 64 位系统(x86-64,页大小 4KB):四级页表(PGD→PUD→PMD→PT);
- 64 位系统(大页,如 2MB):三级页表(PGD→PUD→PMD,PMD 直接映射物理页)。
(1)Linux 32 位二级页表结构
虚拟地址拆分(32 位,4KB 页):
虚拟地址 = 页目录索引(10位) + 页表索引(10位) + 页内偏移(12位)
- 一级页表(页目录表,Page Directory, PGD):
- 每个进程有一个独立的 PGD,存储在进程的
mm_struct结构体中(mm->pgd); - PGD 是一个数组,共
2^10=1024个项(页目录项,Page Directory Entry, PDE); - 每个 PDE 存储二级页表(PT)的物理页号,以及该二级页表的控制权限。
- 每个进程有一个独立的 PGD,存储在进程的
- 二级页表(页表,Page Table, PT):
- 每个 PGD 项对应一个 PT(仅当有虚拟页映射时创建);
- PT 也是一个数组,共
2^10=1024个项(页表项,PTE); - 每个 PTE 存储虚拟页对应的物理页号(PFN),以及页面的控制信息。
(2)Linux 64 位四级页表结构(x86-64)
x86-64 架构仅使用 64 位地址中的 48 位(虚拟地址范围 0~2^48-1,即 256TB),虚拟地址拆分(4KB 页):
虚拟地址 = PGD索引(9位) + PUD索引(9位) + PMD索引(9位) + PT索引(9位) + 页内偏移(12位)
- 各级页表含义:
- PGD(Page Global Directory):全局页目录,每个进程独立,存储 PUD 的物理地址;
- PUD(Page Upper Directory):上级页目录,存储 PMD 的物理地址;
- PMD(Page Middle Directory):中级页目录,存储 PT 的物理地址;
- PT(Page Table):页表,存储 PTE(虚拟页→物理页映射)。
多级页表的核心优势:
- 按需创建:仅为有映射的虚拟页创建下级页表。例如,一个进程仅使用 100 个虚拟页,64 位四级页表仅需创建 1 个 PGD 项、1 个 PUD 项、1 个 PMD 项、1 个 PT(含 100 个 PTE),总占用内存仅
4KB(PGD) + 4KB(PUD) + 4KB(PMD) + 4KB(PT)= 16KB,远低于单级页表的 4PB。 - 内存高效:页表本身也按页存储(4KB),可被内核高效管理(如置换不常用的页表页)。
3. 页表项(PTE)的核心属性(以 Linux x86 为例)
页表项是页表的最小单位,存储映射关系和控制信息,x86 架构中 32 位 PTE 占 4 字节,64 位 PTE 占 8 字节。核心属性(控制位)如下:
| 控制位 | 含义 | 作用场景 |
|---|---|---|
| 存在位(P) | 1 = 页面已加载到物理内存;0 = 页面未加载(在磁盘或交换分区) | 触发缺页中断的关键:P=0 时,CPU 触发 #PF 中断,内核加载页面到物理内存。 |
| 读写位(R/W) | 1 = 页面可写;0 = 页面只读 | 控制页面写权限:尝试写入 R/W=0 的页面,触发段错误(SIGSEGV)。 |
| 用户 / 内核位(U/S) | 1 = 用户态进程可访问;0 = 仅内核态可访问 | 实现用户空间与内核空间隔离:用户进程无法访问 U/S=0 的页面(如内核代码段)。 |
| 执行位(X/D) | 1 = 页面可执行;0 = 页面不可执行(x86-64 新增,旧架构通过 R/W 位间接控制) | 防止代码注入:数据段(如堆、栈)设为 X=0,避免执行恶意注入的代码。 |
| 脏位(D) | 1 = 页面被修改过(脏页);0 = 页面未被修改(干净页) | 页面置换时优化:D=1 的页面需写回磁盘,D=0 的页面可直接释放。 |
| 访问位(A) | 1 = 页面最近被访问过;0 = 页面长期未被访问 | 页面置换算法(如 LRU)的依据:优先置换 A=0 的页面。 |
| 页大小位(PS) | 1 = 大页(如 2MB/4MB);0 = 普通页(4KB) | 控制页大小:大页可减少页表项数量,提升地址转换效率。 |
| 全局位(G) | 1 = 页面为全局页(所有进程共享);0 = 进程私有页 | 共享内核页或共享库代码段:G=1 的页面 TLB 缓存不随进程切换失效。 |
这些控制位由 CPU 硬件(MMU)和内核共同维护:
- 硬件(MMU):地址转换时检查 P、R/W、U/S、X 位,不合法则触发中断;自动更新 A 位(访问时)和 D 位(写入时)。
- 内核:缺页中断时设置 P 位;页面置换时检查 D 位决定是否写回磁盘;通过
mprotect系统调用修改 R/W、X 位。
四、页表的工作流程:虚拟地址→物理地址的完整转换
页表的核心使命是完成地址转换,整个流程由 “CPU 硬件(MMU)+ 内核软件” 协同完成,以 Linux 32 位二级页表为例,步骤如下:
前提准备:
- 进程创建时,内核为其分配 PGD(页目录表),并初始化默认 PDE(如代码段、数据段对应的 PT);
- 进程切换时,内核将当前进程的 PGD 物理地址写入 CPU 的 CR3 寄存器(MMU 通过 CR3 找到当前页表)。
转换步骤(以虚拟地址0x0804a008为例):
-
虚拟地址拆分:假设页大小 4KB(12 位偏移),32 位虚拟地址拆分为:
- 页目录索引(10 位):
0x0804a008 >> 22 = 0x0002(二进制前 10 位); - 页表索引(10 位):
(0x0804a008 >> 12) & 0x3FF = 0x004A(中间 10 位); - 页内偏移(12 位):
0x0804a008 & 0xFFF = 0x008(最后 12 位)。
- 页目录索引(10 位):
-
查找页目录表(PGD):
- MMU 从 CR3 寄存器中读取当前进程的 PGD 物理地址(如
0x10000000); - 以页目录索引(
0x0002)为偏移,计算 PDE 的物理地址:0x10000000 + 0x0002 × 4B = 0x10000008(32 位 PDE 占 4B); - 读取该 PDE 的值(如
0x20000007),解析得到:- 物理页号(PFN):
0x20000007 >> 12 = 0x2000(PDE 的高 20 位,对应 PT 的物理页地址0x20000000); - 控制位:
0x20000007 & 0xFFF = 0x007(P=1,R/W=1,U/S=1,即 PT 已存在,可读写,用户态可访问)。
- 物理页号(PFN):
- MMU 从 CR3 寄存器中读取当前进程的 PGD 物理地址(如
-
查找页表(PT):
- 以 PDE 中的 PT 物理地址(
0x20000000)为基址,页表索引(0x004A)为偏移,计算 PTE 的物理地址:0x20000000 + 0x004A × 4B = 0x20000128; - 读取该 PTE 的值(如
0x30000007),解析得到:- 物理页号(PFN):
0x30000007 >> 12 = 0x3000(对应物理页地址0x30000000); - 控制位:
0x30000007 & 0xFFF = 0x007(P=1,R/W=1,U/S=1,页面已加载,可读写)。
- 物理页号(PFN):
- 以 PDE 中的 PT 物理地址(
-
拼接物理地址:
- 物理页地址(
0x30000000)+ 页内偏移(0x008)= 最终物理地址0x30000008; - MMU 将该物理地址发送给内存控制器,读取 / 写入对应内存单元。
- 物理页地址(
异常情况:缺页中断(Page Fault)
若步骤 3 中读取 PTE 时发现 “存在位(P)=0”,说明页面未加载到物理内存,流程变为:
- CPU 触发缺页中断(#PF),内核暂停当前进程,执行缺页中断处理函数;
- 内核查找页面所在位置(如 ELF 文件的代码段、数据段,或交换分区);
- 内核分配空闲物理页框,将磁盘上的页面加载到该页框;
- 更新 PTE:将新分配的物理页号写入 PTE,设置 P=1,更新其他控制位;
- 恢复当前进程,重新执行触发缺页的指令,此时地址转换成功。
五、页表的优化机制:解决多级页表的性能瓶颈
多级页表解决了内存占用问题,但引入了新的瓶颈:地址转换需多次访问内存(如四级页表需 4 次内存访问),而内存访问速度远低于 CPU 运算速度。为解决此问题,CPU 和操作系统引入了两大核心优化:TLB(快表)和大页(Huge Page)。
1. TLB(Translation Lookaside Buffer):页表缓存
TLB 是 CPU 内置的高速缓存(容量小、速度快),专门缓存最近使用的 “虚拟页号→物理页号” 映射关系,本质是 “页表的缓存”。
工作原理:
- 地址转换时,CPU 先查询 TLB:
- 若 TLB 命中(存在该 VPN 的映射):直接提取 PFN,与 Offset 拼接得到物理地址,无需访问内存中的页表(耗时仅 1~2 个 CPU 时钟周期);
- 若 TLB 未命中:执行多级页表查询(多次内存访问,耗时约 100~200 个 CPU 时钟周期),并将查询结果写入 TLB,供后续访问复用。
TLB 的关键特性:
- 上下文切换失效:进程切换时,CR3 寄存器更新为新进程的 PGD 地址,TLB 中的旧进程映射失效(需清空或标记为无效),这是进程切换的开销之一;
- 全局页例外:G=1 的页面(如内核页、共享库代码段)的 TLB 映射不会因进程切换失效,减少 TLB 重建开销;
- 多级 TLB:现代 CPU(如 x86-64)采用 L1-TLB(小容量、高速度)和 L2-TLB(大容量、中速度),进一步提升命中率。
对编程的影响:
- 内存访问的局部性越好,TLB 命中率越高,程序运行越快。例如,连续访问数组(空间局部性)比随机访问指针链表(无局部性)的 TLB 命中率高得多。
2. 大页(Huge Page):减少页表层级和 TLB 条目
大页是将页大小从默认的 4KB 扩大到 2MB、4MB、1GB 等,核心优势是 “以更大的页为单位映射,减少页表项数量和 TLB 条目数量”。
工作原理(以 Linux 64 位 2MB 大页为例):
- 虚拟地址拆分:
PGD索引(9位) + PUD索引(9位) + PMD索引(9位) + 页内偏移(21位)(无 PT 层级); - 映射关系:PMD 直接指向 2MB 的物理页框,无需创建 PT,减少一级页表查询;
- TLB 效率:一个 2MB 大页仅需 1 个 TLB 条目,可覆盖
2MB / 4KB = 512个普通页的地址范围,大幅提升 TLB 命中率。
应用场景:
- 大内存进程(如数据库、虚拟机):使用大页可减少页表占用的内存,降低 TLB 失效频率,提升性能;
- 内核参数配置(Linux):
# 启用2MB大页(临时) echo 1024 > /proc/sys/vm/nr_hugepages # 查看大页使用情况 cat /proc/meminfo | grep HugePages
缺陷:
- 内存利用率降低:大页无法拆分,若进程仅需少量内存,也需占用整个大页,导致内存碎片。
六、页表与进程、内核的关系
1. 每个进程的页表:独立与共享并存
- 独立部分:用户空间的页表(PGD→PUD→PMD→PT)是进程私有的,确保虚拟地址隔离。例如,进程 A 的虚拟页
0x0804a000和进程 B 的0x0804a000对应不同的 PTE,映射到不同的物理页。 - 共享部分:内核空间的页表(虚拟地址 3~4GB,64 位为 2^47~2^48)是所有进程共享的。内核在初始化时创建内核页表,所有进程的 PGD 都指向同一内核页表,因此进程切换到内核态后,可直接访问内核空间的物理内存。
2. 内核对页表的管理操作
内核通过一系列数据结构和函数管理页表,核心数据结构是进程的mm_struct(内存描述符):
// Linux内核mm_struct关键字段(简化)
struct mm_struct {
pgd_t *pgd; // 指向进程的PGD(页目录表)物理地址
struct vm_area_struct *mmap; // 虚拟地址区域(VMA)链表
unsigned long start_brk, brk; // 堆的范围
unsigned long start_stack; // 栈的起始地址
// ... 其他字段
};
内核对页表的核心操作:
- 创建页表:
fork()时,内核为子进程复制父进程的 PGD,并通过写时复制(COW)共享 PT 和 PTE(仅当修改时才复制); - 销毁页表:进程退出时,内核遍历 PGD→PUD→PMD→PT,释放所有页表占用的物理内存;
- 修改页表:
mmap()创建映射时,内核分配 PT 并添加 PTE;mprotect()修改权限时,内核更新 PTE 的控制位; - 切换页表:进程调度时,内核将当前进程的
mm->pgd写入 CR3 寄存器,完成页表切换。
七、页表在编程中的实际影响(C++/Linux 场景)
理解页表的原理,能帮助解决底层编程中的核心问题,以下是关键应用场景:
1. 段错误(SIGSEGV)的根源与调试
段错误的本质是 “页表项控制位与操作不匹配”,常见原因:
- 访问未映射的虚拟地址(PTE 的 P=0,且无对应的磁盘页面);
- 写入只读页面(PTE 的 R/W=0);
- 执行数据页面(PTE 的 X=0);
- 访问内核空间页面(PTE 的 U/S=0)。
调试方法:
- 查看进程的页表布局:
cat /proc/[pid]/maps(显示 VMA 的虚拟地址范围、权限、映射文件); - 查看页表项详情(需 root):
cat /proc/[pid]/pagemap(每个 PTE 占 8 字节,包含 PFN 和控制位); - 示例:访问
NULL指针(虚拟地址 0x0)触发段错误,因为 0x0 附近的 VMA 未映射(P=0)。
2. 动态内存分配(malloc/new)与页表
malloc分配小块内存(<128KB)时,通过brk()扩展堆,内核仅更新 VMA 范围,未创建 PTE;首次写入时触发缺页中断,内核分配物理页并创建 PTE;malloc分配大块内存(≥128KB)时,通过mmap()创建匿名映射,内核直接创建 VMA 和 PTE(P=0),首次访问触发缺页中断。
3. 共享内存与页表
- 父子进程通过
mmap(MAP_ANONYMOUS | MAP_SHARED)共享内存时,内核为两者创建相同的 PTE(指向同一物理页),因此修改共享内存会同步; - 非父子进程通过 System V 共享内存(
shmget)时,内核创建共享内存段对应的物理页,多个进程通过shmat将其映射到各自的 VAS,PTE 指向同一物理页。
4. 内存优化:提升 TLB 命中率
- 减少内存访问的随机性:尽量使用连续内存(如数组),避免频繁随机访问指针;
- 使用大页:对大内存进程启用 HugePage,减少 TLB 失效;
- 避免过度内存碎片化:合理规划
malloc/free的调用,减少小块内存的频繁分配与释放。
八、总结:页表的核心价值与知识体系串联
页表是虚拟内存机制的 “翻译核心”,其核心价值在于:
- 实现地址虚拟化:将进程的虚拟地址精准映射到物理地址,屏蔽物理内存的分布细节;
- 保障隔离与安全:通过独立页表和控制位,实现进程间内存隔离和权限控制;
- 优化内存效率:结合缺页中断实现按需加载,结合多级页表减少页表内存占用;
- 提升访问性能:通过 TLB 缓存和大页,降低地址转换的时间开销。
从知识体系来看,页表是连接 “虚拟地址空间” 和 “物理内存” 的桥梁:
- 虚拟地址空间提供了进程的 “内存假象”(连续、独立);
- 页表将这份 “假象” 落地为物理内存的实际映射;
- MMU 和 TLB 则为映射提供了硬件级别的高效支持。
700

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



