上一篇文章我们总结了 OOM Killer 的各种操作用法,但会用不等于深入理解。本次我们就深入 Linux 内核源码,来好好研究一下这个“斩杀线” OOM 机制究竟是如何设计的。
接下来我们以 5.10 内核版本为主要分析对象。
分析的起点,自然是走到触发 OOM Killer 的核心函数 out_of_memory。这个函数定义了当系统检测到超出内存资源限制时,应该如何应对。
注意:以下分析基于简化后的类伪代码,旨在阐明核心逻辑,并会用 *** 标注其对应上一篇文章中提到的哪一个调优项。
让我们先看看 out_of_memory 函数的主要逻辑框架:
// 位于 mm/oom_kill.c
bool out_of_memory(struct oom_control *oc)
{
// 规则1:如果全局禁用了OOM Killer,则直接返回
if (oom_killer_disabled)
return false;
// 规则2:检查进程是否在允许被杀的候选集内(涉及cpuset/mempolicy)
if (!is_memcg_oom(oc) && !oom_cpuset_eligible(task, oc)) {
// 如果进程不在候选集内,则跳过它
mem_cgroup_account_oom_skip(task, oc);
return 0;
}
// 规则3:一个特殊策略——谁触发,杀谁
if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task &&
current->mm && !oom_unkillable_task(current) &&
oom_cpuset_eligible(current, oc) &&
current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
// *** 对应上篇文章表中第五项:oom_kill_allocating_task ***
// 条件满足时,直接选择当前触发OOM的进程进行斩杀
get_task_struct(current);
oc->chosen = current;
oom_kill_process(oc, “Out of memory (oom_kill_allocating_task)”);
return true;
}
// 如果上述快速路径都没走通,则进入常规选择流程:挑选“最坏”的进程
select_bad_process(oc);
// ... 后续逻辑:斩杀被选中的进程
oom_kill_process(oc, ...);
}
从框架可以看出,OOM Killer 并非盲目杀戮,而是有一套清晰的优先级判断。在常规选择流程中,核心函数是 select_bad_process,它负责遍历进程并打分,选出那个“最该死”的。
select_bad_process 函数会根据是否是由 Cgroup 内存限制触发的 OOM(即 memcg oom)而走不同的分支:
// 位于 mm/oom_kill.c
static void select_bad_process(struct oom_control *oc)
{
if (cgroup mem oom) {
// Cgroup OOM 路径
mem_cgroup_select_bad_process(oc); // 实现在 mm/memcontrol.c
[
// *** 对应上篇文章表中第一项:cgroup优先级策略 ***
if (oc->use_priority_oom) {
// 选择优先级最低的 Cgroup
victim_memcg = mem_cgroup_select_victim_cgroup(...);
[
if (!memcg->use_hierarchy)
return memcg;
// 否则,迭代遍历子Cgroup,选择其中优先级(oom.priority)最低的一个
return 优先级最低的 memcg;
]
}
// 在选中的 Cgroup 内,遍历其下的任务并计算分数,逻辑与非Cgroup OOM一致
mem_cgroup_scan_tasks(victim_memcg, oom_evaluate_task, oc);
]
} else {
// 全局 OOM 路径
for_each_process(p) {
if (oom_evaluate_task(p, oc)) {
// 找到一个候选进程,可能直接选中或参与评分竞争
break;
}
}
}
}
无论是哪种路径,最终为单个进程打分的核心函数都是 oom_badness,它决定了进程的“坏”程度(分数越高,越容易被杀)。
// 位于 mm/oom_kill.c
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
// *** 对应上篇文章表中第三项:oom_score_adj ***
long adj = (long)p->signal->oom_score_adj;
long points;
// 规则1:不可杀进程(如内核线程、init进程)直接返回最小分
if (task_is_unkillable(p) || task_is_locked_memory(p))
return LONG_MIN;
// 规则2:如果 oom_score_adj 被设为 -1000,则免疫OOM Kill
if (adj == OOM_SCORE_ADJ_MIN) // OOM_SCORE_ADJ_MIN = -1000
return LONG_MIN;
// 计算基础分:主要基于物理内存(RSS)、页表、Swap使用量
points = 常驻内存集(rss) + 页表占用 + swap内存比例;
// 调整分数:oom_score_adj 的加权影响
adj *= totalpages / 1000;
points += adj;
return points;
}
此外,文中提到了两个至关重要的筛选策略,它们决定了哪些进程有资格进入“候选名单”。这主要取决于进程的内存分配策略(mempolicy)或 CPU 亲和性集(cpuset)是否与当前触发 OOM 的上下文有交集。这个判断在函数 oom_cpuset_eligible 中完成。
// 位于 mm/oom_kill.c
bool oom_cpuset_eligible(struct task_struct *start, struct oom_control *oc)
{
const nodemask_t *mask = oc->nodemask;
...
// 遍历线程组
for_each_thread(start, tsk) {
if (mask) {
// 情况A:由内存策略(mempolicy)约束的OOM
// *** 对应上篇文章表中第四项 ***
ret = mempolicy_nodemask_intersects(tsk, mask);
[
// 检查任务的内存策略掩码与触发OOM的掩码是否有交集
struct mempolicy *mempolicy = tsk->mempolicy;
if (!mempolicy) // 默认策略,允许在任何节点分配,视为有交集
return true;
switch (mempolicy->mode) {
// ... 处理 bind, interleave, preferred 等策略
case MPOL_BIND:
case MPOL_INTERLEAVE:
// 关键判断:位图是否有交集
nodes_intersects(mempolicy->v.nodes, *mask);
[
// 底层使用 bitmap_intersects() 判断
// 两个位图中是否有至少一个相同的位被设置为1
]
}
]
} else {
// 情况B:非mempolicy约束的全局或cpuset OOM
// *** 对应上篇文章表中第二项:cpuset内存交集判断 ***
ret = cpuset_mems_allowed_intersects(current, tsk);
[
// 判断当前触发OOM的进程(‘current’)与待检查进程(‘tsk’)
// 它们的 cpuset 所允许的内存节点(mems_allowed)是否有交集。
// 原理:获取两者的 mems_allowed 掩码,进行按位与(AND)操作。
// 结果非零表示有交集,该进程有资格被选中。
]
}
if (ret) // 只要线程组中有一个线程符合条件,即认为该任务符合条件
break;
}
return ret;
}
总结
通过源码分析,我们可以看到 Linux OOM Killer 的“制度设计”是一个多层次的筛选与决策过程:
- 全局开关检查:首先确认 OOM Killer 未被禁用。
- 资格预审:通过
oom_cpuset_eligible 检查进程的内存访问范围(由 cpuset 或 mempolicy 定义)是否与当前出问题的内存区域有交集,无交集者直接豁免。
- 快速通道:若启用
oom_kill_allocating_task 且条件满足,则直接斩杀触发 OOM 的进程,无需遍历评分。
- 分组策略:对于 Cgroup OOM,可配置优先级策略,先挑出优先级最低的 Cgroup。
- 精细打分:在候选进程集合中,通过
oom_badness 函数计算每个进程的“坏”分数。分数综合考虑了物理内存使用量、交换区使用量,并最终由 oom_score_adj 这个用户可调参数进行强力加权(可正可负,甚至设置为-1000实现完全免疫)。
- 最终执行:选择分数最高的进程(或通过快速通道选定的进程)执行
oom_kill_process。
理解这套机制,对于在复杂部署环境(尤其是容器和混合负载场景)下进行有效的内存风险控制和进程保活至关重要。它不仅仅是内存耗尽的应急响应,更是系统资源管理策略的一个关键组成部分。如果你想深入探讨更多内核或计算机系统底层原理,欢迎在 云栈社区 的技术板块,如计算机基础等,与更多开发者交流学习。