前面 16 篇从地址空间到 OOM,逐层拆解了 Linux 虚拟内存子系统的结构和机制。这些知识的价值不只在于读源码——更在于它提供了一种分层诊断习惯:遇到内存相关的性能问题时,先定位层次,再找对象,再看状态转移,最后用指标验证。

核心问题可以压成一句话:

Linux VM 的价值不只在源码知识,而在一种分层诊断习惯:先定位层次,再找对象,再看状态转移,最后用指标验证。

系列概念地图

整个系列覆盖的层次和对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
用户空间视角                     内核视角
───────────── ──────────
malloc / mmap VMA (vm_area_struct)
↓ 虚拟地址 ↓
page fault 页表 (PGD→P4D→PUD→PMD→PTE)
↓ 物理页分配 ↓
RSS 增长 page / folio 对象

buddy allocator (物理页管理)

SLUB (小对象管理)

运行时维护:
反向映射 (rmap) ← 从物理页找回所有映射它的 PTE
LRU / workingset ← 近似"最近使用",指导回收
kswapd / reclaim ← 内存压力时释放页面
swap ← 匿名页的外部 backing store
NUMA balancing ← 页面迁移到访问者所在节点
THP / huge page ← 增大映射粒度降低 TLB 开销

资源边界:
memcg ← 层级内存预算
OOM killer ← 最终兜底

每一层有自己的对象、状态转移和可观测指标。诊断问题的关键是确定"问题发生在哪一层"。

症状到层次的诊断表

症状 可能的层次 首先检查的指标 对应文章
RSS 持续增长 VMA / page fault /proc/<pid>/smapsmaps 01, 03
延迟尖刺(毫秒级) direct reclaim / compaction vmstatsar -B/proc/vmstat allocstall 09, 12
TLB miss 导致 CPU stall huge page / TLB perf stat dTLB-load-misses 12
进程被 SIGKILL OOM killer dmesgmemory.events 16
容器 OOM 但宿主有空闲 memcg 限制 memory.current vs memory.max 15, 16
NUMA remote access 高 内存位置 numastatnuma_maps 11
slab 内存高 SLUB cache /proc/slabinfoslabtop 14
swap 频繁进出 匿名页压力 vmstat si/so/proc/vmstat pswpin/pswpout 10
page fault 延迟高 分配路径 / buddy 碎片 /proc/buddyinfocompact_stall 03, 13
文件 I/O 第二次变快 page cache 命中 free cached 列、/proc/meminfo Cached 05

这张表不是穷举,但提供了一个起点:从可观察的症状出发,快速定位到最可能的 VM 层次。

诊断方法论

1
symptom -> layer -> object -> transition -> evidence

Step 1: 症状分类

先确定问题的性质:是内存使用量异常(RSS 增长、OOM)还是延迟异常(stall、尖刺)还是吞吐异常(带宽低)。这决定了向哪一层排查。

Step 2: 定位层次

用诊断表或经验判断最可能的层次。一个关键启发:延迟问题多在 reclaim/compaction/NUMA 层;使用量问题多在 VMA/page fault/memcg 层。

Step 3: 找到对象

确定层次后,找到具体的对象。例如:reclaim 层的对象是 LRU 上的页面、buddy 层的对象是 free_area 中的块、memcg 层的对象是 cgroup 预算。

Step 4: 看状态转移

对象在什么条件下发生状态变化?页面从 active → inactive → reclaimed、从 free_area → allocated → freed → buddy merge。状态转移的触发条件和频率是性能的关键。

Step 5: 用指标验证

每一层都有对应的 /proc/sys 指标。用指标验证假设:如果假设是"direct reclaim 导致延迟",就看 allocstall_normal 的增量是否与延迟尖刺时间吻合。

案例一:RSS 增长但 maps 看不出问题

症状:Java 服务的 RSS 在 top 中持续增长到 8GB,但 /proc/<pid>/maps 中各段看起来正常,heap 段也不大。

诊断路径

  1. 层次:RSS 增长 → VMA / page fault 层
  2. 对象:用 smaps_rollup 看 RSS 组成
    1
    2
    3
    4
    cat /proc/<pid>/smaps_rollup
    # Rss: 8388608 kB
    # Anonymous: 1048576 kB ← 匿名页只有 1GB
    # 其他去哪了?
  3. 细分:用 smaps 按段查看
    1
    2
    grep -A5 "rss" /proc/<pid>/smaps | sort -k2 -n -r | head
    # 发现大量 file-backed mapping (mmap'd JAR/SO 文件)
  4. 状态转移:file-backed pages 是 page cache,被 mmap 后计入 RSS 但实际可回收
  5. 验证MemAvailable 仍然充裕,Inactive(file) 增长与 RSS 增长一致

结论:RSS 增长来自 file-backed page cache(JVM 加载的 JAR、SO 文件)。这些页面在内存压力下会被回收,不是内存泄漏。真正需要关注的是 Anonymous 部分。

案例二:延迟尖刺来自 direct reclaim

症状:Web 服务的 P99 延迟偶尔从 5ms 飙到 50ms,无明显流量变化。

诊断路径

  1. 层次:延迟尖刺 → reclaim / compaction 层
  2. 指标采集
    1
    2
    3
    4
    # 监控 allocstall(direct reclaim 次数)
    watch -n1 'grep allocstall /proc/vmstat'
    # 监控 compact_stall(直接 compaction 次数)
    watch -n1 'grep compact_stall /proc/vmstat'
  3. 关联allocstall_normal 增量与尖刺时刻吻合
  4. 根因:zone Normal 的 low watermark 被频繁触及,kswapd 回收速度不够,请求线程进入 direct reclaim 阻塞
  5. 验证kswapd_stealpgsteal_direct 的比例——正常系统 kswapd 负责 >90% 的回收,如果 direct 比例高说明 kswapd 跟不上

