mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2362 字
7 分钟
TLB 与页表
2026-03-14

你的程序使用虚拟地址访问内存,但 DRAM 只认物理地址。每次内存访问都需要一次地址翻译——从虚拟地址到物理地址。如果每次翻译都要查页表(存在内存中),那每次内存访问实际上需要两次内存访问:一次查页表,一次访问数据。

TLB(Translation Lookaside Buffer)是地址翻译的缓存,将翻译结果保存在 CPU 内部,避免每次都查页表。TLB 未命中的代价可能比缓存未命中更高——因为页表遍历需要多次内存访问。

一、虚拟内存与页表#

1.1 为什么需要虚拟内存?#

  • 隔离:每个进程有独立的地址空间,互不干扰
  • 简化编程:程序员使用连续的虚拟地址,无需关心物理内存的碎片
  • 按需分配:虚拟地址空间可以大于物理内存,通过换页实现
  • 保护:页表项包含权限位,实现读/写/执行保护

1.2 页式内存管理#

虚拟地址空间被划分为固定大小的页(Page),物理内存被划分为同样大小的页帧(Page Frame)。页表记录虚拟页到物理页帧的映射。

虚拟地址:[页号][页内偏移]
页表查找 → 物理页帧号
物理地址:[物理页帧号][页内偏移]

1.3 页大小#

页大小适用场景Linux 支持
4 KB默认页大小默认
2 MBHuge Pages透明大页 / hugetlbfs
1 GBGiant Pageshugetlbfs
16 KBARM 默认ARM 配置
64 KBPowerPCPPC 配置

二、多级页表#

2.1 为什么需要多级页表?#

以 x86-64 为例:虚拟地址 48 位,页大小 4KB(12 位偏移),页号 36 位。如果用单级页表:

  • 页表项数 = 2^36 = 64G 项
  • 每项 8 字节 → 页表大小 = 512 GB!

这显然不可行。多级页表通过分级索引解决了这个问题——只有实际使用的虚拟地址范围才需要分配页表页。

2.2 x86-64 的 4 级页表#

graph TB VA["虚拟地址<br/>48 位"] --> PML4["PML4<br/>9 位<br/>第 4 级"] PML4 --> PDPT["PDPT<br/>9 位<br/>第 3 级"] PDPT --> PD["PD<br/>9 位<br/>第 2 级"] PD --> PT["PT<br/>9 位<br/>第 1 级"] PT --> PAGE["物理页<br/>12 位偏移<br/>4 KB"] style VA fill:#fff9c4,stroke:#f9a825 style PAGE fill:#e8f5e9,stroke:#2e7d32

地址分解:9 + 9 + 9 + 9 + 12 = 48 位

每一级页表有 512 个条目(2^9),每个条目 8 字节,恰好占一个 4KB 页。

2.3 页表遍历的过程#

一次地址翻译需要 4 次内存访问(4 级页表):

1. 读取 CR3 寄存器 → PML4 表基址
2. 用 PML4 索引查 PML4 表 → PDPT 表基址 (内存访问 1)
3. 用 PDPT 索引查 PDPT 表 → PD 表基址 (内存访问 2)
4. 用 PD 索引查 PD 表 → PT 表基址 (内存访问 3)
5. 用 PT 索引查 PT 表 → 物理页帧号 (内存访问 4)
6. 物理页帧号 + 偏移 → 物理地址

如果每次内存访问都要 4 次页表查找,性能将灾难性下降。这就是 TLB 存在的意义。

三、TLB 的结构与工作原理#

3.1 TLB 是什么?#

TLB 是页表条目的缓存——缓存虚拟页号到物理页帧号的映射。TLB 命中时,地址翻译只需 1 个周期;TLB 未命中时,需要遍历页表(4 次内存访问)。

3.2 现代 CPU 的 TLB 配置#

CPUL1 ITLBL1 DTLBL2 STLBL2 STLB 相联度
Intel Skylake64 项 4-way64 项 4-way1536 项 12-way12
AMD Zen 464 项64 项2048 项8
Apple M1~128 项~128 项~3072 项

