- 前言
1.png
- 声明: 本文所有分析均基于 Linux v6.1 LTS 稳定版 内核原生源码,所引用的代码片段、机制解释及结论仅供学习参考与技术交流。内核版本迭代、架构差异或特定发行版的内核定制改造可能导致行为有所偏差。鉴于内核源码的复杂性与笔者水平有限,文中如有错误、疏漏或表述不当之处,欢迎通过评论或邮箱指正探讨,以期共同完善。请以您所用内核版本的官方源码为准。
- 本文全部代码基于Linux v6.1 LTS稳定版,为生产环境内核基线,适配物理机、云主机、容器全场景。规避原版仓库反爬校验,统一采用内核官方静态源码镜像溯源
- 从内核源码结构体维度,计分数据源完全取自task_struct、mm_struct核心内核结构体
- 精准绑定进程内核级内存记账数据,不受用户态内存统计工具干扰
- 这也是用户态监控内存数据与内核OOM判定不一致的核心根源
- 基于Linux内核稳定版mm/oom_kill.c源码,纯底层机制拆解、零运维操作、零入门科普
- Linux v6.1 oom_kill.c 永久静态镜像
- OOM Killer 内核底层本质
2.png
- Linux OOM Killer并非大众认知的"系统内存耗尽后的兜底杀进程逻辑"
- 而是内核内存压力博弈下的强制资源收敛机制,属于虚拟内存子系统的核心容错模块
- 耦合伙伴系统、Slab内存管理器、进程调度结构体与内存记账体系
- 其核心设计本质并非"回收内存",而是在系统处于内存硬死锁、页分配彻底失败的临界状态下
- 通过确定性计分算法筛选最优牺牲进程,打破内存分配阻塞僵局,保障内核基础调度链路不崩塌
- 绝大多数线上业务OOM异常、核心服务无辜被杀、内存充足却触发OOM等疑难问题
- 均源于开发者对内核计分源码约束、内存权重配比规则、隐性内存透支记账机制的认知盲区
- 而非简单的业务内存溢出
- 聚焦内核底层逻辑与线上底层疑难根因
- OOM 核心计分内核算法源码级深度拆解
3.png
- OOM Killer所有筛选逻辑的核心载体为oom_badness()内核函数,是进程OOM分数判定的唯一源码入口
- 所有进程权重、内存占用、特权属性的计算逻辑均封装于此
- 最终输出以页帧计数的绝对分数,分数越高越优先被杀死(与 oom_score_adj 归一化缩放共同决定最终分数)
- 核心计分源码完整逻辑链路
- 内核计分不采用单一内存占用维度
- 而是物理内存(RSS)+交换内存(SWAP)+页表内存(pgtables)+adj权重修正的多维加权模型(注:v6.1 已移除早期版本的 CAP_SYS_ADMIN 特权衰减逻辑)
- 核心源码计算逻辑如下,完全贴合内核原生实现:
- 基础得分 = 进程RSS物理内存占用 + 交换分区内存占用 + 进程页表占用内存(以页帧计) 最终OOM分数 = 基础得分 + 经totalpages归一化后的oom_score_adj权重修正值
- 内核原生源码片段(v6.1 稳定版|oom_badness完整实现):
- 精准对应上述多维加权计分模型,为OOM打分唯一核心实现,无任何二次封装
// linux/mm/oom_kill.c (v6.1 stable | oom_badness完整实现)
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
long points;
long adj;
// 过滤内核线程与全局init进程
if (oom_unkillable_task(p))
return LONG_MIN;
p = find_lock_task_mm(p);
if (!p)
return LONG_MIN;
// oom_score_adj == -1000 直接豁免(先查 adj 可省去后续 points 计算)
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN ||
test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
in_vfork(p)) {
task_unlock(p);
return LONG_MIN; // ← 豁免检查失败,直接退出,不计算 points
}
// 核心计分:RSS物理内存 + Swap内存 + 页表内存三维度加权
// 注:执行到此说明豁免检查通过,adj 变量后续将复用为归一化系数
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
task_unlock(p);
// adj归一化缩放:adj *= totalpages / 1000
adj *= totalpages / 1000;
points += adj;
return points;
}
- 代码对应核心机制:
- 1. 三点计分:RSS + SWAPENTS + pgtables_bytes 三维度,物理内存权重最高
2. adj归一化缩放:adj *= totalpages / 1000; points += adj 内嵌于本函数,无独立子函数,解释「大机器adj放大、小容器adj失效」的故障根因
3. 前置过滤:oom_unkillable_task() 过滤内核线程与init进程,adj == OOM_SCORE_ADJ_MIN 直接豁免
4. 注意:v6.1 版本 oom_badness() 已简化为 (p, totalpages) 双参数签名,且 不含 CAP_SYS_ADMIN 特权衰减(该衰减在 v6.1 已移除)
- 关键内核权重约束机制
- 普通技术认知仅知晓oom_score_adj可调权重,却完全不懂内核权重归一化底层约束
- 内核并不会直接叠加adj原始值,而是会基于系统总内存完成归一化计算:
- adj *= totalpages / 1000
- 该底层机制决定了:大内存服务器环境下,相同adj数值的修正效果会被放大
- 小内存容器环境下权重修正会被稀释,这是容器场景核心服务微调adj仍被误杀的核心底层原因
- adj权重归一化核心实现(v6.1 稳定版):
- 该逻辑并非独立函数,而是内嵌于oom_badness()函数的尾部(详见上节代码),位于计分计算与task_unlock()之后
// linux/mm/oom_kill.c (v6.1 stable | oom_badness() 尾部内联)
adj = (long)p->signal->oom_score_adj; // 读取 adj
// ... 前置过滤与三维度计分后 ...
adj *= totalpages / 1000; // totalpages 全局归一化缩放
points += adj;
return points;
- 关键差异说明:
- v6.1 中 不存在 名为 oom_score_adj_update() 的独立函数,adj归一化是 oom_badness() 的内联逻辑;
v6.1 已移除 CAP_SYS_ADMIN专属的 points /= 4 特权衰减机制(该机制存在于更早内核版本,v6.1不再适用);
totalpages 来源:全局场景来自 totalram_pages() + total_swap_pages(constrained_alloc() 中计算),cgroup场景来自 mem_cgroup_get_max()。
- 内核原生补充证据——constrained_alloc()中totalpages的实际计算逻辑(v6.1 稳定版):
- totalpages在constrained_alloc()函数中按约束类型动态赋值,该值直接传递给oom_badness()决定adj归一化的缩放幅度:
// linux/mm/oom_kill.c (v6.1 stable | constrained_alloc totalpages 赋值逻辑)
static enum oom_constraint constrained_alloc(struct oom_control *oc)
{
// MEMCG 场景:totalpages = cgroup 内存上限
if (is_memcg_oom(oc)) {
oc->totalpages = mem_cgroup_get_max(oc->memcg) ?: 1;
return CONSTRAINT_MEMCG;
}
// 全局场景:totalpages = 全部物理内存 + 全部 swap
oc->totalpages = totalram_pages() + total_swap_pages;
// MEMORY_POLICY 场景:totalpages = 指定 NUMA 节点的物理 + swap
// ...
// CPUSET 场景:totalpages = cpuset 绑定的节点物理 + swap
// ...
}
- 源码机制溯源:
- totalpages在不同约束场景取值不同,直接导致adj归一化效果的差异化
- 物理机totalpages极大导致adj修正被显著放大,cgroup容器totalpages较小导致adj修正被稀释
- 这是"在物理机上微调 adj 有效、在容器中微调无效"的底层源码根因
- 该逻辑属于内核硬编码约束,无任何配置开关可关闭
- 进程虚实内存权重配比底层源码约束
4.png
- 物理内存(RSS)与虚拟内存权重博弈机制
- 内核OOM计分的核心权重倾斜规则:
- 物理内存占用权重远高于虚拟内存,交换分区内存占用具备惩罚性加权
- 源码中通过get_mm_rss统计进程实际物理驻留内存,通过get_mm_counter统计MM_SWAPENTS交换内存
- 二者直接计入基础得分,无任何衰减
- 而进程虚拟内存总占用仅作为辅助判定维度,不参与核心计分
- 内核源码佐证:虚实内存计分权重差异化约束(v6.1 稳定版)
// include/linux/mm.h (v6.1 stable | get_mm_rss 实际定义位置)
// 注意:该函数定义在 mm.h 头文件中,所有包含该头文件的源文件均可使用
static inline unsigned long get_mm_rss(struct mm_struct *mm)
{
return get_mm_counter(mm, MM_FILEPAGES) +
get_mm_counter(mm, MM_ANONPAGES);
}
// include/linux/mm_inline.h (v6.1 stable | MM_SWAPENTS 枚举)
enum mm_counter_item {
MM_FILEPAGES, // 0
MM_ANONPAGES, // 1
MM_SWAPENTS, // 2
MM_SHMEMPAGES, // 3
NR_MM_COUNTER_ITEMS
};
- OOM计分核心仅统计文件页+匿名页物理内存、swap交换内存、页表内存三维度
- 虚拟内存total_vm不纳入计分,这是「虚存透支、物理落地瞬间OOM」的核心代码根源
- 内核原生补充证据——dump_task()输出中的total_vm仅用于诊断打印(v6.1 稳定版):
- dump_task()函数用于OOM触发时打印各进程内存状态(dmesg 中可见的 Tasks state 信息),它确实输出了total_vm
- 但该值仅用于诊断展示,从未被oom_badness()引用为计分因子:
// linux/mm/oom_kill.c (v6.1 stable | dump_task 输出诊断信息)
static int dump_task(struct task_struct *p, void *arg)
{
// ... 前置过滤逻辑 ...
task = find_lock_task_mm(p);
// ...
// 注意:total_vm 在此处仅打印,不参与 oom_badness 计分
pr_info("[%7d] %5d %5d %8lu %8lu %8ld %8lu %5hd %s\n",
task->pid, from_kuid(&init_user_ns, task_uid(task)),
task->tgid, task->mm->total_vm, get_mm_rss(task->mm),
mm_pgtables_bytes(task->mm),
get_mm_counter(task->mm, MM_SWAPENTS),
task->signal->oom_score_adj, task->comm);
task_unlock(task);
return 0;
}
- 源码维度证据链:
- dump_task明确输出total_vm用于诊断,但回看上节核心计分源码完整逻辑链路oom_badness()计分公式
- 仅包含get_mm_rss(物理内存)+ MM_SWAPENTS(swap)+ mm_pgtables_bytes(页表)
- total_vm被内核刻意排除在计分之外
- 这是开发者在dmesg中看到某进程total_vm极大但OOM却杀了另一个进程的底层源码根因
- 该底层规则直接导致典型线上疑难场景:
- 大量业务进程仅申请虚拟内存、未落地物理内存,看似内存占用极低,但突发批量物理内存落地时,会瞬间抢占系统内存,触发OOM
而长期占用大量物理内存的沉睡进程,会持续拉高自身OOM分数,成为优先被杀对象,完全违背业务直观认知
- 页表内存的隐性计分权重盲区
- 绝大多数开发者完全忽略mm_pgtables_bytes页表内存占用的计分贡献
- 内核源码会将进程页表占用的内存换算为页帧数量,全额计入基础OOM得分
- 对于海量连接、多线程业务进程,页表内存会随线程数、文件描述符数线性膨胀
- 即便业务数据内存占用极低,页表内存也会导致进程OOM分数飙升,引发无辜被杀
- 内核源码片段:页表内存计分核算逻辑(v6.1 稳定版)
// linux/mm/memory.c (v6.1 stable | mm_pgtables_bytes 定义)
size_t mm_pgtables_bytes(const struct mm_struct *mm)
{
return mm->pgtable_bytes;
}
// oom_kill.c (oom_badness 内部) 页表内存全额纳入计分核心逻辑
points += mm_pgtables_bytes(p->mm) / PAGE_SIZE;
- 页表内存无豁免、无衰减全额计分,精准解释「轻量高并发多线程业务内存低占用却无辜OOM」的隐蔽故障
- 多线程进程会创建独立页表映射,pgtable_bytes持续累加
- 且内核无页表内存豁免规则,是高并发轻量业务进程无辜OOM的核心代码根源
- Slab 内存占用对 OOM 判定的底层干扰机制(内核级隐蔽漏洞)
5.png
- Slab作为内核缓存内存管理核心模块,其内存占用不归属任何用户进程OOM统计
- 但会占用系统全局可用内存,直接压缩系统内存水位
- 触发全局OOM判定,这是线上最隐蔽的OOM诱因
- Slab内存的内核记账隔离机制
- 内核Slab缓存(dentry、inode、buffer_head等)属于内核态全局内存,不计入task_struct进程内存统计
- 用户态监控工具无法感知其对OOM的影响
- 当业务频繁创建销毁文件、套接字、目录节点时,Slab缓存会持续膨胀
- 且Slab回收存在内核延迟机制,不会随业务进程退出即时释放
- 内核源码片段:OOM评估过滤内核线程机制(v6.1 稳定版)
// linux/mm/oom_kill.c (v6.1 stable | oom_evaluate_task 前段过滤逻辑)
static int oom_evaluate_task(struct task_struct *task, void *arg)
{
struct oom_control *oc = arg;
long points;
// oom_unkillable_task 过滤 init 进程和 PF_KTHREAD 内核线程
if (oom_unkillable_task(task))
goto next;
// cpuset 亲和性过滤
if (!is_memcg_oom(oc) && !oom_cpuset_eligible(task, oc))
goto next;
// 已标记为 OOM victim 且有 MMF_OOM_SKIP 的跳过
if (!is_sysrq_oom(oc) && tsk_is_oom_victim(task)) {
if (test_bit(MMF_OOM_SKIP, &task->signal->oom_mm->flags))
goto next;
goto abort;
}
// oom_badness 打分筛选(异常点为 LONG_MAX 时由 oom_task_origin 提前选择)
points = oom_badness(task, oc->totalpages);
if (points == LONG_MIN || points < oc->chosen_points)
goto next;
select: // 更新最优候选
if (oc->chosen)
put_task_struct(oc->chosen);
get_task_struct(task);
oc->chosen = task;
oc->chosen_points = points;
return 0;
next:
return 0;
abort:
if (oc->chosen)
put_task_struct(oc->chosen);
oc->chosen = (void *)-1UL;
return 1;
}
- 注意:
- struct kmem_cache 结构体定义于 include/linux/slab_def.h,字段远多于 total_pages/free_pages,
博客此处仅为示意Slab内存与用户进程内存分离的概念,不代表实际内核结构体定义。
OOM评估链路通过 oom_unkillable_task() 过滤内核线程(PF_KTHREAD),
但Slab内存消耗的是系统可用物理内存,其本身不归属任何用户进程RSS统计。
- 源码核心结论:
- oom_unkillable_task() 天然过滤内核线程与init进程,OOM计分仅针对用户态进程打分,不直接清算内核Slab内存。
Slab内存挤占系统资源后,OOM机制只能杀死用户进程来回收内存,这是"系统内存充足、业务无辜OOM"的底层代码壁垒
- 内核原生补充证据——Slab检测函数should_dump_unreclaim_slab()(v6.1 稳定版):
- 内核自身内置了专门检测"不可回收 Slab 是否超过全部用户 LRU 页面"的函数
- 该函数在dump_header()中被调用,是OOM发生时内核主动打印Slab超量信息的源码依据
// linux/mm/oom_kill.c (v6.1 stable | should_dump_unreclaim_slab 完整实现)
/*
* Check whether unreclaimable slab amount is greater than
* all user memory(LRU pages).
* dump_unreclaimable_slab() could help in the case that
* oom due to too much unreclaimable slab used by kernel.
*/
static bool should_dump_unreclaim_slab(void)
{
unsigned long nr_lru;
nr_lru = global_node_page_state(NR_ACTIVE_ANON) +
global_node_page_state(NR_INACTIVE_ANON) +
global_node_page_state(NR_ACTIVE_FILE) +
global_node_page_state(NR_INACTIVE_FILE) +
global_node_page_state(NR_ISOLATED_ANON) +
global_node_page_state(NR_ISOLATED_FILE) +
global_node_page_state(NR_UNEVICTABLE);
return (global_node_page_state_pages(NR_SLAB_UNRECLAIMABLE_B) > nr_lru);
}
// dump_header 中调用 should_dump_unreclaim_slab
if (should_dump_unreclaim_slab())
dump_unreclaimable_slab();
- 源码机制溯源:
- 该函数的存在本身就证明了内核社区已知"不可回收 Slab 超量"是OOM的重要诱因
- 当不可回收Slab占比超过所有用户态LRU页面总量时,OOM日志会主动dump slab信息
- 这是"内存充足却 OOM——Slab隐性挤占"场景的源码级直接证据,也是线上故障排查时dmesg中出现Slab信息的内核底层原因
- Slab 干扰OOM判定的核心场景
- 系统整体内存剩余充足、业务进程内存占用极低,但Slab缓存占用过半系统内存
- 导致系统可分配空闲内存枯竭,内核触发OOM流程
- 此时OOM计分模型仅针对用户进程打分,不会清算内核Slab内存
- 最终随机杀死正常业务进程,形成典型的"内存充足却OOM、核心服务无辜被杀"疑难问题
- 其本质是内核用户态/内核态内存记账隔离的底层约束
- 内核内存透支机制源码解析(隐蔽型OOM根源)
6.png
- Linux内核采用乐观内存分配机制,这是所有内存透支问题的底层根源
- 内核允许进程申请的虚拟内存总量远超系统物理内存上限,无实时内存总量校验
- 仅在页表落地、物理内存写入时触发内存校验,形成内存透支
- 内存透支的内核实现逻辑
- 伙伴系统仅维护物理内存页帧的分配状态,虚拟内存的mmap申请、brk扩容均为逻辑记账操作,无物理内存占用
- 内核为提升内存利用率,默认允许超量虚拟内存分配
- 当多个进程同时触发虚拟内存落地、批量抢占物理内存时
- 系统物理内存瞬间耗尽,形成突发式内存透支,直接触发OOM Killer强制执行
- 内核源码相关机制:乐观内存分配与内存透支底层机制(v6.1 稳定版)
// linux/mm/mmap.c (v6.1 stable | do_mmap 入口示意 - 省略内部复杂校验)
// do_mmap 仅校验文件映射/匿名映射的地址区间合法性,
// 不会校验当前系统剩余物理内存是否足够分配,支撑内核乐观分配模型
// linux/mm/oom_kill.c (v6.1 stable | out_of_memory 入口)
bool out_of_memory(struct oom_control *oc)
{
// 检查 oom_killer_disabled
// 触发 blocking_notifier 通知链(用户态可注册回调)
// 检查 current 是否即将释放内存
// 通过 constrained_alloc 确定约束类型
// 通过 select_bad_process 选择目标
// 通过 oom_kill_process 执行杀进程
}
- 核心机制:
- do_mmap 虚存分配无物理内存校验,仅做地址空间合法性检查,支撑内核乐观分配模型;
out_of_memory 入口触发条件:物理页分配(伙伴系统 __alloc_pages)彻底耗尽即调用该函数,是流量高峰瞬时内存透支击穿、突发OOM的底层源码依据
- 源码机制溯源:虚拟内存分配无全局阈值校验,物理内存按需落地分配
- 是Linux内核内存透支、突发OOM故障的原生设计特性,非bug
- 线上高频隐蔽内存透支场景
- 1. 容器场景内存限额透支:
容器cgroup内存限额仅约束物理内存,不限制虚拟内存,容器内进程批量预分配虚拟内存,累计透支远超容器限额,突发物理内存落地触发OOM;
2. 异步批量内存落地:
业务预热阶段批量申请虚拟内存,长期无读写操作,内存处于透支状态,流量高峰瞬时批量写入,物理内存瞬间击穿水位;
3. 子进程内存继承透支:
fork子进程继承父进程全部虚拟内存空间,无实时内存拷贝,多进程叠加后虚拟内存透支量指数级增长,触发内核内存压力阈值
- 内核原生补充证据——cgroup OOM全杀机制oom_kill_memcg_member()(v6.1 稳定版):
- 当容器cgroup触发OOM时,内核不仅仅杀死单个候选进程
- 而是会扫描整个cgroup内所有非豁免进程,分别执行__oom_kill_process:
// linux/mm/oom_kill.c (v6.1 stable | oom_kill_memcg_member 完整实现)
/*
* Kill provided task unless it's secured by setting
* oom_score_adj to OOM_SCORE_ADJ_MIN.
*/
static int oom_kill_memcg_member(struct task_struct *task, void *message)
{
if (task->signal->oom_score_adj != OOM_SCORE_ADJ_MIN &&
!is_global_init(task)) {
get_task_struct(task);
__oom_kill_process(task, message);
}
return 0;
}
- 源码维度证据链:容器场景触发OOM时,oom_kill_process()通过mem_cgroup_scan_tasks(oom_group, oom_kill_memcg_member, message) 遍历整个cgroup
- 所有未设置oom_score_adj=-1000的成员进程都会被逐一__oom_kill_process
- 这意味着:容器内单个进程触发OOM,可能导致整个容器内所有服务进程被连坐杀死,这是"容器中核心服务无故全部挂掉"的物理根因
- 业务无辜被杀完整根因溯源(全底层链路复盘)
7.png
- 结合上述所有内核底层机制,可精准复盘99%线上业务无辜OOM被杀的核心根因,完全区别于表层业务内存溢出结论:
- 1. adj权重归一化适配失效:大内存服务器未适配内核adj归一化机制,微调的低权重配置被系统内存总量稀释,核心服务无法获得有效豁免;
2. 虚实内存权重认知偏差:核心业务长期占用稳定物理内存,OOM分数持续偏高,而突发透支内存的非核心进程虚拟内存不计分,最终核心进程被优先杀死;
3.Slab内核内存隐性挤占:内核缓存内存耗尽系统空闲资源,用户进程无内存可分配,OOM机制只能清算用户态正常进程;
4. 内存透支突发击穿机制:长期虚拟内存透支累积,流量高峰瞬时物理内存落地,系统无缓冲时间,直接触发强制OOM杀进程逻辑
- 高可用自愈模型与生产环境调优实战(落地方案)
8.png
- 摒弃常规重启、扩容、简单调adj的表层方案,基于内核底层机制
- 构建纯内核约束级的高可用OOM自愈规避模型,从根源杜绝无辜OOM与突发OOM故障
- 精准权重隔离模型(基于adj内核归一化适配)
- 突破常规固定adj配置误区,基于系统总内存、容器内存限额做动态adj权重适配,抵消内核归一化稀释效应
- 核心服务配置极限豁免权重(oom_score_adj = -1000),从计分模型底层直接跳过OOM计分筛选,降低被杀优先级
- Slab 内存可控回收内核机制
- 针对内核Slab隐蔽挤占问题,构建定时精准回收模型
- 针对dentry、inode等高频膨胀缓存做定向回收,规避全局Slab内存溢出
- 同时监控内核态内存水位,将Slab内存占用纳入系统内存压力判定指标,提前触发内存节流,避免被动OOM
- 内存透支前置拦截模型
- 打破内核乐观分配机制的弊端,基于cgroup内存记账、进程虚拟内存透支量做前置监控
- 实时统计进程虚拟内存与物理内存差值,识别隐性透支进程
- 在内存透支量逼近系统阈值时,触发前置内存回收、流量限流,杜绝瞬时物理内存击穿引发的突发OOM
- 内核源码片段:OOM通知机制与豁免判定(v6.1 稳定版)
// linux/mm/oom_kill.c (v6.1 stable | oom_notify_list 定义位置)
static BLOCKING_NOTIFIER_HEAD(oom_notify_list);
int register_oom_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_register(&oom_notify_list, nb);
}
EXPORT_SYMBOL_GPL(register_oom_notifier);
int unregister_oom_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_unregister(&oom_notify_list, nb);
}
EXPORT_SYMBOL_GPL(unregister_oom_notifier);
// include/linux/oom.h (v6.1 stable | OOM_SCORE_ADJ_MIN 宏定义)
#define OOM_SCORE_ADJ_MIN (-1000)
// oom_kill.c (oom_badness 内部) 豁免判定为内联逻辑,无独立函数的 oom_skip_task
static bool oom_unkillable_task(struct task_struct *p)
{
if (is_global_init(p))
return true;
if (p->flags & PF_KTHREAD)
return true;
return false;
}
- 以上为兜底前的主动拦截,而一旦拦截失效、OOM不可避免时,需要利用内核原生通知机制与豁免判定接口实现最后一层防御
- 具体而言,基于内核OOM通知机制,可构建用户态与内核态联动自愈链路
- 生产环境调优实战建议
- 内核参数调优:在 /etc/sysctl.conf 中配置 vm.overcommit_ratio=50、vm.swappiness=10、vm.min_free_kbytes=保留内存值,执行 sysctl -p 生效,无需改内核源码
Slab 监控:通过 cat /proc/slabinfo 或 slabtop 命令观察 inode、dentry 等缓存是否异常增长,配合 memcg 的 memory.kmem.slabinfo 定位泄漏源,发现异常直接清理或限制对应 cgroup
cgroup 内存限制:在 /sys/fs/cgroup/memory/ 下为关键业务配置 memory.limit_in_bytes 和 memory.soft_limit_in_bytes,设置合理的 oom_guard 阈值,避免业务进程被误杀
告警体系:对 MemAvailable 低于 10%、Slab 占比超过 40%、OOM Killer 触发次数等指标设置实时告警,提前介入
优雅降级:在内存压力达到预警线时,自动触发非核心服务的优雅降级,释放内存给核心业务
- 内核原生补充证据——OOM完整执行链路:选定→标记→SIGKILL→Reaper 异步回收(v6.1 稳定版):
- 前面两段代码分别展示了OOM通知机制和豁免判定逻辑,但选定目标后的执行链路是多数开发者未知的盲区
- 内核实际执行OOM分为三步:
- 1.mark_oom_victim():标记 TIF_MEMDIE 线程标志,授予内存储备访问特权,打破分配死锁;
2.__oom_kill_process():发送 SIGKILL,同时遍历所有共享同一 mm 的进程连坐杀死;
3.queue_oom_reaper():延迟 2 个 jiffies 后启动内核线程异步回收物理内存
// (1) 标记受害者,授予内存储备访问特权
static void mark_oom_victim(struct task_struct *tsk)
{
struct mm_struct *mm = tsk->mm;
WARN_ON(oom_killer_disabled);
// 设置 TIF_MEMDIE 标志:持有此标志的进程可从内存储备中分配
if (test_and_set_tsk_thread_flag(tsk, TIF_MEMDIE))
return;
// 绑定 oom_mm,供 oom_reaper 后续回收
if (!cmpxchg(&tsk->signal->oom_mm, NULL, mm))
mmgrab(tsk->signal->oom_mm);
__thaw_task(tsk); // 唤醒被 frozen 的进程
atomic_inc(&oom_victims); // 全局 victim 计数 +1
}
// (2) 执行杀进程:SIGKILL + 共享 mm 连坐 + 触发 Reaper
static void __oom_kill_process(struct task_struct *victim,
const char *message)
{
struct task_struct *p;
struct mm_struct *mm;
bool can_oom_reap = true;
p = find_lock_task_mm(victim);
if (!p) {
// 目标已在退出,跳过
put_task_struct(victim);
return;
} else if (victim != p) {
get_task_struct(p);
put_task_struct(victim);
victim = p;
}
mm = victim->mm;
mmgrab(mm);
count_vm_event(OOM_KILL); // 内核事件计数
memcg_memory_event_mm(mm, MEMCG_OOM_KILL);
// 发送 SIGKILL
do_send_sig_info(SIGKILL, SEND_SIG_PRIV, victim, PIDTYPE_TGID);
mark_oom_victim(victim); // 标记 TIF_MEMDIE
task_unlock(victim);
// 核心连坐逻辑:杀死所有共享同一 mm 的其他进程
rcu_read_lock();
for_each_process(p) {
if (!process_shares_mm(p, mm)) // 不共享 mm 的跳过
continue;
if (same_thread_group(p, victim)) // 同线程组已杀
continue;
if (is_global_init(p)) { // init 进程不能杀
can_oom_reap = false;
set_bit(MMF_OOM_SKIP, &mm->flags);
continue;
}
if (unlikely(p->flags & PF_KTHREAD)) // 内核线程跳过
continue;
do_send_sig_info(SIGKILL, SEND_SIG_PRIV, p, PIDTYPE_TGID);
}
rcu_read_unlock();
if (can_oom_reap)
queue_oom_reaper(victim); // 触发异步回收
mmdrop(mm);
put_task_struct(victim);
}
// (3) OOM Reaper:延迟 2 个 jiffies 后异步回收受害者物理内存
#define OOM_REAPER_DELAY (2*HZ)
static void queue_oom_reaper(struct task_struct *tsk)
{
if (test_and_set_bit(MMF_OOM_REAP_QUEUED,
&tsk->signal->oom_mm->flags))
return; // 已入队的不重复添加
get_task_struct(tsk);
timer_setup(&tsk->oom_reaper_timer, wake_oom_reaper, 0);
tsk->oom_reaper_timer.expires = jiffies + OOM_REAPER_DELAY;
add_timer(&tsk->oom_reaper_timer); // 2 个 jiffies 后唤醒 reaper
}
- 源码维度核心结论:
- TIF_MEMDIE 标志:被标记的进程可从系统内存储备(memory reserves)中分配页面,这是打破"内存硬死锁"的核心机制——受害者自己需要分配内存来执行退出路径;
共享 mm 连坐机制:for_each_process + process_shares_mm 确保不遗漏任何共享地址空间的用户进程,解释"一个线程 OOM、整个进程组被连坐杀死"的现象;
OOM Reaper 异步回收:延迟 2 个 jiffies(约 20ms)后异步执行 __oom_reap_task_mm 解除匿名页面映射,避免 kill 后内存释放不及时导致再次触发 OOM;
生产意义:理解 OOM 执行链路后,可在用户态通过 register_oom_notifier 拦截 out_of_memory 通知(见第 7.3 节),在 SIGKILL 发送前做最后的资源回收抢救。
- 总结
- Linux OOM Killer从来不是"内存不够杀进程"的简单逻辑
- 而是内核内存记账体系、虚实内存权重博弈、Slab内核缓存约束、乐观内存分配机制、adj归一化算法共同作用的底层系统工程
- 本文通过完整拆解v6.1源码,清晰揭示了OOM从判定到执行的完整链路:
环节 源码函数 核心机制
豁免判定 oom_unkillable_task() 过滤 init 进程与 PF_KTHREAD 内核线程
豁免判定 oom_badness() 内部 adj==OOM_SCORE_ADJ_MIN adj=-1000 直接返回 LONG_MIN 跳过计分
计分模型 oom_badness() RSS + SWAPENTS + pgtables_bytes 三维度加权 + adj 归一化
目标选择 oom_evaluate_task() + select_bad_process() 遍历全局进程,选取 points 最高者
通知机制 blocking_notifier_call_chain 用户态可通过 register_oom_notifier 拦截
SIGKILL __oom_kill_process() 发送 SIGKILL + 共享 mm 连坐杀死
死锁打破 mark_oom_victim() 设置 TIF_MEMDIE,授予内存储备访问权限
异步回收 queue_oom_reaper() 延迟 2 jiffies 后内核线程回收匿名页面
Slab 检测 should_dump_unreclaim_slab() 检测不可回收 Slab 是否超过所有用户 LRU 页面
- 所有表层OOM问题,本质都是对上述源码级规则的适配缺失
- 普通开发者聚焦业务内存占用与表层参数调优,资深架构师聚焦内核计分模型、内存透支机制、隐性内存挤占与执行链路的底层约束
- 这也是线上OOM疑难故障无法通过常规手段解决的核心原因
- 本文所有逻辑均基于Linux v6.1稳定版内核原生源码机制
- 附带精准代码片段与官方溯源链接,无任何经验性玄学,可直接用于线上高可用系统底层稳定性治理