在现代AI和深度学习系统中,内存管理是性能的基石。当操作系统(OS)面临内存压力时,它并非立刻启动OOM(Out of Memory)杀手,而是经历一系列递进式的回收策略。本文将深度解析Linux从优雅到爆烈的七层内存回收防线,帮助你理解内核的设计智慧,并掌握运维调优的关键。

https://blog.csdn.net/Hehuyi_In/article/details/161494113?spm=1001.2014.3001.5501

1. 七层防线概览:从快速路径到系统崩溃

Linux的内存管理遵循递进式防线,从最轻量的快速路径到最极端的Kernel Panic。运维的核心目标是让系统永远不要走到最后三层。

  • 快速路径(Fast Path):空闲链表充足,直接分配,延迟~100ns。
  • 异步回收(kswapd):后台悄悄回收,用户无感。
  • 直接回收(Direct Reclaim):申请进程亲自回收,性能开始下降。
  • 内存规整(Compaction):移动页面以消除碎片,换取连续空间。
  • THP拆分:拆分透明大页为小页,最后的挣扎。
  • OOM Killer:杀死占用最多的进程,释放内存。
  • Kernel Panic:系统完全崩溃,等待重启。

2. 预备知识:内存水位线(Watermark)

Linux通过三条水位线判断内存状态:

  • min:由 min_free_kbytes 控制,可通过 sysctl vm.min_free_kbytes 查看/设置。
  • low:≈ 1.25 × min,由 watermark_scale_factor 微调。
  • high:≈ 1.5 × min,用于 kswapd 休眠恢复线。

空闲低于 low 唤醒 kswapd,低于 min 触发 Direct Reclaim。设置 min_free_kbytes 过大导致频繁回收,过小则可能直接触发 OOM。

3. 快速路径与异步回收:系统日常运行的守护者

快速路径:当空闲内存高于 low水位线 时,内核从 Buddy System 直接摘取页面,时间复杂度 O(1),延迟 ~100ns。这是系统最理想的状态。

异步回收(kswapd):当内存降至 low 以下,内核唤醒 kswapd 守护线程。它调用 balance_pgdat()shrink_node()shrink_lruvec() 扫描 LRU 链表,回收文件页和匿名页,直到内存恢复到 high 水位。用户通常无感,但若 kswapd 长期 100% CPU,说明内存压力持续。

# 查看 kswapd 活动

top -H -p $(pgrep -f kswapd)

# 监控扫描与回收

grep -E "pgscan|pgsteal" /proc/vmstat

4. 直接回收与内存规整:性能下降的起点

Direct Reclaim:当空闲跌破 min 水位,申请进程调用 __alloc_pages_direct_reclaim() 亲自回收。进程进入 D 状态,延迟可达毫秒到秒级。这是性能急剧下降的起点。加大 vm.min_free_kbytes 可减少 Direct Reclaim,但会牺牲可用内存。

内存规整:内核通过移动页面(如匿名页、文件缓存)来消除碎片。异步规整(kcompactd)不阻塞进程,而直接规整(Direct Compaction)会使进程进入 D 状态。数据库中大量 mlock 的共享内存可能导致规整无效,建议关闭透明大页。

5. THP拆分与OOM Killer:最后的挣扎与终极手段

THP拆分:当所有回收和规整都失败时,内核调用 split_huge_page() 将 2MB 透明大页拆为 512 个 4KB 小页。这并不释放内存,只是临时解困。频繁触发说明碎片化严重,应关闭 THP 或增加物理内存。

OOM Killer:遵循“拿得多死得快”原则,通过 /proc/[pid]/oom_score_adj 可调整进程优先级。内核计算 Badness Score 并杀死得分最高的进程。系统日志会打印详细报告。

相关命令

# 查看拆分次数

grep thp_split /proc/vmstat

# 若持续发生,评估是否关闭 THP

echo never > /sys/kernel/mm/transparent_hugepage/enabled

相关参数

# 查看 OOM 历史

dmesg | grep -i "out of memory"

# 保护关键进程(永不杀)

echo -1000 > /proc/[PID]/oom_score_adj

可以配置  避免 OOM 时直接崩溃。

[AFFILIATE_SLOT_1]

6. Kernel Panic与设计哲学:为什么内核不拒绝分配?

Kernel Panic:当 OOM 后仍无法满足分配,或内核数据结构严重异常时,调用 panic() 函数,系统完全挂死。若配置了 kernel.panic=30 ,30 秒后自动重启。

灵魂拷问

  • 为什么内核不拒绝分配? 内核无法安全地拒绝,因为拒绝的代价比杀进程更大。
  • 为什么 min_free_kbytes 用绝对值? 为小内存和大内存系统提供可预测的底线,后续引入 watermark_scale_factor 作为比例补充。
  • 为什么 Direct Reclaim 让申请方亲自回收? 避免死锁和优先级反转,保证分配成功。