3.3 TLB 的覆盖范围#

TLB 能覆盖的内存大小 = TLB 项数 × 页大小

配置TLB 项数页大小覆盖范围
L1 DTLB + 4KB 页644 KB256 KB
L2 STLB + 4KB 页15364 KB6 MB
L1 DTLB + 2MB 页642 MB128 MB
L2 STLB + 2MB 页15362 MB3 GB
Note

使用 2MB Huge Pages 后,L2 STLB 的覆盖范围从 6MB 增长到 3GB——500 倍的提升!这就是为什么数据库和大数据应用强烈推荐使用 Huge Pages。

3.4 TLB 未命中的代价#

场景TLB 命中TLB 未命中(L2 命中)TLB 未命中(页表遍历)
延迟1 周期5-10 周期100-200 周期
相对代价1x5-10x100-200x

四、Huge Pages#

4.1 Huge Pages 的原理#

Huge Pages 使用更大的页大小(2MB 或 1GB),减少页表级数和 TLB 压力:

graph TB subgraph 普通4KB页["4KB 页:4 级页表"] P1["PML4"] --> P2["PDPT"] --> P3["PD"] --> P4["PT"] --> PG1["4KB 页"] end subgraph 2MB大页["2MB 大页:3 级页表"] H1["PML4"] --> H2["PDPT"] --> H3["PD"] --> PG2["2MB 页"] end subgraph 1GB大页["1GB 大页:2 级页表"] G1["PML4"] --> G2["PDPT"] --> PG3["1GB 页"] end style 普通4KB页 fill:#ffcdd2,stroke:#c62828 style 2MB大页 fill:#fff9c4,stroke:#f9a825 style 1GB大页 fill:#e8f5e9,stroke:#2e7d32

4.2 Huge Pages 的性能影响#

应用4KB 页性能2MB Huge Pages 性能提升
PostgreSQL基准+10-20%TLB 压力减少
Redis基准+5-15%大内存分配
Java G1 GC基准+5-10%堆内存大
向量搜索基准+15-30%大数组遍历

4.3 Linux 的 Huge Pages 配置#

# 方式 1:透明大页(Transparent Huge Pages, THP)
# 自动将 4KB 页合并为 2MB 大页
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
# 启用 THP
echo always > /sys/kernel/mm/transparent_hugepage/enabled
# 方式 2:hugetlbfs(预留大页)
# 预留 1024 个 2MB 大页 = 2GB
echo 1024 > /proc/sys/vm/nr_hugepages
# 查看大页状态
cat /proc/meminfo | grep Huge
# HugePages_Total: 1024
# HugePages_Free: 512
# Hugepagesize: 2048 kB

4.4 Huge Pages 的注意事项#

Warning

Huge Pages 的潜在问题:

  1. 内存浪费:2MB 页即使只使用 1 字节也占 2MB,内部碎片

  2. 预留困难:hugetlbfs 需要预先预留,系统启动后可能无法获取连续物理内存

  3. THP 的延迟:khugepaged 线程合并页时可能导致延迟尖刺

  4. NUMA 影响:大页的物理内存可能分布在远端 NUMA 节点

五、TLB 刷新#

5.1 什么时候需要刷新 TLB?#

事件刷新范围频率
上下文切换全局刷新(或使用 PCID)每次切换
页表修改单项或全局刷新mmap/munmap
内存保护修改单项刷新mprotect
进程迁移全局刷新NUMA 迁移

5.2 PCID:减少上下文切换的 TLB 刷新#

x86 的 PCID(Process-Context Identifier)允许 TLB 中同时存在多个进程的映射:

# 查看 CPU 是否支持 PCID
cat /proc/cpuinfo | grep pcid
# Linux 4.14+ 默认启用 PCID(如果硬件支持)
# 效果:上下文切换时不再刷新整个 TLB

5.3 TLB 刷新的性能影响#

