Linux 内存管理子系统是内核中最复杂的模块之一。它不仅要为进程分配物理页帧,还要在内存不足时做出艰难的权衡决策。本文从 overcommit 策略、OOM Killer、交换机制、cgroup 控制、内存水位以及诊断工具六个维度,系统地梳理 Linux 内存子系统的核心原理。
1. overcommit_memory 策略
Linux 默认允许进程申请远超物理内存总量的地址空间。这种**内存过量分配(overcommit)**基于一个观察事实:大多数进程会 malloc 大块内存,但实际只使用其中一小部分(稀疏使用)。如果内核为每次分配都预留物理页,大量内存将被闲置。
内核通过 vm.overcommit_memory 参数控制过量分配策略:
| 值 | 模式 | 行为 |
|---|---|---|
| 0 | Heuristic(默认) | 内核启发式判断:合理的小分配允许过量,明显不合理的拒绝 |
| 1 | Always | 永远允许分配,直到真正缺页时触发 OOM |
| 2 | Strict | 严格限制,总提交量不得超过 “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=500M、MemoryHigh=400M 进行原生资源管控。
5. 内存水位线与回收机制
每个 NUMA zone(DMA、Normal、Movable 等)维护三条水位线:
| 水位 | 含义 |
|---|---|
| High | 充裕状态,kswapd 休眠 |
| Low | 低于此,kswapd 后台异步唤醒,进行轻量回收 |
| Min | 紧急阈值,低于此的分配将触发直接回收(direct reclaim),阻塞当前进程 |
kswapd vs 直接回收
- kswapd:后台内核线程,低优先级异步回收旧页缓存和冷匿名页,对业务延迟几乎无感
- Direct Reclaim:分配路径阻塞式回收,进程执行暂停等待,可能产生数百毫秒甚至秒级延迟
为什么"有空闲内存"仍然感到压力大?
/proc/zoneinfo reveal 了每个 zone 的水位线。若应用程序的大量分配导致水位跌至 low 或 min,即使 free 命令显示还有几百 MB,系统已处于压力状态,后台回收甚至直接回收正在运行。Free 值不包括可回收的文件页缓存;可用内存应关注 MemAvailable。
内存规整(compaction)
长时间运行后,内存可能出现严重外部碎片——空闲总量足够,但无法满足大页或连续分配需求。Watermarks 也驱动 compaction:当分配高阶页失败且水位允许时,内核迁移页帧来创建更大的连续内存块。
6. 实际诊断工具
快速概览
free -h:查看总量、已用、buff/cache、availablevmstat 1:每秒输出 swap in/out(si/so)、free、buff、cache、us/sy/id/wasar -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 限制过紧,并作出有针对性的调整。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。