建议配置  自动重启和  收集崩溃转储,以便事后分析根因。

# panic 后自动重启

sysctl kernel.panic=30

# 永久生效

echo "kernel.panic=30" >> /etc/sysctl.conf

① 时序错位:malloc 的承诺与物理页的延迟

用户程序调用  申请的是虚拟地址空间,不是物理内存。只有当 CPU 首次写入这块虚拟地址时,才会触发缺页中断,内核此时才调用  分配物理页。

这意味着:从  返回成功到真正分配物理内存之间,存在巨大的时间窗口。当缺页发生时,内核已经无法向调用者返回 NULL 了——因为调用者根本没有在等待一个返回值,它只是在执行一条普通的  指令。内核如果此时选择“拒绝”,唯一能做的事情是向当前进程发送 SIGSEGV(段错误),这会导致进程非预期崩溃,而且完全没有上下文来优雅处理。


② 用户程序不会检查 NULL

即便内核想办法让  返回 NULL(比如关闭 overcommit,设置 ),绝大多数应用程序也不会检查  的返回值。从几十年的软件工程实践来看,程序员普遍假设小内存分配不会失败,或者检查失败后只会调用 。

与其让进程带着未初始化的指针继续运行,最终在某处产生更难排查的崩溃、数据损坏或安全漏洞,不如让内核主动选择一个进程,干净利落地杀掉。OOM Killer 的日志至少能告诉你谁被杀、为什么,而随机的段错误则几乎没有可追溯性。


③ 乐观分配与兜底惩罚的设计哲学

Linux 内核采用的是“乐观分配 + 兜底惩罚”策略,背后是两种效率权衡:

  • 乐观分配(overcommit):允许进程申请远超物理内存容量的虚拟内存。这在现实中是合理的——大部分进程申请的堆内存并不会完全使用,fork 出的子进程大多立即 exec。乐观分配让系统可以运行更多进程、更充分地利用内存,避免了宝贵的物理内存闲置。

  • 兜底惩罚(OOM Killer):当乐观分配玩脱了,物理内存真的耗尽时,内核不会默默“降级”为返回 NULL,而是主动介入,选择一个最占内存的进程终止。这是一种“牺牲个体,保存整体”的降级策略,比起让整个系统随机的进程崩溃,杀伤范围更可控。

       更进一步,如果 OOM Killer 也无法挽回(比如内核自身内存泄漏,或所有用户进程都是关键进程无法杀死),内核选择 Panic 挂死,遵循的是“数据一致性优先于可用性”的原则:宁可停机,也不能让一个已经内存紊乱的系统继续写入磁盘、产生不可逆的数据损坏。

① 直接原因:历史惯性与行为可预测性

Linux 内核的  从最初实现时就采用了绝对值(KB 为单位),原因很朴素:

  • 早期内存很小:2.4/2.6 早期时代,服务器内存通常是 512MB~4GB。绝对值(如 16MB、32MB)直观且够用,不需要比例。

  • 绝对值行为稳定:如果改成比例(如“总内存的 1%”),那么同一个系统在热插拔内存或内存容量变化时, 会随之改变,可能导致不可预期的行为波动。绝对值一旦设定,管理员心里有数,不会因为内存变化而“悄悄”改变系统的回收行为。

  • 紧急预留的性质决定: 的本质是“为极端紧急情况预留的最后一道物理内存防线”。这道防线的宽度取决于系统需要处理的最坏情况——例如同时多个进程触发 Direct Reclaim 时,需要多少原子操作内存——而不是总内存的百分比。


② 绝对值的问题:大内存机器反而预留不足

绝对值的设计在小内存时代没问题,但在大内存时代暴露了缺陷。

内核的默认计算公式大致为:(实际更复杂,与 Zone 数量和内存位宽有关)。这意味着:

内存越大, 占比反而越小。 这是根函数(sqrt)的特性导致的。

对于 256GB 甚至 1TB 的大内存机器,几百 MB 的紧急预留是远远不够的。一旦内存碎片化或突发大量分配,这点预留很快被击穿,Direct Reclaim 和 OOM 频繁发生。这就是为什么很多运维会在高内存服务器上手动调大  到数 GB。


③ 内核的补救:watermark_scale_factor

内核开发者早就意识到绝对值不够灵活,因此在 4.6 版本(2016 年)引入了  参数(默认值 10,范围 10~1000,对应 0.1%~10% 的比例因子)。它在  的绝对值基础上,按比例拉宽 low 和 high 水位线之间的距离。公式如下:

其中  是 Zone 的总内存。也就是说, 越大,三条线之间的距离越远:

  • kswapd 唤醒更早(low 离 min 更远),有更多时间在后台回收;

  • 但可用内存也会略微减少。

最终,内核选择了“绝对值基准 + 比例因子微调”的混合方案: 作为绝对底线, 作为灵敏度调节旋钮。