// 测量 TLB 刷新的影响
#include <stdio.h>
#include <time.h>
#include <sys/mman.h>
#define SIZE (256 * 1024 * 1024) // 256MB
int main() {
char *pages[65536]; // 65536 个 4KB 页
// 分配并访问所有页(填充 TLB)
for (int i = 0; i < 65536; i++) {
pages[i] = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
pages[i][0] = 1; // 触发页表填充
}
// 测量访问时间(TLB 热状态)
clock_t start = clock();
for (int i = 0; i < 65536; i++) {
pages[i][0]++;
}
clock_t end = clock();
printf("TLB 热状态: %.3f\n", (double)(end - start) / CLOCKS_PER_SEC);
// 触发 TLB 刷新(修改页保护)
for (int i = 0; i < 65536; i++) {
mprotect(pages[i], 4096, PROT_READ);
}
// 测量访问时间(TLB 冷状态)
start = clock();
for (int i = 0; i < 65536; i++) {
pages[i][0]++; // TLB 未命中
}
end = clock();
printf("TLB 冷状态: %.3f\n", (double)(end - start) / CLOCKS_PER_SEC);
return 0;
}

六、TLB 与缓存的关系#

6.1 地址翻译在缓存访问之前#

虚拟地址 → TLB 查找 → 物理地址 → 缓存查找 → 数据

TLB 未命中会阻塞后续的缓存访问——即使数据在缓存中,也无法获取,因为不知道物理地址。

6.2 VIPT(Virtual Index Physical Tag)#

L1 缓存使用虚拟索引 + 物理标签的设计,允许 TLB 查找和缓存索引并行进行:

虚拟地址:[索引位][标签位][偏移]
↓ ↓
缓存索引(虚拟) TLB 查找 → 物理标签
↓ ↓
缓存行查找 ←──── 标签比较

这要求 L1 缓存的大小 ≤ 页大小 × 相联度。例如 4KB 页 × 8-way = 32KB——这正是 L1 D-Cache 的典型大小。

七、动手实验#

7.1 实验 1:测量 TLB 大小#

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
printf("步长(页数) 每次访问延迟(ns)\n");
for (int stride_pages = 1; stride_pages <= 4096; stride_pages *= 2) {
int total_pages = stride_pages * 64;
int *pages = malloc(total_pages * sizeof(int));
for (int i = 0; i < total_pages; i++) pages[i] = (i + stride_pages) % total_pages;
clock_t start = clock();
int idx = 0;
for (int i = 0; i < 10000000; i++) idx = pages[idx];
clock_t end = clock();
double ns = (double)(end - start) / CLOCKS_PER_SEC * 1e9 / 10000000;
printf("%8d %.1f\n", stride_pages, ns);
free(pages);
}
return 0;
}
// 延迟在 TLB 容量边界处跳跃

7.2 实验 2:Huge Pages 的效果#

# 使用 perf 测量 TLB 未命中
perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses ./your_program
# 对比 4KB 页和 2MB 大页
# 4KB 页
./your_program # 默认
# 2MB 大页(需要程序支持)
HUGETLB_MORECORE=yes ./your_program

7.3 实验 3:查看 TLB 信息#

# 查看 TLB 配置
lscpu | grep -i tlb
# 查看页大小
getconf PAGESIZE
# 4096
# 查看大页信息
cat /proc/meminfo | grep -i huge
# 查看进程的页表大小
cat /proc/$(pidof your_program)/status | grep VmPTE

八、虚拟地址翻译的完整过程#

8.1 x86-64 虚拟地址分解#

一个 64 位虚拟地址(实际使用 48 位或 57 位)的分解如下:

4 级页表(48 位虚拟地址):
┌────────┬────────┬────────┬────────┬──────────────┐
│ 保留 │ PML4 │ PDPT │ PD │ PT │ 页内偏移 │
│ 16位 │ 9位 │ 9位 │ 9位 │ 9位 │ 12位 │
└────────┴────────┴────────┴────────┴──────────────┘
63-48 47-39 38-30 29-21 20-12 11-0
5 级页表(57 位虚拟地址,LA57):
┌────────┬────────┬────────┬────────┬────────┬──────────────┐
│ 保留 │ PML5 │ PML4 │ PDPT │ PD │ PT │ 偏移 │
│ 7位 │ 9位 │ 9位 │ 9位 │ 9位 │ 9位 │ 12位 │
└────────┴────────┴────────┴────────┴────────┴──────────────┘
63-57 56-48 47-39 38-30 29-21 20-12 11-0

8.2 完整翻译流程示例#

假设虚拟地址为 0x7FF8A3B2C100,4KB 页,4 级页表:

flowchart TD VA["虚拟地址 0x7FF8A3B2C100"] --> EXTRACT["提取各级索引<br/>PML4=0x1FF, PDPT=0x0A3<br/>PD=0x1B2, PT=0x0C1"] EXTRACT --> L1["Step 1: 读 CR3 → PML4 表基址"] L1 --> L2["Step 2: PML4[0x1FF] → PDPT 表基址<br/>内存访问 #1"] L2 --> L3["Step 3: PDPT[0x0A3] → PD 表基址<br/>内存访问 #2"] L3 --> L4["Step 4: PD[0x1B2] → PT 表基址<br/>内存访问 #3"] L4 --> L5["Step 5: PT[0x0C1] → 物理页帧号<br/>内存访问 #4"] L5 --> PA["物理地址 = 页帧号 + 0x100<br/>总共 4 次内存访问!"] style VA fill:#fff9c4,stroke:#f9a825 style PA fill:#e8f5e9,stroke:#2e7d32

8.3 页表项(PTE)的结构#

每个页表项 64 位,包含物理页帧号和权限/属性位:

位域名称含义
0P(Present)页是否存在
1R/W读/写权限
2U/S用户/超级用户
5A(Accessed)是否被访问过
6D(Dirty)是否被写入过
7PS(Page Size)是否为大页
8G(Global)全局页(不随 CR3 刷新)
12-51物理页帧号40 位物理地址
63XD(Execute Disable)禁止执行
Note

A 位(Accessed)和 D 位(Dirty)由硬件自动设置。操作系统利用 A 位进行页面替换决策(类似 LRU),利用 D 位决定换出时是否需要写回磁盘。

九、5 级页表(LA57)#

9.1 为什么需要 5 级页表#

4 级页表支持 48 位虚拟地址 = 256 TB 虚拟地址空间。对于大多数应用足够,但以下场景需要更大空间:

  • 大型数据库(如 Oracle、SAP HANA)需要映射数十 TB 内存
  • 多个虚拟机共享物理内存
  • 内存映射文件(如大型数据集)
  • Intel Optane 持久内存

5 级页表将虚拟地址扩展到 57 位 = 128 PB。

9.2 4 级 vs 5 级页表对比#

维度4 级页表5 级页表
虚拟地址位数4857
虚拟地址空间256 TB128 PB
页表遍历次数4 次5 次
TLB 未命中代价更低更高(多一次内存访问)
硬件支持所有 x86-64 CPUIce Lake+
Linux 支持4.0+4.14+(需启用 CONFIG_X86_5LEVEL)

十、TLB 未命中的处理#

10.1 硬件页表遍历器(Page Table Walker)#

现代 CPU 内置了硬件页表遍历器,TLB 未命中时自动遍历页表:

flowchart TD ACCESS["CPU 访问虚拟地址"] --> TLB_CHECK{"TLB 查找"} TLB_CHECK -->|"命中"| HIT["1 周期返回物理地址"] TLB_CHECK -->|"未命中"| L2_TLB{"L2 STLB 查找"} L2_TLB -->|"命中"| L2_HIT["5-10 周期"] L2_TLB -->|"未命中"| WALKER["硬件页表遍历器"] WALKER --> W1["读取 PML4 表"] W1 --> W2["读取 PDPT 表"] W2 --> W3["读取 PD 表"] W3 --> W4["读取 PT 表"] W4 --> FILL["填充 TLB"] FILL --> DONE["100-200 周期"] style HIT fill:#e8f5e9,stroke:#2e7d32 style DONE fill:#ffcdd2,stroke:#c62828

