本页目录
虚拟内存
覆盖: 页表遍历 (4-level/5-level) → PTE 格式 → TLB 机制 → HugeTLB/THP → KPTI 隔离 内核版本: 2.6 ~ 6.x,重点标注 5-level paging 和 mTHP 演进
概述
虚拟内存是 Linux 内存管理的基石。它将每个进程隔离在自己的地址空间中,同时通过页表实现延迟分配 (demand paging)、写时复制 (COW)、内存共享 (mmap MAP_SHARED) 和 swap。
理解虚拟内存的关键在于理解页表遍历——CPU 如何把一个虚拟地址转换成物理地址。这个过程发生在每次内存访问中(由 MMU 硬件完成),速度至关重要。
页表结构
x86-64 4-level paging (48-bit)
flowchart LR
VA["虚拟地址 (48-bit)"]
VA --> PGD["PGD/PML4 索引<br/>[47:39] 9 bit<br/>512 entries"]
PGD -->|"CR3 →"| PUD["PUD 索引<br/>[38:30] 9 bit<br/>512 entries"]
PUD --> PMD["PMD 索引<br/>[29:21] 9 bit<br/>512 entries"]
PMD --> PTE["PTE 索引<br/>[20:12] 9 bit<br/>512 entries"]
PTE --> PAGE["4KB 物理页<br/>offset [11:0] 12 bit"]
classDef va fill:#e3f2fd,stroke:#1565c0
classDef level fill:#f3e5f5,stroke:#7b1fa2
classDef phys fill:#e8f5e9,stroke:#2e7d32
class VA va
class PGD,PUD,PMD,PTE level
class PAGE phys
为什么每级正好 9 bit (512 entries)? 因为每个 PTE 是 8 bytes,512 × 8 = 4KB = 正好一个物理页。这种自相似的设计使得页表本身可以用页面分配器管理。
5-level paging (57-bit, kernel 4.14+)
flowchart LR
VA5["虚拟地址 (57-bit)"]
VA5 --> P5E["PML5 索引<br/>[56:48] 9 bit<br/>512 entries"]
P5E --> P4D["P4D 索引<br/>[47:39] 9 bit<br/>512 entries"]
P4D --> PUD5["PUD 索引<br/>[38:30] 9 bit"]
PUD5 --> PMD5["PMD 索引<br/>[29:21] 9 bit"]
PMD5 --> PTE5["PTE 索引<br/>[20:12] 9 bit"]
PTE5 --> PAGE5["4KB 物理页<br/>offset [11:0]"]
classDef va fill:#e3f2fd,stroke:#1565c0
classDef level fill:#f3e5f5,stroke:#7b1fa2
classDef phys fill:#e8f5e9,stroke:#2e7d32
class VA5 va
class P5E,P4D,PUD5,PMD5,PTE5 level
class PAGE5 phys
需要 CPU 支持 LA57 feature (Ice Lake+)
检查: grep la57 /proc/cpuinfo
5-level 将虚拟地址空间从 256TB (128T user + 128T kernel) 扩展到 128PB。不是为今天的应用准备的——是为未来的 CXL 共享内存池和 PB 级 NVDIMM。
大页如何减少层级
flowchart LR
subgraph HUGE["大页遍历层级"]
direction LR
A["PGD"] --> B["PUD"]
B -->|"2MB: PS=1"| C["PMD → 2MB 大页 ✅"]
B -->|"1GB: PS=1"| D["PUD → 1GB 大页 ✅"]
end
subgraph NORMAL["4KB 页遍历"]
direction LR
E["PGD"] --> F["PUD"] --> G["PMD"] --> H["PTE → 4KB"]
end
classDef huge fill:#e8f5e9,stroke:#2e7d32
classDef normal fill:#e3f2fd,stroke:#1565c0
class HUGE huge
class NORMAL normal
在 PMD/PUD entry 中: bit 7 (PS bit) = 1 → 不再往下走,这一级就是最终物理地址
页表项 (PTE) 格式
x86-64 PTE (简化, 64 bit):
bit 0: Present → 页在物理内存中 (1) 还是被换出/未映射 (0)
bit 1: R/W → 可写 (1) 还是只读 (0) — COW 的关键!
bit 2: U/S → User 可访问 (1) 还是 kernel only (0)
bit 3: PWT → Page Write-Through caching
bit 4: PCD → Page Cache Disable
bit 5: Accessed → 硬件自动设 1 (页面被访问过)
→ 回收的关键: 没访问过的页面是 swap 首选
bit 6: Dirty → 硬件自动设 1 (页面被写过)
→ 脏页 swap 时需写回,干净页直接丢弃
bit 7: PAT / PS → PTE 级: PAT (Page Attribute Table)
→ PMD/PUD 级: PS (Page Size, 指示大页)
bit 8: Global → CR3 切换时不刷 (用于内核页表)
bit 11: NX bit 63 → No-Execute (XD), 不可执行
bit 63: NX → 同 bit 11 的别名
物理地址: bits [51:12] → 指向 4KB 对齐的物理页
其余 bits [11:0] 是 offset,不参与地址转换
Accessed 和 Dirty 位是内存回收的硬件加速器。没有它们,内核需要用 page fault 来模拟"是否被用过",成本高数百倍。
TLB (Translation Lookaside Buffer)
基本概念
TLB = MMU 内部的页表 cache (硬件)
L1 TLB: ~64 entries, 1 cycle 延迟 (指令和数据分开)
L2 TLB: ~1500 entries, ~5 cycles
TLB miss:
1. 硬件遍历页表 (x86: hardware page walker, 4 次内存访问)
2. 如果遍历中间任何一级的 Present=0 → page fault → 内核处理
3. 找到 PTE 后,加载到 TLB
TLB 覆盖率:
4KB pages: L2 TLB 1500 entries × 4KB = 6MB 覆盖
2MB pages: L2 TLB 1500 entries × 2MB = 3GB 覆盖
1GB pages: 32 entries (L1 only) × 1GB = 32GB 覆盖
PCID / ASID
问题: 上下文切换时写 CR3 → TLB 中所有 non-global entries 失效
→ 新进程的每个内存访问都是 TLB miss → 严重性能损失
解决方案:
x86 PCID (Process-Context Identifier, 12-bit):
写 CR3 时附上 ASID → TLB 中不同 ASID 的 entries:
- 不会被刷掉 (保留在 TLB 中)
- 不会误命中 (ASID 不匹配)
→ 热进程切换回来后,TLB 仍然大部分命中
ARM64 ASID (8-bit 或 16-bit):
类似原理,TTBR0_EL1 写入时指定 ASID
硬件区分不同 ASID 的 TLB entries
内核态页表 (Global bit):
kernel 页表的 PTE 设 Global bit → CR3 切换不刷
→ 内核态 TLB entries 在不同进程间共享
TLB Shootdown
HugeTLB
预留大块连续物理内存,提供确定的大页映射:
// 配置
echo 128 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
// 或 boot param: hugepagesz=1G hugepages=4 hugepagesz=2M hugepages=1024
// 使用方式 1: hugetlbfs
mount -t hugetlbfs -o pagesize=2M none /dev/hugepages
;
// 使用方式 2: mmap flag
;
// QEMU/libvirt 使用 HugeTLB 为 VM 映射 guest RAM
// → 减少嵌套页表 (stage-2 page table) 的 TLB miss
HugeTLB 的优点:确定可用(预留时分配)、TLB 效率高。缺点:需要连续物理内存、不可 swap(5.x 之前)、小数据浪费整页。
THP (Transparent Huge Pages)
机制
内核自动将连续的 4KB 页合并为 2MB 大页,对用户透明:
分配:
进程 malloc(128KB) → 触发 page fault → do_anonymous_page()
→ 如果 VM 支持 THP 且对齐 → 尝试分配 2MB 零页
→ 成功: 整个 2MB 用一个大页映射 (减少后续 512 个 page faults)
→ 失败: 回退到 4KB 页
khugepaged:
后台内核线程 → 周期性扫描进程地址空间
→ 发现可以合并的连续 4KB 页 → 迁移/合并 → 升级为 2MB THP
→ 发现 THP 内大部分页被释放 → 分裂为 4KB 页
→ 扫描通过 /sys/kernel/mm/transparent_hugepage/khugepaged/ 控制
策略控制
# always: 总是尝试分配 THP (默认)
# madvise: 仅 MADV_HUGEPAGE 标记的 VMA
# never: 禁用
# 应用级控制:
;
THP 的代价
优点:
+ TLB miss 率大幅降低 (512 倍覆盖率)
+ page fault 减少 (一次 allocation 512 个页)
+ 内核页表遍历减少一级
缺点:
- 分配延迟 (需要找 2MB 连续物理内存 → 可能需要 compact/reclaim)
- 内部碎片 (3KB 有效数据占 2MB 物理内存)
- 某些 workload 变慢 (如某些数据库自己管理大页)
这就是为什么 DBA 经常建议: echo never > transparent_hugepage/enabled
mTHP (multi-size THP, 6.12+)
正在演进的方案: 允许 THP 的大小不是固定 2MB
→ 4KB / 16KB / 32KB / 64KB / 128KB / 256KB / 512KB / 1MB / 2MB
→ 按需选择合适的大小
→ 减少内部碎片 (不需要整个 2MB)
→ 保留 TLB 效率 (比 4KB 好很多)
这是内核社区正在活跃开发的领域
KPTI: 页表隔离
Meltdown 与修复
Meltdown (2018):
CPU 在推测执行中不检查 U/S bit → 用户态可以推测读取内核内存
→ cache 中的残留数据可以通过 side channel 读取
KPTI (Kernel Page Table Isolation):
x86: PTI (Page Table Isolation)
ARM: KPTI (Kernel Page Table Isolation)
每个进程有**两套**页表:
内核态页表: 完整映射 (user + kernel 地址)
用户态页表: 最小映射 (user + 仅 trampoline)
trampoline (入口蹦床):
只包含 entry_SYSCALL_64 / iret / 中断描述符表 (IDT)
→ 足够进入内核态 → 进入后立即切换到内核态页表
→ syscall 返回前切回用户态页表 → 然后 iret
性能影响
syscall 密集型负载: ~5%~30% 性能退化
每次 syscall: 2 次 CR3 写 + 2 次 TLB miss (trampoline 太小)
缓解:
- PCID: 避免完全刷 TLB (但 CR3 写本身开销还在)
- 非 Meltdown 影响的 CPU (AMD / newer Intel): KPTI 自动禁用
调试接口
# 完整页表遍历 (需要 root)
# bit 63: page present
# bits [54:0]: PFN (物理页号)
# 检查 THP 使用情况
# AnonHugePages: THP 用于匿名内存
# ShmemHugePages: THP 用于 shmem
# 检查是否启用 5-level paging
# 大页信息
|
# 页表大小 (per-process)
|
参考与延伸
- 内核文档:
Documentation/x86/x86_64/mm.rst,Documentation/vm/transhuge.rst,Documentation/x86/pti.rst - LWN:
- "5-level paging" (lwn.net/Articles/717293/)
- "Transparent huge pages in 6.x" (lwn.net/Articles/971579/)
- "The multi-size THP patch set" (lwn.net/Articles/931701/)
- 源码文件:
arch/x86/include/asm/pgtable.h— PTE 格式定义arch/x86/mm/tlb.c— TLB flush 实现mm/huge_memory.c— THP 核心mm/hugetlb.c— HugeTLBarch/x86/mm/pti.c— KPTI
关键词: 页表, PGD/PUD/PMD/PTE, PTE flags (A/D/RW/NX), TLB, PCID, ASID, HugeTLB, THP, khugepaged, mTHP, KPTI, Meltdown