Linux OOM机制分析

作者 by adtxl / 2022-02-25 / 暂无评论 / 385 个足迹

转载自https://learning-kernel.readthedocs.io/en/latest/mem-management.html

Linux下有一种 OOM KILLER 的机制,它会在系统内存耗尽的情况下,启用自己算法有选择性的杀掉一些进程。

1. 为什么会有OOM killer

当我们使用应用时,需要申请内存,即进行 malloc 的操作,进行 malloc 操作如果返回一个 非NULL 的操作表示申请到了可用的内存。事实上,这个地方是可能存在问题的。

当我们在用户空间申请内存时,一般使用 malloc ,是不是当 malloc 返回为空时,没有可以申请的内存空间就会返回呢?答案是 否定 的。在 malloc 申请内存的机制中有如下一段描述

By default, Linux follows an optimistic memory allocation strategy.
This means that when malloc() returns non-NULL there is  no  guarantee
that  the  memory  really is available.  This is a really bad bug.  In
case it turns out that the system is out of memory, one or more processes
will be killed by the infamous OOM killer.  In case Linux is employed
under circumstances where it would be less desirable to suddenly lose
some randomly picked processes, and moreover the kernel version is
sufficiently recent, one can switch off this overcommitting behavior
using a command like:

# echo 2  > /proc/sys/vm/overcommit_memory

See also the kernel Documentation directory, files vm/overcommit-accounting and sysctl/vm.txt.

上面的描述中说明了在Linux中当malloc返回的是非空时,并不代表有可以使用的内存空间。Linux系统允许程序申请比系统可用内存更多的内存空间,这个特性叫做 overcommit 特性,这样做可能是为了系统的优化,因为不是所有的程序申请了内存就会立刻使用,当真正的使用时,系统可能已经回收了一些内存。但是,当你使用时Linux系统没有内存可以使用时,OOM Killer就会出来让一些进程退出。

Linux下有3种Overcommit的策略(参考内核文档: Documentation/vm/overcommit-accounting ),可以在 /proc/sys/vm/overcommit_memory 配置(可以取0,1和2三个值,默认是0)。

  • 0: 启发式策略 ,比较严重的Overcommit将不能得逞,比如你突然申请了128TB的内存。而轻微的overcommit将被允许。另外,root能Overcommit的值比普通用户要稍微多。
  • 1: 永远允许overcommit ,这种策略适合那些不能承受内存分配失败的应用,比如某些科学计算应用。
  • 2: 永远禁止overcommit ,在这个情况下,系统所能分配的内存不会超过 swap+RAM*系数 (/proc/sys/vm/overcmmit_ratio,默认50%,你可以调整),如果这么多资源已经用光,那么后面任何尝试申请内存的行为都会返回错误,这通常意味着此时没法运行任何新程序。

2. 如何保护一个进程不被OOM killer杀掉呢?

我们可以修改 /proc/<pid>/oom_adj 的值,这里的默认值为0,当我们设置为-17时,对于该进程来说,就不会触发OOM机制,被杀掉:

echo -17 > /proc/$(pidof sshd)/oom_adj

这里为什么是-17呢?这和Linux的实现有关系。在Linux内核中的oom.h文件中,可以看到下面的定义:

/* /proc/<pid>/oom_adj set to -17 protects from the oom-killer */
#define OOM_DISABLE (-17)
/* inclusive */
#define OOM_ADJUST_MIN (-16)
#define OOM_ADJUST_MAX 15

这个oom_adj中的变量的范围为15到-16之间。越大越容易被kill。oom_score就是它计算出来的一个值,就是根据这个值来选择哪些进程被kill掉的。

总之,通过上面的分析可知,满足下面的条件后,就是启动OOM机制。

  1. VM里面分配不出更多的page(注意linux kernel是延迟分配page策略,及用到的时候才alloc;所以malloc + memset才有效)。
  2. 用户地址空间不足,这种情况在32bit机器上user space超过了3GB,在64bit机器上不太可能发生。

3. 如何选择要kill掉的进程

只要存在overcommit,就可能会有OOM killer。

Linux系统的选择策略也一直在不断的演化。我们可以通过设置一些值来影响OOM killer做出决策。Linux下每个进程都有个OOM权重,在/proc/<pid>/oom_adj里面,取值是-17到+15,取值越高,越容易被干掉。

最终OOM killer是通过 /proc/<pid>/oom_score 这个值来决定哪个进程被干掉的。这个值是系统综合进程的内存消耗量、CPU时间(utime + stime)、存活时间(uptime - start time)和oom_adj计算出的,消耗内存越多分越高,存活时间越长分越低。

总之,总的策略是:损失最少的工作,释放最大的内存同时不伤及无辜的用了很大内存的进程,并且杀掉的进程数尽量少。 另外,Linux在计算进程的内存消耗的时候,会将子进程所耗内存的一半同时算到父进程中。

4. /proc/<pid>/oom_*

4.1. /proc/<pid>/oom_score_adj

该文件的描述如下:

The value of /proc/<pid>/oom_score_adj is added to the badness score before it
is used to determine which task to kill.  Acceptable values range from -1000
(OOM_SCORE_ADJ_MIN) to +1000 (OOM_SCORE_ADJ_MAX).  This allows userspace to
polarize the preference for oom killing either by always preferring a certain
task or completely disabling it.  The lowest possible value, -1000, is
equivalent to disabling oom killing entirely for that task since it will always
report a badness score of 0.