① kswapd 不可靠:异步回收无法提供确定性保证

 是一个后台内核线程,它的行为是尽力而为的:

  • 回收速度不确定:kswapd 的扫描和回收速度受限于 CPU 调度、磁盘 I/O 和 LRU 链表的长度。当系统突发大量内存申请时(例如多个进程同时启动或批量数据加载),kswapd 的回收速度可能远远跟不上分配需求。

  • 没有强保证:kswapd 的目标是让空闲内存恢复到 ,但它并不承诺某一次具体的分配一定成功。如果把所有希望都寄托在 kswapd 上,就相当于将“是否能分配成功”变成了一件随机事件。

因此,内核不能假设 kswapd 总能准时交付内存。必须有一个同步的兜底机制来保证分配最终能够成功。


② 死锁风险:回收路径本身也需要内存

这是最关键的技术原因。内存回收过程(无论是 kswapd 还是 Direct Reclaim)在执行时,内核自身可能需要分配一些临时内存,如果所有内存分配都依赖 kswapd 来回收,而 kswapd 在回收过程中又需要分配内存,那么一旦 kswapd 卡在这些临时分配上,就形成了“等自己回收”的死锁。

Linux 的解法是:让申请者进程在 Direct Reclaim 路径上亲自执行回收。这样,当前进程可以一边回收、一边满足自己的临时内存需求。即使回收过程需要分配一些内存,这些分配也可以由当前进程自己完成(继续递归回收),从而打破死锁循环。


③ 避免全局拥塞和优先级反转

如果所有内存申请都在  上排队,会导致:

  • 全局同步瓶颈:所有分配者都必须等待同一个(或少数几个)后台线程,系统吞吐量会急剧下降,尤其是在多核系统上。

  • 优先级反转:一个低优先级的进程可能因为少量内存需求而阻塞高优先级进程,因为都在等待 kswapd 回收。而 Direct Reclaim 让高优先级进程可以直接执行回收并立即获得内存,避免了无关的低优先级进程影响关键任务。

  • 时间不可控:进程无法预测 kswapd 何时完成工作,对于需要低延迟响应的应用(如数据库、交易系统)来说,这种不确定性是不可接受的。

Direct Reclaim 的本质是“谁要谁回收”,相当于将回收工作分布式地交给各个申请者并行完成,既能充分利用多核,又能让急需内存的进程得到更快的响应。

[AFFILIATE_SLOT_2]

总结

Linux 内存回收的七层防线体现了内核设计者的智慧:从快速路径到 Kernel Panic,每一步都经过深思熟虑。运维人员应通过监控水位线、调整 min_free_kbytesswappiness,避免系统进入 Direct Reclaim 及之后阶段。理解这些机制,才能在 AI、深度学习等内存密集型场景中,确保系统稳定高效运行。

步骤机制触发条件阻塞延迟关键监控指标
① Fast PathBuddy System空闲充足~100ns/proc/buddyinfo
② kswapd异步 LRU 回收空闲 < lowms~spgscan_kswapd
③ Direct Reclaim同步直接回收空闲 < minms~sallocstall
④ Compaction内存规整需连续大块可能ms~百mscompact_stall
⑤ THP拆分拆分大页多次重试失败msthp_split_page
⑥ OOM Killer杀进程所有回收失败s级oom_kill 计数
⑦ Panic系统崩溃OOM后仍失败-kdump
cat /proc/buddyinfo
Node 0, zone      DMA      1      1      1      1      1      1      1      1      1      2      2
Node 0, zone    DMA32   5528   3979   3210   1625   1111    678    225    132     29     12      1
Node 0, zone   Normal  10388   5540   2822   2098   1509    681    356    172     78     28     10
如何观察
grep allocstall /proc/vmstat   # 查看 Direct Reclaim 停滞次数
sar -B 1 10                    # 观察直接回收扫描(pgscand)
相关命令
echo 1 > /proc/sys/vm/compact_memory   # 手动触发规整
grep compact /proc/vmstat              # 查看成功率
vm.panic_on_oom=0kernel.panickdumpmalloc()alloc_pages()mallocmovmallocvm.overcommit_memory=2mallocabort()min_free_kbytesmin_free_kbytesmin_free_kbytesmin_free_kbytes ≈ sqrt(总内存) × 16
总内存默认 min_free_kbytes占总内存比例
4 GB~88 MB~2.2%
16 GB~176 MB~1.1%
64 GB~352 MB~0.55%
256 GB~704 MB~0.28%
1 TB~1408 MB~0.14%
min_free_kbytesmin_free_kbyteswatermark_scale_factormin_free_kbytes
low  = min + (max - min) × watermark_scale_factor / 10000
high = min + (max - min) × watermark_scale_factor × 2 / 10000
maxwatermark_scale_factormin_free_kbyteswatermark_scale_factorkswapdwatermark_highkswapd