4. ARMv8缓存与存储层次
MMU 这玩意儿,说白了就是 CPU 访问内存的“翻译官”。ARMv8 的 MMU 比之前复杂了不少,尤其是引入了 4 级页表之后。我刚开始接触时也觉得头大,但摸清它的脾气后,你会发现性能调优的很多关键点都藏在这里。
4.1 页表遍历:从虚拟地址到物理地址的“寻宝之旅”
ARMv8 支持 4KB、16KB 和 64KB 的页面大小。最常用的是 4KB 页面,配合 4 级页表(L0 到 L3)。每次 CPU 访问内存,MMU 都要做一次“寻宝”——遍历页表。
遍历过程大致是这样:
- L0 级(PGD):用 VA[47:39] 索引,找到 L1 页表的基地址。
- L1 级(PUD):用 VA[38:30] 索引,找到 L2 页表的基地址。
- L2 级(PMD):用 VA[29:21] 索引,找到 L3 页表的基地址。
- L3 级(PTE):用 VA[20:12] 索引,找到最终的物理页框号。
每次遍历都要访问内存,这开销可不小。所以 TLB 就派上用场了。
关键点:页表遍历的深度直接影响内存访问延迟。4 级页表意味着一次内存访问最多需要 4 次额外的内存读取。如果 TLB 命中,这 4 次就省了。
我在项目中遇到过一个问题:某个数据库应用的内存访问延迟突然飙升。排查后发现,是因为页表项被换出,导致每次访问都要触发缺页中断。嗯,这其实是个经典问题——页表本身也可能被换出。
4.2 TLB 结构:CPU 的“地址缓存”
TLB 就是页表遍历结果的缓存。ARMv8 的 TLB 通常分为 L1 TLB 和 L2 TLB。L1 TLB 又分为指令 TLB(ITLB)和数据 TLB(DTLB)。
| TLB 级别 | 类型 | 典型容量 | 延迟 |
|---|---|---|---|
| L1 ITLB | 指令 | 32-64 条 | 1 个周期 |
| L1 DTLB | 数据 | 32-64 条 | 1 个周期 |
| L2 TLB | 统一 | 512-2048 条 | 几个周期 |
TLB 的条目数有限,所以刷新策略就很重要了。
4.3 TLB 刷新策略:什么时候清空缓存?
TLB 刷新是个“重操作”,能少做就少做。ARMv8 提供了几种刷新指令:
- TLBI VMALLE1:刷新所有 TLB 条目。最粗暴,但开销最大。
- TLBI VAE1:刷新指定虚拟地址的 TLB 条目。更精细。
- TLBI ASIDE1:刷新指定 ASID 的 TLB 条目。适合进程切换。
我个人习惯在进程切换时只刷新当前 ASID 的 TLB,而不是全部刷新。这样可以保留其他进程的 TLB 条目,减少性能损失。
小技巧:在上下文切换时,使用 TLBI ASIDE1 而不是 TLBI VMALLE1,可以显著降低 TLB 缺失率。我在一个多进程的 Web 服务器上做过测试,性能提升了约 15%。
你可能会问:为什么不用更精细的刷新?因为有些场景下,你根本不知道哪些虚拟地址被修改了。比如,当页表被修改时,你只能选择全部刷新。
4.4 大页与透明大页:减少页表遍历的“捷径”
大页(Huge Pages)就是使用更大的页面(如 2MB 或 1GB),减少页表级数。ARMv8 支持两种大页:
- 2MB 大页:使用 L2 页表项直接映射,跳过 L3 级。
- 1GB 大页:使用 L1 页表项直接映射,跳过 L2 和 L3 级。
大页的好处很明显:
- 减少 TLB 条目占用:一个 2MB 大页只需要 1 个 TLB 条目,而 4KB 页面需要 512 个。
- 减少页表遍历深度:从 4 级降到 2 级或 1 级。
- 减少 TLB 缺失率:因为 TLB 能覆盖更大的内存区域。
但大页也有缺点:
- 内存浪费:如果应用只使用部分大页,剩余部分就浪费了。
- 分配困难:大页需要连续物理内存,在内存碎片化时可能分配失败。
避坑指南:我曾经在一个内存密集型应用上使用 1GB 大页,结果系统启动后内存分配失败,因为物理内存碎片化严重。后来我改用 2MB 大页,问题就解决了。所以,大页不是越大越好,要根据实际内存情况选择。
4.5 透明大页:自动化的“大页管理”
透明大页(Transparent Huge Pages, THP)是 Linux 内核的一个特性,它自动将 4KB 页面合并为 2MB 大页,无需应用修改代码。
THP 的工作原理:
- khugepaged 内核线程:定期扫描内存,寻找可以合并的连续 4KB 页面。
- 合并条件:512 个连续的 4KB 页面,且访问模式一致。
- 合并过程:修改页表,将 512 个 PTE 替换为 1 个 PMD 条目。
THP 的配置:
- /sys/kernel/mm/transparent_hugepage/enabled:可选值
always、madvise、never。 - /sys/kernel/mm/transparent_hugepage/defrag:控制是否进行内存碎片整理。
我建议在数据库、HPC 等内存密集型应用上开启 THP。但要注意,THP 的合并过程本身有开销,而且可能导致内存碎片化。
经验之谈:在一个 Redis 集群上,我开启了 THP 后,延迟抖动明显增加。排查发现,THP 的合并操作导致了短暂的停顿。最后我改用 madvise 模式,只对特定内存区域启用 THP,问题就解决了。
4.6 知识结构图:MMU 核心逻辑
下面这张图展示了 MMU 的核心工作流程,从虚拟地址到物理地址的完整路径:
这张图清晰地展示了:TLB 命中时直接得到物理地址,未命中时才触发页表遍历。大页可以跳过部分页表级数,减少遍历深度。
4.7 实战建议:MMU 调优 checklist
最后,我总结了一份 MMU 调优的 checklist,供你参考:
- 检查 TLB 缺失率:使用
perf stat -e dTLB-load-misses,iTLB-load-misses监控。 - 启用大页:对内存密集型应用,优先使用 2MB 大页。
- 谨慎使用 THP:在延迟敏感的应用上,使用
madvise模式。 - 优化页表布局:尽量让页表项在物理上连续,减少 TLB 缺失。
- 监控内存碎片化:使用
/proc/buddyinfo检查大页分配是否成功。
嗯,MMU 这块内容就讲到这里。记住,TLB 是性能的关键,大页是优化的利器。下次遇到内存访问延迟问题,先看看 TLB 缺失率,再考虑大页配置。