前文回顾: 我们此前已经梳理了 OOM 的触发条件与 Memory Reserve 机制。当系统彻底无页可回收到、内存保留耗尽后,内核就进入 out_of_memory() 中的“杀人决策”环节——从数百个运行进程和内核线程中,精准选出一个作为 victim 处死。这个决策的核心,就藏在 oom_badness() 这个“死亡名单打分算法”里。本章我们就完全基于源码,逐行解析这个看似简单却充满工程智慧的打分逻辑。
一、总体流程:三道过滤 + 一个打分
oom_badness() 不是被 out_of_memory() 直接调用的,它位于 oom_evaluate_task() 这一层评估函数中,被 select_bad_process() 遍历所有任务时调用。整体流水线可以总结为:
out_of_memory()
└─→ select_bad_process(oc)
├─ 全局 OOM:for_each_process(p) → oom_evaluate_task(p, oc)
└─ 内存组 OOM:mem_cgroup_scan_tasks(oc->memcg, ...)
└─→ oom_evaluate_task(task, arg)
├─ oom_unkillable_task(task) → 第一道过滤
├─ oom_cpuset_eligible(task, oc) → 第二道过滤
├─ tsk_is_oom_victim(task) → 第三道过滤
├─ oom_task_origin(task) → 特殊豁免
├─ oom_badness(task, totalpages) → 核心打分
└─ 比较 chosen_points,更新 chosen这就是整个选择过程:遍历 → 过滤 → 打分 → 取最高分。

二、三道前置过滤
2.1 第一道:oom_unkillable_task —— 谁绝对不能死
/* 第 164 行 */
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;
}这个函数直白而冷酷:如果进程是 init(PID 1) 或者 内核线程(PF_KTHREAD),立即返回 true——这意味着 oom_evaluate_task 中会走到 goto next,该任务被永久排除在候选名单之外。
为什么?
init是系统核心进程,若被杀系统直接崩溃,无法再执行任何清理操作。内核线程(如
kworker、kswapd)是维持内核运转的关键基础设施,杀死它们会导致系统死锁。
两道检查 is_global_init() 与 PF_KTHREAD 确保了内核不会自杀。
2.2 第二道:oom_cpuset_eligible —— 进程是否“可触及”
static bool oom_cpuset_eligible(struct task_struct *start,
struct oom_control *oc)
{
struct task_struct *tsk;
bool ret = false;
const nodemask_t *mask = oc->nodemask;
if (is_memcg_oom(oc))
return true; /* 内存组 OOM 不限制 */
rcu_read_lock();
for_each_thread(start, tsk) {
if (mask) {
/* mempolicy 约束 OOM:检查内存策略交集 */
ret = mempolicy_in_oom_domain(tsk, mask);
} else {
/* 非 mempolicy OOM:检查 cpuset 内存交集 */
ret = cpuset_mems_allowed_intersects(current, tsk);
}
if (ret)
break;
}
rcu_read_unlock();
return ret;
}关键行为:
若 OOM 是由 Memory Cgroup 触发,直接返回
true(该 cgroup 内的进程都是候选,无需额外过滤)。若 OOM 是全局性内存压力,则对每个线程检查它是否可以触及当前 OOM 触发者的内存域。
Mempolicy 约束(
__GFP_THISNODE等):通过mempolicy_in_oom_domain()检查策略节点重叠。Cpuset 约束:通过
cpuset_mems_allowed_intersects()检查 cpuset 的内存节点是否相交。
这道过滤的目的:防止杀死那些内存不在同一域中的进程——比如一个进程只绑定了 NUMA node 0,但当前 OOM 发生在 node 1 上的分配,杀死它并不能释放可用内存。
2.3 第三道:tsk_is_oom_victim —— 已标记死亡者不再参与竞争
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;
}tsk_is_oom_victim(task)的实现极其简洁,仅是一个指针检查:static inline bool tsk_is_oom_victim(struct task_struct * tsk) { return tsk->signal->oom_mm; }若
task->signal->oom_mm非空,该进程已经被选为 OOM victim 且正在被杀。两种情形:
如果该进程
MMF_OOM_SKIP标志已设置(OOM reaper 已处理过),则跳过它(goto next),防止重复选择。否则直接
goto abort,中止遍历(oom_evaluate_task返回 1),select_bad_process停止寻找。
为什么要 abort? 如果已经有进程被选为 victim 且正在释放内存(但 OOM reaper 还没来得及回收),立即中止遍历并杀死新进程会破坏当前 victim 的“优先释放内存”机制,可能导致更多浪费。
三、特殊豁免:oom_task_origin —— 必杀标记
if (oom_task_origin(task)) {
points = LONG_MAX;
goto select;
}这个豁免在过滤之后、打分之前,拥有绝对优先级。
oom_task_origin() 的实现:
static inline bool oom_task_origin(const struct task_struct *p)
{
return p->signal->oom_flag_origin;
}它仅检查一个布尔位 oom_flag_origin,该位由 set_current_oom_origin() 设置,用来标记触发 OOM 的那个进程本身。它的意义是:当系统必须选一个进程时,优先杀死发起者自己——因为它最清楚自己占了多少内存,杀死它后释放的内存最多。
源码中如何设置这个位: 在 out_of_memory() 入口之前、或用户态通过 /proc/<pid>/oom_score_adj 调整时,内核会设置该标志。一旦设置,该进程的 points 被直接设为 LONG_MAX,无需打分,直接选中。
四、核心打分:oom_badness() 逐行拆解

