人生在世,被喜欢和被讨厌各占50%;自己喜欢自己,喜欢你的人占比就超过了50%。

Linux 内核中的 OOM (Out-Of-Memory) 机制是当系统内存耗尽时,用来杀死某些进程以释放内存的机制。为了决定应该杀死哪个进程,内核使用了一套复杂的打分系统,这个过程称为 OOM 打分机制。

基本概述

OOM 机制的触发

当系统内存耗尽,无法满足进程分配内存的需求时,内核会触发 OOM 杀手(OOM Killer)。OOM 杀手会选中一个进程并终止它,以释放内存。整个过程大致分为以下几个步骤:

  1. 检测内存不足:当系统内存不足且无法通过释放缓存、回收页面等方式缓解时,触发 OOM 机制。
  2. 选择目标进程:通过打分机制选定一个进程作为目标。
  3. 杀死目标进程:终止目标进程并释放其占用的内存资源。

OOM 打分机制

内核通过为每个进程打分来决定哪个进程应被杀死。打分机制主要依据进程对系统资源的占用情况,打分越高,越有可能被杀死。打分过程涉及多个因素:

  1. 进程的内存占用:占用内存越多的进程,得分越高。这是最重要的因素。
    • 通过 mm->total_vm (虚拟内存总量) 计算,值越大,打分越高。
  2. 进程的优先级 (nice 值):进程的优先级也影响得分。优先级越低(nice 值越高),得分越低。
    • 通过进程的 oom_score_adj 进一步调整。用户可以通过 /proc/[pid]/oom_score_adj 来手动调节这个值,使某些进程更容易或更不容易被杀。
  3. 进程年龄:某些情况下,运行时间越长的进程得分越低,内核倾向于保留长期运行的服务进程。
  4. 子进程数:如果进程有大量子进程,意味着它可能是一个服务进程,内核可能降低它的得分。
  5. CAP_SYS_ADMIN 权限:具有此权限的进程通常得分较低,除非没有其他选择,因为这些进程通常是系统管理进程。
  6. 用户调整:用户可以通过调整 oom_score_adj 或者 oom_adj(旧版本)来影响某个进程的 OOM 分数。这些值允许用户通过 /proc/[pid]/oom_score_adj 对某个进程的 OOM 得分进行加权。

具体打分公式

在 Linux 3.8 及更高版本中,内核使用了一个计算公式来评估进程的 OOM 分数:

1
2
3
4
cCopy codepoints = mm->total_vm + (mm->hiwater_rss - mm->rss);
points += mm->hiwater_rss * 3 / 2;
points += mm->rss;
points += task_struct->oom_score_adj;

解释:

  • total_vm 是进程的虚拟内存总量。
  • hiwater_rss 是进程历史上消耗内存的最高点。
  • rss 是当前实际驻留在内存中的页数。
  • oom_score_adj 是用户自定义的调整值。

OOM 优先级与保护机制

  • oom_score: 每个进程的 OOM 分数可以在 /proc/[pid]/oom_score 中查看。分数越高,进程越有可能被杀死。
  • oom_adj 和 oom_score_adj:用户和管理员可以通过 /proc/[pid]/oom_adj/proc/[pid]/oom_score_adj 文件调整进程的 OOM 优先级。例如,将 oom_score_adj 设置为 -1000 可以保护进程不被 OOM 杀手杀死,而设置为 1000 会让它优先被杀。

OOM 杀手的执行

一旦 OOM 打分完成,内核会选择得分最高的进程进行杀死。杀死进程时,会发送 SIGKILL 信号给目标进程,强制终止它。杀死目标进程后,内核会重新评估系统内存,如果仍然不足,可能继续杀死其他进程,直到内存压力缓解。

具体的系统配置

管理员可以通过 /proc/sys/vm/oom_kill_allocating_task 来配置是否优先杀死当前触发 OOM 的进程。此外,还可以通过 /proc/sys/vm/panic_on_oom 配置系统在 OOM 时是否直接崩溃而不是杀死进程。

源码

1. OOM Killer 触发机制

