Linux 内存子系统:OOM、Swap 与 cgroup 内存限制

Linux 内存管理子系统是内核中最复杂的模块之一。它不仅要为进程分配物理页帧,还要在内存不足时做出艰难的权衡决策。本文从 overcommit 策略、OOM Killer、交换机制、cgroup 控制、内存水位以及诊断工具六个维度,系统地梳理 Linux 内存子系统的核心原理。

Linux 内存管理子系统是内核中最复杂的模块之一。它不仅要为进程分配物理页帧,还要在内存不足时做出艰难的权衡决策。本文从 overcommit 策略、OOM Killer、交换机制、cgroup 控制、内存水位以及诊断工具六个维度,系统地梳理 Linux 内存子系统的核心原理。


1. overcommit_memory 策略

Linux 默认允许进程申请远超物理内存总量的地址空间。这种**内存过量分配(overcommit)**基于一个观察事实:大多数进程会 malloc 大块内存,但实际只使用其中一小部分(稀疏使用)。如果内核为每次分配都预留物理页,大量内存将被闲置。

内核通过 vm.overcommit_memory 参数控制过量分配策略:

模式行为
0Heuristic(默认)内核启发式判断:合理的小分配允许过量,明显不合理的拒绝
1Always永远允许分配,直到真正缺页时触发 OOM
2Strict严格限制,总提交量不得超过 “CommitLimit”

模式 2 计算公式为:

CommitLimit = (总物理内存 × overcommit_ratio / 100) + Swap总量

可通过 vm.overcommit_ratio(默认 50)或 vm.overcommit_kbytes 精确调整。数据库、金融交易等对承诺一致性要求高的场景适合模式 2,避免运行时因 OOM 导致业务中断。但大多数通用服务器建议保留默认的启发式模式,在利用率与稳定性之间取得平衡。


2. OOM Killer

当物理内存和交换空间全部耗尽,后续内存分配无法得到满足时,内核会触发 OOM Killer(Out-of-Memory Killer),它必须选择杀死一个或多个进程来释放内存。

badness 启发式评分

内核使用 badness() 启发式算法为每个进程打分,核心考量因素:

  • 内存占用量:RSS 和页表越大,得分越高,越容易被选中
  • 运行时:运行时间短的进程得分更高(长运行服务被视为更"重要")
  • root 权限:通过 total_vm / 2 的微调降低 root 进程被选中概率
  • oom_score_adj:管理员可手动调节进程的评分权重

查看与调控 OOM

每个进程的 /proc/[pid]/oom_score 完整展示了其被杀优先级(数值越大越危险)。而 oom_score_adj 允许从 -1000 到 1000 调整:

# 保护关键进程免于 OOM 杀死
echo -1000 > /proc/$(pidof sshd)/oom_score_adj

oom_score_adj = -1000 表示该进程完全免疫 OOM Killer。systemd 服务可通过 OOMScoreAdjust=-1000 自动配置。

OOM 日志与调试

OOM 触发时内核 Ring Buffer 会输出详细日志,包含被选中进程、内存用量、以及各进程的 oom_score。配合 dmesg -T 可快速定位受害者。现代 cgroup v2 进一步引入了 cgroup-aware OOM:当 cgroup 达到内存上限时,优先在该 cgroup 内部选择受害者,而不是全系统范围杀死,这对容器化环境至关重要。


3. Swap 与 Zswap

Swap 的本质

Swap 机制将不活跃的匿名页(anonymous pages)换出到磁盘,为活跃内存腾出空间。它不是内存不足的"补丁",而是虚拟内存体系的核心组成部分,实现了物理内存的超量使用

Swap 分区 vs Swap 文件:分区性能略优(无文件系统开销),但灵活性差;swap 文件易于调整大小,现代内核性能差距已很小。云环境中常用 swap 文件适应动态规格。

vm.swappiness

vm.swappiness(0-200,现代内核默认值通常为 60)控制内核回收匿名页(swap)与文件页(drop cache)的相对倾向:

  • 值低(如 10):尽量保留匿名页,优先回收文件缓存
  • 值高(如 100+):积极换出匿名页,适合数据库等文件缓存价值低的场景

主流误解是"swap 导致系统变慢所以应禁用"。实际上完全无 swap 时,系统更容易触发 OOM,且 Linux 文件缓存无法有效释放。正确的做法是保留适量 swap,并根据业务特征调整 swappiness。

Zswap: 压缩型交换缓存

当内存压力过大、内核准备将页写入慢速磁盘时,zswap 会先尝试用 CPU 压缩页并存入 RAM 中的压缩池。如果后续进程访问该页,可直接从压缩池中解压,避免了磁盘 I/O。开销仅为 CPU 压缩/解压,往往远低于磁盘延迟。

Zram: 内存压缩块设备

zram 在内存中创建压缩块设备作为 swap 目标,完全不涉及物理磁盘 I/O。嵌入式设备(Android 手机)广泛使用 zram 以极小的 CPU 代价换取更多可用内存。


4. cgroups v2 内存控制

容器化和 systemd 资源管理依赖 cgroup 对进程组的内存施加精确约束。

核心控制文件