10.2 TLB 未命中的性能影响量化#

// 测量 TLB 未命中的影响
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define SIZE_MB 256
#define PAGE_SIZE 4096
int main() {
int pages = SIZE_MB * 1024 / PAGE_SIZE;
char *buf = malloc(SIZE_MB * 1024 * 1024);
// 顺序访问(TLB 友好)
clock_t start = clock();
long long sum = 0;
for (int i = 0; i < SIZE_MB * 1024 * 1024; i += PAGE_SIZE) {
sum += buf[i]; // 每页只访问 1 字节
}
clock_t end = clock();
printf("顺序访问(每页1字节): %.3f\n",
(double)(end - start) / CLOCKS_PER_SEC);
// 随机跳页访问(TLB 不友好)
int *order = malloc(pages * sizeof(int));
for (int i = 0; i < pages; i++) order[i] = i;
// shuffle order...
start = clock();
sum = 0;
for (int i = 0; i < pages; i++) {
sum += buf[order[i] * PAGE_SIZE]; // 随机页访问
}
end = clock();
printf("随机页访问: %.3f\n",
(double)(end - start) / CLOCKS_PER_SEC);
free(buf);
free(order);
return 0;
}

10.3 Huge Pages 的配置实战#

# ===== 透明大页(THP)=====
# 查看当前状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# always [madvise] never
# 启用 THP
echo always | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
# 在代码中使用 madvise 请求大页
# madvise(ptr, size, MADV_HUGEPAGE);
# ===== 静态大页(hugetlbfs)=====
# 预留 1024 个 2MB 大页 = 2GB
echo 1024 | sudo tee /proc/sys/vm/nr_hugepages
# 查看大页状态
cat /proc/meminfo | grep Huge
# HugePages_Total: 1024
# HugePages_Free: 1024
# HugePages_Rsvd: 0
# Hugepagesize: 2048 kB
# 挂载 hugetlbfs
sudo mount -t hugetlbfs nodev /mnt/hugepages
# ===== 1GB 大页 =====
# 预留 4 个 1GB 大页(需要启动参数)
# 在 /etc/default/grub 添加:
# default_hugepagesz=1G hugepagesz=1G hugepages=4
sudo update-grub && sudo reboot
# 查看可用大页大小
cat /proc/meminfo | grep Hugepages
大页类型配置方式适用场景缺点
透明大页(THP)echo always / madvise通用应用khugepaged 延迟尖刺
2MB 静态大页nr_hugepages数据库、Java需要预留,内部碎片
1GB 大页启动参数超大内存数据库需要重启,严重碎片
Warning

透明大页(THP)在数据库场景中可能导致严重的延迟问题。Oracle 和 MongoDB 官方文档都建议禁用 THP,改用静态大页。原因是 khugepaged 线程在合并 4KB 页为 2MB 页时需要获取锁,可能导致延迟尖刺。

十一、小结#

上一章理解了SIMD 与向量化计算。

概念要点对软件的影响
4 级页表x86-64 的地址翻译每次翻译 4 次内存访问
TLB页表条目的缓存命中 1 周期,未命中 100+ 周期
TLB 覆盖范围TLB 项数 × 页大小大页显著增加覆盖范围
Huge Pages2MB/1GB 大页TLB 压力减少,数据库/大数据受益
PCID减少上下文切换 TLB 刷新多进程场景性能提升
VIPTL1 缓存与 TLB 并行L1 大小受页大小限制

下一步NUMA 架构——多插槽系统的内存拓扑如何影响性能?跨 NUMA 节点访问的延迟惩罚有多大?

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

TLB 与页表
https://blog.souloss.com/posts/cpu-architecture/tlb-and-page-tables/
作者
Souloss
发布于
2026-03-14
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时