当系统内存耗尽且无法回收更多内存时,OOM Killer 会被触发。这个过程通常发生在内存管理的页面分配过程中,如果内核无法满足内存分配请求且所有回收机制(如文件系统缓存回收、页面交换等)均失败,就会调用 OOM Killer。

代码位置:

1
2
3
4
5
6
// Linux 内核 进程内存管理代码
// 文件:mm/page_alloc.c
if (should_alloc_retry(gfp_mask, order, ac))
goto retry;
...
out_of_memory(gfp_mask, order, ac);

2. out_of_memory() 函数

out_of_memory() 是 OOM Killer 的入口函数,它决定是否需要触发 OOM Killer,以及后续的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 文件:mm/oom_kill.c
bool out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask, unsigned int order, struct mem_cgroup *memcg, int *ret)
{
if (!oom_killer_disabled && !mutex_trylock(&oom_lock))
return false;

// 判断当前系统状态,如果已经在 OOM 处理中,返回
if (oom_killer_disabled)
return false;

// 选择要杀死的进程
struct task_struct *p = select_bad_process();
if (!p)
return false;

// 执行杀死操作
oom_kill_process(p, "Out of memory");
return true;
}

3. 选择被杀进程:select_bad_process()

select_bad_process() 函数会遍历系统中的所有进程,并为每个进程计算一个 OOM 分数,根据分数决定哪个进程应该被杀死。分数越高的进程越可能被杀死。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 文件:mm/oom_kill.c
static struct task_struct *select_bad_process(void)
{
struct task_struct *tsk, *selected = NULL;
unsigned long points = 0;

for_each_process(tsk) {
// 检查进程是否可杀
if (is_oom_unkillable(tsk))
continue;

// 计算 OOM 分数
unsigned long totalpoints = oom_badness(tsk, NULL, NULL);

// 选择得分最高的进程
if (totalpoints > points) {
selected = tsk;
points = totalpoints;
}
}
return selected;
}

4. 计算 OOM 分数:oom_badness()

oom_badness() 函数根据多个因素计算进程的 OOM 分数,包括内存占用、优先级、进程年龄等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 文件:mm/oom_kill.c
static unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *mem, const nodemask_t *nodemask)
{
unsigned long points = 0;

// 计算内存占用分数
points += p->mm->total_vm;

// 考虑优先级调整
points += p->signal->oom_score_adj;

// 如果进程是一个长期运行的服务,降低其分数
if (has_capability_noaudit(p, CAP_SYS_ADMIN))
points /= 2;

return points;
}

5. 杀死进程:oom_kill_process()

oom_kill_process() 函数负责最终的杀死操作。它向选中的进程发送 SIGKILL 信号,强制终止该进程。

1
2
3
4
5
6
7
8
9
10
11
12
// 文件:mm/oom_kill.c
void oom_kill_process(struct task_struct *p, const char *message)
{
pr_err("%s: Kill process %d (%s) score %lu or sacrifice child\n",
message, task_pid_nr(p), p->comm, p->signal->oom_score_adj);

// 发送 SIGKILL 信号
do_send_sig_info(SIGKILL, SEND_SIG_FORCED, p, true);

// 释放 OOM 锁
mutex_unlock(&oom_lock);
}

6. 相关数据结构

task_struct 是进程的核心数据结构,mm_struct 则用于描述进程的内存使用情况。oom_score_adj 则是用户可以调整的一个参数,用于影响进程的 OOM 打分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct task_struct {
struct mm_struct *mm;
struct signal_struct *signal;
...
};

struct mm_struct {
unsigned long total_vm;
...
};

struct signal_struct {
int oom_score_adj;
...
};

总结

Linux 内核中的 OOM Killer 是一个复杂但高效的机制,用于在系统内存耗尽时自动释放内存资源。通过 out_of_memory() 函数触发,选择打分最高的进程并执行杀死操作,以保持系统的稳定性。用户还可以通过调整 oom_score_adj 等参数影响 OOM Killer 的行为。

kernel 3.10代码分析–Out Of Memory(OOM)处理流程