oom "out_of_memory" implementatoin 介绍

oom “out_of_memory” implementatoin 介绍.

注意,这里是基于内核 kernel-3.10.0-957.el7 (RHEL7.6)

1 OOM logs大概长什么样(这里使用手动触发OOM作为例子).

1.1 例子 1

1
2
Sep 15 23:08:48 XYZ kernel: SysRq : Manual OOM execution
Sep 15 23:08:48 XYZ kernel: Purging GPU memory, 29 pages freed, 30133 pages still pinned.

1.2 例子 2

1
2
3
4
5
6
7
8
9
Sep 15 23:38:48 XYZ kernel: SysRq : Manual OOM execution
Sep 15 23:38:48 XYZ kernel: kworker/5:0 invoked oom-killer: gfp_mask=0xd0, order=0,
Sep 15 23:38:48 XYZ kernel: Workqueue: events moom_callback
Sep 15 23:38:48 XYZ kernel: Call Trace:
Sep 15 23:38:48 XYZ kernel: [<ffffffffa7161dc1>] dump_stack+0x19/0x1b
Sep 15 23:38:48 XYZ kernel: [<ffffffffa715c7ea>] dump_header+0x90/0x229
Sep 15 23:38:48 XYZ kernel: [<ffffffffa6bba274>] oom_kill_process+0x254/0x3d0
Sep 15 23:38:48 XYZ kernel: [<ffffffffa6bbaab6>] out_of_memory+0x4b6/0x4f0
Sep 15 23:38:48 XYZ kernel: [<ffffffffa6e61f0d>] moom_callback+0x4d/0x50

2 看一下OOM的out_of_memory函数. 它主要分了7步,先看简化的代码,之后看具体介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask,
int order, nodemask_t *nodemask, bool force_kill){
...
blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
if (freed > 0)
return;
...
if (fatal_signal_pending(current) || current->flags & PF_EXITING) {
set_thread_flag(TIF_MEMDIE);
return;
}
...
check_panic_on_oom(constraint, gfp_mask, order, mpol_mask);

if (sysctl_oom_kill_allocating_task && current->mm && !oom_unkillable_task(current, NULL, nodemask) && current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
...
oom_kill_process(current, gfp_mask, order, 0, totalpages, NULL, nodemask, "Out of memory (oom_kill_allocating_task)");
goto out;
}
p = select_bad_process(&points, totalpages, mpol_mask, force_kill);
...
if (!p) {
dump_header(NULL, gfp_mask, order, NULL, mpol_mask);
panic("Out of memory and no killable processes...\n");
}
...
oom_kill_process(p, gfp_mask, order, points, totalpages, NULL, nodemask, "Out of memory");
out:
...
schedule_timeout_killable(1);
}

第一, 如果出现oom, 先去处理通知链oom_notify_list的回调函数,如果内存回收成功(表现为freed大于0),则直接返回,然后快乐的收工了.

1
blocking_notifier_call_chain(&oom_notify_list, 0, &freed);

第二, 如果当前进程current(是一个thread_info结构)正在等待SIGKILL或者正在退出,设置进程标记为”TIF_MEMDIE”(代表进程由于OOM,目前正在关闭), 然后直接返回, 然后快乐的收工了.

1
if (fatal_signal_pending(current) || current->flags & PF_EXITING)

第三, 限于NUMA场景(x86_64基本都是了,我们也可以看到内核配了CONFIG_NUMA=y). 如果配置了vm.panic_on_oom=1 (或者其他非0值,比如2)出现OOM, 系统就panic了(注意,RHEL8内核作了加强,如果是由于sysrq-trigger的,就不panic了). panic之后也就没的玩了,被迫收工.

1
check_panic_on_oom(constraint, gfp_mask, order, mpol_mask);

第四, 如果配置了vm.oom_kill_allocating_task=1,而且当前进程不是内核进程,也不是1号init进程,而且有内存可以释放, 并且进程的oom_score_adj不是-1000;总而言之,就是这个进程具备被kill的条件, 那就把它kill掉. 然后快乐的收工了.

1
if (sysctl_oom_kill_allocating_task && current->mm &&!oom_unkillable_task(current, NULL, nodemask) && current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN)

第五, 如果跑到了这里,那就要花点心思选一个分数最高的进程来kill了. 基本的要点就是每个任务的rss,页表和交换空间使用的RAM的比例, 谁totalpages多, 那就越危险了 (因为totalpages的值会算到得分里面去D, 如果是root用户的进程,会给额外3%的折扣,root就是牛呀.), 如果找到就kill掉它,然后也可以收工了.

1
select_bad_process(&points, totalpages, mpol_mask, force_kill);

第六, 这一步和第七是二选一的. 如果还没找到可以kill的进程,那就倒霉了. 输出信息之后就等系统panic了.没的玩了,被迫收工

1
panic("Out of memory and no killable processes...\n");

第七, 这一步和第六是二选一的. 如果找到可以kill的进程,kill掉它.将进程设置为TASK_KILLABLE, 然后等待1个jiffies,那就全部打完收工咯.

1
2
oom_kill_process(p, gfp_mask, order, points, totalpages, NULL,nodemask, "Out of memory");
schedule_timeout_killable(1);

3 再来补充一下前面提到的oom_notify_list的通知链.

3.1 内核为OOM定义了一个oom_notify_list通知链.

1
static BLOCKING_NOTIFIER_HEAD(oom_notify_list);

3.2 一些希望在OOM时,收到内核通知的, 就先把自己注册到通知链 (目前virtio_balloon和i915注册到了oom_notify_list通知链里面,所以出现OOM, 会先在通知链里面找这两个敢死队员:P). 如果出现OOM, 就先去通知链去找已经注册好的回调函数. 这也就是为什么我们能看到第1个OOM的log,而没有出现更多的OOM logs.

  • virtio_balloon
    1
    vb->nb.notifier_call = virtballoon_oom_notify;
  • i915
    1
    i915->mm.oom_notifier.notifier_call = i915_gem_shrinker_oom;