在计算最终的 badness score 时,会在计算结果是中加上 oom_score_adj ,这样用户就可以通过该在值来保护某个进程不被杀死或者每次都杀某个进程。其取值范围为-1000到1000 。

如果将该值设置为-1000,则进程永远不会被杀死,因为此时 badness score 永远返回0。

4.2. /proc/<pid>/oom_adj

该文件的描述如下:

The value of /proc/<pid>/oom_score_adj is added to the badness score before it
for backwards compatibility with previous kernels, /proc/<pid>/oom_adj may also
be used to tune the badness score.  Its acceptable values range from -16
(OOM_ADJUST_MIN) to +15 (OOM_ADJUST_MAX) and a special value of -17
(OOM_DISABLE) to disable oom killing entirely for that task.  Its value is
scaled linearly with /proc/<pid>/oom_score_adj.

该设置参数的存在是为了和旧版本的内核兼容。其设置范围为-17到15。

注意 :内核使用以上两个接口时,如果更改其中一个,另一个会自动跟着变化。

内核的实现方式为:

  • oom_score_adj时,内核里都记录在变量 task->signal->oom_score_adj 中;
  • oom_score_adj时,从内核的变量 task->signal->oom_score_adj 中读取;
  • oom_adj时,也是记录到变量 task->signal->oom_score_adj 中,会根据oom_adj值按比例换算成oom_score_adj
  • oom_adj时,也是从内核变量 task->signal->oom_score_adj 中读取,只不过显示时又按比例换成oom_adj的范围。

所以,就会产生如下精度丢失的情况:

# echo 9 > /proc/556/oom_adj
# cat /proc/556/oom_score_adj
529
# cat /proc/556/oom_adj
8

警告 :当选择杀死一个父进程时,oom killer会尝试着杀死该父进程的第一个子进程(具有独立的地址空间的子进程),而不是直接杀死这个父进程。这样就避免了杀死一些重要的系统守护进程。

4.3. /proc/<pid>/oom_score

该文件的描述如下:

This file can be used to check the current score used by the oom-killer is for
any given <pid>. Use it together with /proc/<pid>/oom_score_adj to tune which
process should be killed in an out-of-memory situation.

OOM killer机制主要根据该值和 /proc/<pid>/oom_score_adj 来决定杀死哪一个进程的。

5. 下面看看内核是如何实现的?

下图是out_of_memory的调用关系

__alloc_pages_nodemask -> __alloc_pages_slowpath -> __alloc_pages_may_oom -> out_of_memory ->
__out_of_memory -> select_bad_process -> oom_kill_process

其逻辑操作如下:

  1. 首先通过函数 blocking_notifier_call_chain 遍历用户注册的通知链函数,如果通知链的callback函数能够处理OOM,则直接退出OOM killer操作。
  2. 如果引起OOM的进程(current)拥有pending SIGKILL,或者正在退出,则选择current进程。
  3. 检查发生OOM时,内核是否需要panic,如果panic,这后续代码就不会执行。
  4. 如果设置了 sysctl_oom_kill_allocating_task ,并且 current->mm 不为空,current的 oom_score_adj != OOM_SCORE_ADJ_MIN ,且可以杀死current,则直接杀死current进程,释放内存。
  5. 调用 select_bad_process 选择一个最优的进程p去杀
  6. 如果 p == null, 即没有进程可杀,内核则直接panic,否则调用函数oom_kill_process去kill选择的进程p。

6. 那 select_bad_process 如何选择一个可以杀死的进程呢?

  1. 该函数遍历所有的进程和线程 for_each_process_thread(g, p)
  2. 针对每一个线程:该函数先利用 oom_scan_process_thread 检查线程的类别,排除一些特殊的线程,然后对可以作为候选的线程进行评分。
  3. 最后返回评分最高的线程。

第2步骤中,排除的线程包括:

  • init进程
  • kernel thread线程
  • 进程没有可以释放的内存
  • 不是引起oom的memcg中的进程
  • 进程正被OOM kill
  • 等等

7. 其他控制oom killer的行为

7.1. /proc/sys/vm/oom_dump_tasks

可以取值为0或者非0(默认为1),表示是否在发送oom killer时,打印task的相关信息。

7.2. /proc/sys/vm/oom_kill_allocating_task

可以取值为0或者非0(默认为0),0代表发送oom时,进行遍历任务链表,选择一个进程去杀死,而非0代表,发送oom时,直接kill掉引起oom的进程,并不会去遍历任务链表。

7.3. /proc/sys/vm/panic_on_oom

当发送out of memory时,该值允许或者禁止内核panic。(默认为0)

  • 0: 发生oom时,内核会选择性的杀死一些进程,然后尝试着去恢复。
  • 1: 发送oom时,内核直接panic。(如果一个进程使用mempolicy、cpusets来限制内存在一个nodes中消耗,则不会发生panic)
  • 2: 发送oom时,内核无条件直接panic

·panic_on_oom=2+kdump`,一起作用时,这样用户就可以分析出为什么会发送oom的原因了。

7.4. 完全关闭 OOM killer

如果需要的话,可以完全关闭 OOM killer(不推荐用在生产环境下):

# sysctl -w vm.overcommit_memory=2
# echo "vm.overcommit_memory=2" >> /etc/sysctl.conf

独特见解