文件作用
memory.max硬限制,进程组总内存(含页缓存)不得超过此值,超限触发 OOM
memory.high软限制,超限时内核先通过回收和 throttle 施压,避免直接 OOM
memory.low保护量,当系统整体内存不足时,优先保护该 cgroup 不回收其内存
memory.min硬保护,在 memory.min 范围内的内存绝不被回收,即使系统整体严重缺页
memory.swap.max该 cgroup 可使用的swap上限

memory.high 是生产环境中最实用的参数:它允许工作负载短暂突破常规使用,但通过延迟和回收速率克制野蛮增长,给运维争取反应时间。

memory.stat 解析

memory.stat 提供了细粒度的内存构成:

  • anon:匿名映射(堆、栈、malloc),换出的主要候选人
  • file:文件页缓存,可回收
  • kernel_stack:内核栈用量
  • pagetables:页表结构本身占用的内存(大量小对象进程可能惊人)
  • sock:网络协议栈缓冲区

实践场景

Docker --memory=1g 映射为 cgroup 的 memory.max=1G 和自动计算的 memory.swap.max=1G。Kubernetes 的 memory.limit 同样基于 memory.max。systemd 单元可通过 MemoryMax=500MMemoryHigh=400M 进行原生资源管控。


5. 内存水位线与回收机制

每个 NUMA zone(DMA、Normal、Movable 等)维护三条水位线:

水位含义
High充裕状态,kswapd 休眠
Low低于此,kswapd 后台异步唤醒,进行轻量回收
Min紧急阈值,低于此的分配将触发直接回收(direct reclaim),阻塞当前进程

kswapd vs 直接回收

  • kswapd:后台内核线程,低优先级异步回收旧页缓存和冷匿名页,对业务延迟几乎无感
  • Direct Reclaim:分配路径阻塞式回收,进程执行暂停等待,可能产生数百毫秒甚至秒级延迟

为什么"有空闲内存"仍然感到压力大?

/proc/zoneinfo reveal 了每个 zone 的水位线。若应用程序的大量分配导致水位跌至 lowmin,即使 free 命令显示还有几百 MB,系统已处于压力状态,后台回收甚至直接回收正在运行。Free 值不包括可回收的文件页缓存;可用内存应关注 MemAvailable

内存规整(compaction)

长时间运行后,内存可能出现严重外部碎片——空闲总量足够,但无法满足大页或连续分配需求。Watermarks 也驱动 compaction:当分配高阶页失败且水位允许时,内核迁移页帧来创建更大的连续内存块。


6. 实际诊断工具

快速概览

  • free -h:查看总量、已用、buff/cacheavailable
  • vmstat 1:每秒输出 swap in/out(si/so)、free、buff、cache、us/sy/id/wa
  • sar -r 1:历史友好的内存利用率统计

/proc/meminfo 关键字段

字段含义
MemTotal / MemFree物理总量与空闲
MemAvailable真正可用于新分配的内存(含可回收缓存)
Buffers / Cached块设备缓存和文件页缓存
SwapCached同时存在于 swap 和内存中的页
Active / Inactive活跃与非活跃 LRU 列表,Inactive 是回收首选
Dirty / Writeback已修改未写盘 / 正在写盘回写的页
Slab / SReclaimable内核对象缓存及其可回收部分

Slab 诊断

slabtop 实时展示内核 slab 分配:dentry、inode、kmalloc-xx 等。内核对象泄漏或某些驱动创建过多对象时,slab 会持续增长,即便进程 RSS 正常。

进程级内存剖析

/proc/[pid]/smaps(以及精简版 smaps_rollup)列出了进程的每段 VMA(Virtual Memory Area)大小、RSS、PSS(按比例共享)、脏页量。它是定位内存泄漏和大块分配的黄金数据。

PSI(Pressure Stall Information)

/proc/pressure 提供了精确到微秒的内存压力时间占比:

$ cat /proc/pressure/memory
some avg10=1.24 avg60=0.85 avg300=0.42 total=123456789
full avg10=0.56 avg60=0.32 avg300=0.12 total=987654321
  • some:至少一个任务因内存等待而停滞的时间占比
  • full:所有任务同时因内存等待而停滞的时间占比(反映 direct reclaim 导致的全面阻塞)

systemd 可根据 PSI 阈值自动触发 OOM 或资源调整,是现代 Linux 监控内存压力的首选指标。


总结

Linux 内存子系统是一个多层次的平衡系统。overcommit 机制提高了资源利用率,但也埋下了 OOM 的隐患;OOM Killer 是最后防线,cgroup 使其更加可控;Swap 和 Zswap 以磁盘或压缩换取时间;cgroup v2 为容器提供了精细的 QoS 分级;内存水位线驱动后台与前台回收的双轨策略;而 PSI、meminfo、smaps 等工具则为诊断提供了数据支撑。

理解这些机制之间的相互作用,才能在真正出现内存问题时,迅速判断是配置失当、程序泄漏、还是 cgroup 限制过紧,并作出有针对性的调整。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. 进程与线程:从 PCB 到内核调度实体
  2. 虚拟内存与分页机制:从 MMU 到 TLB
  3. 系统性能诊断与调优:strace、perf、bpftrace