2. Linux 内存管理基础:虚拟地址空间、物理内存、MMU 与 TLB 的工作原理
在深入页表遍历性能瓶颈之前,必须彻底理解 Linux 内存管理的核心抽象层。现代操作系统通过 虚拟内存 机制,为每个进程提供独立的、连续的地址空间,而底层物理内存则被分页管理。MMU(内存管理单元)与 TLB(转译后备缓冲器)是连接虚拟地址与物理地址的关键硬件组件,它们的效率直接决定了内存访问的性能。
2.1 虚拟地址空间与物理内存
每个用户态进程都拥有独立的虚拟地址空间。在 64 位 x86 架构(x86_64)上,Linux 将虚拟地址空间划分为用户空间与内核空间两部分,典型的划分比例为 47 位用户空间 + 17 位内核空间(实际使用 48 位地址)。
2.1.1 虚拟地址空间布局
以下表格展示了 x86_64 架构下 Linux 进程的典型虚拟地址空间布局(从低地址到高地址):
| 区域 | 起始地址(典型) | 大小 | 说明 |
|---|---|---|---|
| 代码段(.text) | 0x400000 | 可变 | 可执行指令,只读 |
| 数据段(.data/.bss) | 0x600000 | 可变 | 全局变量、静态变量 |
| 堆(Heap) | 0x601000 起 | 向上增长 | 动态内存分配(malloc) |
| 内存映射区(mmap) | 0x7f0000000000 | 可变 | 共享库、文件映射、匿名映射 |
| 栈(Stack) | 0x7ffc00000000 | 向下增长 | 局部变量、函数调用帧 |
| 内核空间 | 0xffff800000000000 | 128 TB | 内核代码、模块、直接映射区 |
2.1.2 物理内存与页帧
物理内存被划分为固定大小的块,称为 页帧(Page Frame)。Linux 默认页大小为 4 KB。内核维护一个 struct page 数组来描述每个物理页帧的状态(空闲、使用中、脏页等)。
物理内存的分配与回收由 伙伴系统(Buddy System) 管理,它按 2 的幂次阶(order)组织空闲页块,以减少外部碎片。
// 内核中分配物理页面的核心函数
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
// order = 0 表示分配 1 页 (4KB)
// order = 1 表示分配 2 页 (8KB)
// order = 9 表示分配 512 页 (2MB)
2.2 MMU 与地址转换
MMU(Memory Management Unit)是 CPU 内部负责地址转换的硬件单元。当 CPU 执行加载或存储指令时,发出的虚拟地址被 MMU 截获,MMU 通过查询 页表(Page Table) 将其转换为物理地址。
2.2.1 四级页表结构(x86_64)
x86_64 架构使用四级页表层次结构:PGD(Page Global Directory)、P4D(Page 4th Directory)、PUD(Page Upper Directory)、PMD(Page Middle Directory)、PTE(Page Table Entry)。每个层级包含 512 个表项,每个表项 8 字节。
虚拟地址被划分为多个位段,用于索引不同层级的页表:
虚拟地址 (48位) 分解:
| 63 ... 48 | 47 ... 39 | 38 ... 30 | 29 ... 21 | 20 ... 12 | 11 ... 0 |
| 符号扩展 | PGD索引 | PUD索引 | PMD索引 | PTE索引 | 页内偏移 |
| (16位) | (9位) | (9位) | (9位) | (9位) | (12位) |
地址转换过程如下:
- 从控制寄存器
cr3获取顶级页表(PGD)的物理基地址。 - 使用虚拟地址的 PGD 索引 在 PGD 表中查找,得到下一级页表(P4D)的物理地址。
- 使用 P4D 索引 在 P4D 表中查找,得到 PUD 表的物理地址。
- 使用 PUD 索引 在 PUD 表中查找,得到 PMD 表的物理地址。
- 使用 PMD 索引 在 PMD 表中查找,得到 PTE 表的物理地址。
- 使用 PTE 索引 在 PTE 表中查找,得到最终的物理页帧号(PFN)。
- 将 PFN 与虚拟地址的 页内偏移 组合,形成完整的物理地址。
2.2.2 页表遍历流程 SVG 图
2.3 TLB:转译后备缓冲器
TLB(Translation Lookaside Buffer)是 MMU 内部的一个硬件缓存,用于存储最近使用的虚拟地址到物理地址的映射关系。它的目的是避免每次内存访问都进行完整的页表遍历。
2.3.1 TLB 的结构与操作
TLB 通常分为 指令 TLB(ITLB) 和 数据 TLB(DTLB),部分架构还包含 统一的二级 TLB(L2 TLB)。TLB 表项(TLB Entry)包含虚拟页号(VPN)、物理页帧号(PFN)、权限位、脏位、访问位等。
TLB 的查找过程:
- CPU 发出虚拟地址。
- MMU 并行查询 TLB(全相联或组相联)。
- TLB 命中: 直接返回物理地址,延迟极低(约 1-2 个 CPU 周期)。
- TLB 未命中: 触发硬件页表遍历(Page Walk),将结果写入 TLB,然后重试指令。
2.3.2 TLB 刷新与上下文切换
当进程切换(context switch)时,由于不同进程使用不同的虚拟地址空间,TLB 中的旧映射必须失效。Linux 通过以下方式处理:
- 全局页(Global Pages): 内核空间页表项通常标记为全局,在进程切换时不清除,减少 TLB 刷新开销。
- 进程上下文标识符(PCID): 现代 CPU 支持 PCID,允许 TLB 同时缓存多个进程的映射,避免完全刷新。
- 显式刷新指令: 如 x86 的
INVLPG(刷新单个页)和MOV CR3(刷新全部非全局 TLB)。
// 内核中 TLB 刷新相关函数
void flush_tlb_all(void); // 刷新所有 TLB(SMP 环境下广播)
void flush_tlb_mm(struct mm_struct *mm); // 刷新指定进程的所有 TLB
void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr); // 刷新单个页
2.3.3 TLB 与页表遍历的交互 SVG 图
2.4 大页(Huge Pages)与页表遍历优化
大页是减少页表遍历开销和 TLB 压力的重要手段。Linux 支持多种大页机制:
| 特性 | 标准 4KB 页 | 透明大页(THP) | 显式大页(Hugetlbfs) |
|---|---|---|---|
| 页大小 | 4 KB | 2 MB(默认) | 2 MB / 1 GB |
| 页表层级 | 4 级(PGD→PUD→PMD→PTE) | 3 级(跳过 PTE) | 2 级(跳过 PMD 和 PTE) |
| TLB 覆盖范围 | 小(64 个 TLB 项覆盖 256 KB) | 中(64 个 TLB 项覆盖 128 MB) | 大(64 个 TLB 项覆盖 128 MB / 64 GB) |
| 分配方式 | 自动 | 内核自动合并/分裂 | 应用程序显式申请(mmap) |
| 适用场景 | 通用 | 内存密集型应用(数据库、JVM) | 性能敏感型应用(DPDK、虚拟化) |
2.5 本章小结
- 虚拟地址空间 为进程提供隔离和连续性,物理内存 被分页管理,页帧是基本分配单位。
- MMU 通过四级页表结构将虚拟地址转换为物理地址,每次转换需要 4 次内存访问。
- TLB 是 MMU 内部的硬件缓存,用于加速地址转换。TLB 未命中是页表遍历性能瓶颈的直接体现。
- 大页 通过减少页表层级和扩大 TLB 覆盖范围,是优化页表遍历效率的核心手段。
- 理解这些基础原理,是后续使用
perf工具定位 TLB 未命中、页表遍历开销以及进行系统调优的前提。