解决方向

  • 调高 vm.min_free_kbytes → 提前唤醒 kswapd
  • 减少 page cache 占用(调低 vm.dirty_ratio
  • 如果是 THP compaction 导致:设置 defrag=defer 避免同步 compaction

案例三:容器被 OOM kill 但宿主有空闲内存

症状:Kubernetes pod 被 OOMKilled(exit code 137),但 node 的 MemAvailable 显示 32GB 空闲。

诊断路径

  1. 层次:OOM → memcg 层(不是全局)
  2. 确认 scope
    1
    2
    3
    4
    5
    6
    7
    # 查看 pod 的 memory limit
    kubectl describe pod <name> | grep -A5 Limits
    # memory: 512Mi

    # 查看 cgroup events
    cat /sys/fs/cgroup/.../memory.events
    # oom_kill 1
  3. 分析使用模式
    1
    2
    3
    cat /sys/fs/cgroup/.../memory.stat
    # anon 450M ← 匿名页高
    # file 60M ← page cache
  4. 状态转移:匿名页无法回收(没有 swap),file pages 已经被回收到极低,仍然不够 → memcg OOM
  5. 验证memory.current 触碰 memory.max 时 OOM 发生

解决方向

  • 调高 pod 的 memory limit(如果宿主有余量)
  • 排查应用内存泄漏(anon RSS 持续增长?)
  • 启用 swap(允许匿名页换出而不是直接 OOM)
  • 设置 memory.high 实现 graceful degradation

工具速查

工具 / 文件 用途 对应层次
/proc/<pid>/maps, smaps 进程地址空间布局 VMA
/proc/<pid>/pagemap 虚拟页 → 物理页映射 页表
/proc/vmstat 全局 VM 计数器 全层
/proc/meminfo 全局内存分类统计 全层
/proc/buddyinfo buddy allocator 碎片状态 buddy
/proc/slabinfo slab cache 统计 SLUB
/proc/zoneinfo per-zone 水位线和统计 zone/reclaim
numastat, numa_maps NUMA 分布 NUMA
memory.stat, memory.events per-cgroup 统计 memcg
perf stat 硬件 PMU 计数器(TLB miss 等) TLB/huge page
vmstat, sar -B 系统级 paging 活动 reclaim/swap
dmesg OOM 日志 OOM

后续阅读路线

从本系列出发,几条深入方向:

源码阅读mm/ 目录是入口。按需读——带着具体问题进入源码比通读更有效。每篇的"源码锚点"提供了起点。

性能分析:Brendan Gregg 的 Systems Performance(第二版)覆盖了从 CPU 到内存到 I/O 的性能方法论。本系列提供的是内核视角的底层机制,Gregg 提供的是诊断工作流。

容器内存管理:cgroup v2 文档 + Kubernetes memory management 文档。理解 memcg 之后,Kubernetes 的 requests/limits 语义会变得透明。

NUMA 和大内存系统numactlnumad、AutoNUMA(NUMA balancing)的实际调优。在 128+ 核的机器上,NUMA 策略的影响远大于小系统。

内存安全:KASAN(内核地址消毒剂)、KMSAN(未初始化内存检测)。理解 SLUB 的 redzone 和 poisoning 后,这些工具的原理会更清晰。

模式提炼:分层诊断

1
symptom -> layer -> object -> transition -> evidence

这个诊断框架不限于内存。CPU 问题(哪个核?哪个调度类?哪个 cgroup?)、I/O 问题(哪个设备?哪个 I/O 调度器?哪个文件系统?)、网络问题(哪一层协议?哪个 socket?哪个 netns?)都可以用同样的分层思路。Linux 内核的设计本身就是分层的——诊断方法与系统结构对齐,效率最高。

系列导航

全系列索引

# 标题 核心问题
00 重读 Linux VM:给系统研究生的虚拟内存导读 虚拟内存全貌与阅读路线
01 地址空间不是数组:mm_struct 和 vm_area_struct 进程地址空间的内核表示
02 页表:CPU 能读懂的翻译结构 多级页表的结构与遍历
03 缺页异常:一次访问如何进入内核 page fault 分发与处理
04 匿名页和 COW:fork 为什么便宜,写入为什么变贵 copy-on-write 机制
05 文件映射和 page cache:文件内容怎样变成页面 文件 I/O 与缓存
06 Folio:为什么内核重新组织 page 抽象 page → folio 的演进
07 反向映射:从物理页找回虚拟地址 rmap 机制
08 LRU 和 workingset:内核如何近似"最近使用" 页面老化与 working set
09 页回收:kswapd 和 direct reclaim 内存回收机制
10 Swap:换出去的页怎样回来 swap 生命周期
11 NUMA:内存为什么有远近 非均匀内存访问
12 Huge Page:TLB 压力和页表开销 大页与 TLB 优化
13 Buddy system:物理页如何按阶分配 物理页分配器
14 SLUB:小对象为什么不直接按页分配 内核对象分配器
15 cgroup memory:内存从全局资源变成局部预算 内存资源隔离
16 OOM Killer:内核什么时候决定杀进程 最终兜底机制
17 回到工程:Linux VM 如何改变性能诊断 诊断方法论与案例

参考资料