评分函数的核心定义(mm/oom_kill.c 第 203-241 行):
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
long points;
long adj;
/* 第一道内部防线 */
if (oom_unkillable_task(p))
return LONG_MIN;
/* 获取拥有 mm 的真实线程 */
p = find_lock_task_mm(p);
if (!p)
return LONG_MIN;
/* 第二道内部防线 */
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;
}
/* 基础得分:RSS + Swap + 页表 / PAGE_SIZE */
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
task_unlock(p);
/* 用户态调整:adj * totalpages / 1000 */
adj *= totalpages / 1000;
points += adj;
return points;
}4.1 内部防线一:oom_unkillable_task
与 oom_evaluate_task 中的检查一致。打分函数内部再次调用,是双重保险——避免外部调用时遗漏。
4.2 查找拥有 mm 的真实线程:find_lock_task_mm
struct task_struct *find_lock_task_mm(struct task_struct *p)
{
struct task_struct *t;
rcu_read_lock();
for_each_thread(p, t) {
task_lock(t);
if (likely(t->mm))
goto found;
task_unlock(t);
}
t = NULL;
/* found: */
/* RCU + task_lock 保护 t->mm 不为空 */
...
}为什么需要这个函数?
一个进程的 task_struct 可能有多个线程,而 task->mm 可能被某些线程通过 kthread_use_mm() 分离出去——这些线程不再使用原进程的虚拟内存。同时,还有线程可能已经退出,->mm 已经清空。
find_lock_task_mm 的作用:
遍历进程
p的所有线程找到第一个拥有
t->mm != NULL的线程用
task_lock()持有该线程的自旋锁,防止线程切换->mm以
RCU方式保护task_struct不被释放
如果所有线程都找不到有 mm 的,返回 NULL,该进程被标记为 LONG_MIN(不可杀)。
4.3 内部防线二:用户态调整 + vfork + 已跳过
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;
}oom_score_adj == -1000:用户态(/proc/<pid>/oom_score_adj)将值设为OOM_SCORE_ADJ_MIN (-1000),等同于oom_score_adj=0时的最大负偏移,相当于将该进程"免疫"——永远不会被杀死。打分函数立即返回LONG_MIN。MMF_OOM_SKIP标志:该进程的页表已在OOM reaper处理过程中被映射清除,不再需要作为 victim 被杀死。in_vfork(p):若进程正在进行 vfork 调用,内核不应选中它,以免破坏 vfork 父子进程间共享页表的语义。
4.4 基础得分:RSS + Swap + 文件页 + 页表
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;得分公式 = 三部分之和,单位都是 页。
RSS(驻留集大小):
get_mm_rss()是一个内联函数:static inline unsigned long get_mm_rss(struct mm_struct *mm) { return get_mm_counter(mm, MM_FILEPAGES) + get_mm_counter(mm, MM_ANONPAGES) + get_mm_counter(mm, MM_SHMEMPAGES); }它将
文件页+匿名页+共享页三部分合并,即进程的物理驻留内存总量。Swap 占用:
get_mm_counter(p->mm, MM_SWAPENTS)获取该进程占用的 swap 页数。swap 虽然不占物理 RAM,但它占用了总交换空间,且交换出去的内存无法立即回收,对 OOM 系统构成压力。页表大小:
mm_pgtables_bytes(p->mm) / PAGE_SIZE将该进程页表占用的字节数转换为页单位,纳入得分。这是为了让内存占用大且地址空间分散的进程(如 mmap 大量内存)也能被选中——页表本身也消耗内存。
为什么用驻留集而非总虚拟内存?
OOM 发生在物理内存耗尽,所以必须看 物理页占用,而不是虚拟地址空间大小。一个进程可能映射了 1TB 虚拟内存,但若没有真正分配页,它实际占用的物理内存极少,不应被优先杀死。
4.5 用户态调整:oom_score_adj 缩放调整
adj *= totalpages / 1000;
points += adj;公式:adjusted_points = raw_points + adj * totalpages / 1000
totalpages:oc->totalpages,表示整个系统中可用于回收的总页面数(RAM 页面 + Swap 页面)。adj:p->signal->oom_score_adj,范围是 -1000 ~ +999(OOM_SCORE_ADJ_MIN = -1000)。缩放系数:
totalpages / 1000,将范围 [1000, 999] 映射到[raw_points - totalpages * 1000/1000, raw_points + totalpages * 999/1000],即raw_points - totalpages到raw_points + totalpages * 0.999。
实际效果:
若
adj = 0,无调整。若
adj = 1000,得分增加totalpages,该进程几乎必然成为 victim(除非有更大的)。若
adj = -1000,得分减少totalpages,可能跌入负值 → 被oom_evaluate_task的points < oc->chosen_points过滤掉。
设计意图: 允许管理员将关键进程的 oom_score_adj 降低(如守护进程设为 -1000),或将高消耗进程(如浏览器、Java)设为正值,人为影响 OOM 决策。
五、oom_evaluate_task 的遍历与选择
回到 oom_evaluate_task 的完整逻辑:
static int oom_evaluate_task(struct task_struct *task, void *arg)
{
struct oom_control *oc = arg;
long points;
/* 过滤 1:不可杀任务 */
if (oom_unkillable_task(task))
goto next;
/* 过滤 2:cpuset/mempolicy 过滤 */
if (!is_memcg_oom(oc) && !oom_cpuset_eligible(task, oc))
goto next;
/* 第三道:正在被杀的任务 */
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;
}
/* 特殊豁免:必杀 */
if (oom_task_origin(task)) {
points = LONG_MAX;
goto select;
}
/* 核心打分 */
points = oom_badness(task, oc->totalpages);
if (points == LONG_MIN || points < oc->chosen_points)
goto next;
/* 更新 chosen */
select:
if (oc->chosen)
put_task_struct(oc->chosen);
get_task_struct(task);
oc->chosen = task;
oc->chosen_points = points;
next:
return 0;
abort:
if (oc->chosen)
put_task_struct(oc->chosen);
oc->chosen = (void *)-1UL;
return 1;
}关键细节:
oc->chosen_points初始化为LONG_MIN,第一个合法候选的points就会成为新的chosen。get_task_struct/put_task_struct是引用计数保护,避免在遍历过程中选中的进程被释放。返回
0表示继续遍历(找更大的),返回1表示停止遍历(abort,已有 victim 正在被杀)。
select_bad_process() 的调用方式也很关键:
if (is_memcg_oom(oc))
mem_cgroup_scan_tasks(oc->memcg, oom_evaluate_task, oc);
else {
struct task_struct *p;
rcu_read_lock();
for_each_process(p)
if (oom_evaluate_task(p, oc))
break;
rcu_read_unlock();
}全局 OOM:
for_each_process遍历所有 task,找到后break。内存组 OOM:
mem_cgroup_scan_tasks只扫描 cgroup 内的进程。
六、总结:一个简单的打分,背后的设计哲学

整个 oom_badness 体系虽然只有几十行代码,但蕴含了丰富的设计哲学:
简单可靠:评分公式只用了驻留内存 + swap + 页表,避免了复杂的权重与优先级。内核开发者认为**"内存消耗最大的进程应该被杀"** 是最可预测的准则,不需要引入负载、优先级、用户身份等复杂因素。
多层防御:
oom_unkillable_task、oom_cpuset_eligible、tsk_is_oom_victim、MMF_OOM_SKIP、oom_score_adj == -1000、in_vfork—— 六道过滤防线,确保不会被错误选中。可干预性:
oom_score_adj为用户态保留了精细调控的入口,支持-1000(免疫)到+999(优先被杀)全范围调节。安全优先:
find_lock_task_mm用 RC +task_lock双重保护,确保不会访问到已经被分离、释放的mm,避免 use-after-free 漏洞。快速决策:
oom_task_origin直接给发起者赋LONG_MAX,避免不必要的遍历,让"谁搞出来的问题谁背锅"的原则落地。
下一章我们将继续深入 oom_reaper —— 那个在进程被杀后异步解除地址空间映射的幕后英雄,以及 OOM Kill + panic_on_oom 的两种紧急处理机制。