oom “out_of_memory” implementatoin 介绍.
注意,这里是基于内核 kernel-3.10.0-957.el7 (RHEL7.6)
1 OOM logs大概长什么样(这里使用手动触发OOM作为例子).
1.1 例子 1
1 | Sep 15 23:08:48 XYZ kernel: SysRq : Manual OOM execution |
1.2 例子 2
1 | Sep 15 23:38:48 XYZ kernel: SysRq : Manual OOM execution |
2 看一下OOM的out_of_memory函数. 它主要分了7步,先看简化的代码,之后看具体介绍:
1 | void out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask, |
第一, 如果出现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 | oom_kill_process(p, gfp_mask, order, points, totalpages, NULL,nodemask, "Out of memory"); |
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;