Linux OOM killer 与相关参数详解

一、前言
本文是描述Linux virtual memory运行参数的第二篇,主要是讲OOM相关的参数的。为了理解OOM参数,第二章简单的描述什么是OOM。如果这个名词对你毫无压力,你可以直接进入第三章,这一章是描述具体的参数的,除了描述具体的参数,我们引用了一些具体的内核代码,本文的代码来自4.0内核,如果有兴趣,可以结合代码阅读,为了缩减篇幅,文章中的代码都是删减版本的。按照惯例,最后一章是参考文献,本文的参考文献都是来自linux内核的Documentation目录,该目录下有大量的文档可以参考,每一篇都值得细细品味。
二、什么是OOM
OOM就是out of memory的缩写,虽然linux kernel有很多的内存管理技巧(从cache中回收、swap out等)来满足各种应用空间的vm内存需求,但是,当你的系统配置不合理,让一匹小马拉大车的时候,linux kernel会运行非常缓慢并且在某个时间点分配page frame的时候遇到内存耗尽、无法分配的状况。应对这种状况首先应该是系统管理员,他需要首先给系统增加内存,不过对于kernel而言,当面对OOM的时候,咱们也不能慌乱,要根据OOM参数来进行相应的处理。
三、OOM参数
1、panic_on_oom
当kernel遇到OOM的时候,可以有两种选择:
(1)产生kernel panic(就是死给你看)。
(2)积极面对人生,选择一个或者几个最“适合”的进程,启动OOM killer,干掉那些选中的进程,释放内存,让系统勇敢的活下去。
panic_on_oom这个参数就是控制遇到OOM的时候,系统如何反应的。当该参数等于0的时候,表示选择积极面对人生,启动OOM killer。当该参数等于2的时候,表示无论是哪一种情况,都强制进入kernel panic。panic_on_oom等于其他值的时候,表示要区分具体的情况,对于某些情况可以panic,有些情况启动OOM killer。kernel的代码中,enum oom_constraint 就是一个进一步描述OOM状态的参数。系统遇到OOM总是有各种各样的情况的,kernel中定义如下:
enum oom_constraint {
    CONSTRAINT_NONE,
    CONSTRAINT_CPUSET,
    CONSTRAINT_MEMORY_POLICY,
    CONSTRAINT_MEMCG,
};
对于UMA而言, oom_constraint永远都是CONSTRAINT_NONE,表示系统并没有什么约束就出现了OOM,不要想太多了,就是内存不足了。在NUMA的情况下,有可能附加了其他的约束导致了系统遇到OOM状态,实际上,系统中还有充足的内存。这些约束包括:
(1)CONSTRAINT_CPUSET。cpusets是kernel中的一种机制,通过该机制可以把一组cpu和memory node资源分配给特定的一组进程。这时候,如果出现OOM,仅仅说明该进程能分配memory的那个node出现状况了,整个系统有很多的memory node,其他的node可能有充足的memory资源。
(2)CONSTRAINT_MEMORY_POLICY。memory policy是NUMA系统中如何控制分配各个memory node资源的策略模块。用户空间程序(NUMA-aware的程序)可以通过memory policy的API,针对整个系统、针对一个特定的进程,针对一个特定进程的特定的VMA来制定策略。产生了OOM也有可能是因为附加了memory policy的约束导致的,在这种情况下,如果导致整个系统panic似乎有点不太合适吧。
(3)CONSTRAINT_MEMCG。MEMCG就是memory control group,Cgroup这东西太复杂,这里不适合多说,Cgroup中的memory子系统就是控制系统memory资源分配的控制器,通俗的将就是把一组进程的内存使用限定在一个范围内。当这一组的内存使用超过上限就会OOM,在这种情况下的OOM就是CONSTRAINT_MEMCG类型的OOM。
OK,了解基础知识后,我们来看看内核代码。内核中sysctl_panic_on_oom变量是和/proc/sys/vm/panic_on_oom对应的,主要的判断逻辑如下:
void check_panic_on_oom(enum oom_constraint constraint, gfp_t gfp_mask,
            int order, const nodemask_t *nodemask)
{
    if (likely(!sysctl_panic_on_oom))----0表示启动OOM killer,因此直接return了
        return;
    if (sysctl_panic_on_oom != 2) {----2是强制panic,不是2的话,还可以商量
        if (constraint != CONSTRAINT_NONE)---在有cpuset、memory policy、memcg的约束情况下
            return;                                                  的OOM,可以考虑不panic,而是启动OOM killer
    }
    dump_header(NULL, gfp_mask, order, NULL, nodemask);
    panic("Out of memory: %s panic_on_oom is enabled\n",
        sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide");---死给你看啦
}
2、oom_kill_allocating_task
当系统选择了启动OOM killer,试图杀死某些进程的时候,又会遇到这样的问题:干掉哪个,哪一个才是“合适”的哪那个进程?系统可以有下面的选择:
(1)谁触发了OOM就干掉谁
(2)谁最“坏”就干掉谁
oom_kill_allocating_task这个参数就是控制这个选择路径的,当该参数等于0的时候选择(2),否则选择(1)。具体的代码可以在参考__out_of_memory函数,具体如下:
static void __out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask,
        int order, nodemask_t *nodemask, bool force_kill)   {
……
    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) {
        get_task_struct(current);
        oom_kill_process(current, gfp_mask, order, 0, totalpages, NULL,
                 nodemask, "Out of memory (oom_kill_allocating_task)");
        goto out;
    }
……
}
当然也不能说杀就杀,还是要考虑是否用户空间进程(不能杀内核线程)、是否unkillable task(例如init进程就不能杀),用户空间是否通过设定参数(oom_score_adj)阻止kill该task。如果万事俱备,那么就调用oom_kill_process干掉当前进程。
3、oom_dump_tasks
当系统的内存出现OOM状况,无论是panic还是启动OOM killer,做为系统管理员,你都是想保留下线索,找到OOM的root cause,例如dump系统中所有的用户空间进程关于内存方面的一些信息,包括:进程标识信息、该进程使用的total virtual memory信息、该进程实际使用物理内存(我们又称之为RSS,Resident Set Size,不仅仅是自己程序使用的物理内存,也包含共享库占用的内存),该进程的页表信息等等。拿到这些信息后,有助于了解现象(出现OOM)之后的真相。
当设定为0的时候,上一段描述的各种进程们的内存信息都不会打印出来。在大型的系统中,有几千个进程,逐一打印每一个task的内存信息有可能会导致性能问题(要知道当时已经是OOM了)。当设定为非0值的时候,在下面三种情况会调用dump_tasks来打印系统中所有task的内存状况:
(1)由于OOM导致kernel panic
(2)没有找到适合的“bad”process
(3)找适合的并将其干掉的时候
4、oom_adj、oom_score_adj和oom_score
准确的说这几个参数都是和具体进程相关的,因此它们位于/proc/xxx/目录下(xxx是进程ID)。假设我们选择在出现OOM状况的时候杀死进程,那么一个很自然的问题就浮现出来:到底干掉哪一个呢?内核的算法倒是非常简单,那就是打分(oom_score,注意,该参数是read only的),找到分数最高的就OK了。那么怎么来算分数呢?可以参考内核中的oom_badness函数:
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
              const nodemask_t *nodemask, unsigned long totalpages)
{……
    adj = (long)p->signal->oom_score_adj;
    if (adj == OOM_SCORE_ADJ_MIN) {----------------------(1)
        task_unlock(p);
        return 0;---------------------------------(2)
    }
    points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
        atomic_long_read(&p->mm->nr_ptes) + mm_nr_pmds(p->mm);---------(3)
    task_unlock(p);
    if (has_capability_noaudit(p, CAP_SYS_ADMIN))-----------------(4)
        points -= (points * 3) / 100;
    adj *= totalpages / 1000;----------------------------(5)
    points += adj; 
    return points > 0 ? points : 1;
}
(1)对某一个task进行打分(oom_score)主要有两部分组成,一部分是系统打分,主要是根据该task的内存使用情况。另外一部分是用户打分,也就是oom_score_adj了,该task的实际得分需要综合考虑两方面的打分。如果用户将该task的 oom_score_adj设定成OOM_SCORE_ADJ_MIN(-1000)的话,那么实际上就是禁止了OOM killer杀死该进程。
(2)这里返回了0也就是告知OOM killer,该进程是“good process”,不要干掉它。后面我们可以看到,实际计算分数的时候最低分是1分。
(3)前面说过了,系统打分就是看物理内存消耗量,主要是三部分,RSS部分,swap file或者swap device上占用的内存情况以及页表占用的内存情况。
(4)root进程有3%的内存使用特权,因此这里要减去那些内存使用量。
(5)用户可以调整oom_score,具体如何操作呢?oom_score_adj的取值范围是-1000~1000,0表示用户不调整oom_score,负值表示要在实际打分值上减去一个折扣,正值表示要惩罚该task,也就是增加该进程的oom_score。在实际操作中,需要根据本次内存分配时候可分配内存来计算(如果没有内存分配约束,那么就是系统中的所有可用内存,如果系统支持cpuset,那么这里的可分配内存就是该cpuset的实际额度值)。oom_badness函数有一个传入参数totalpages,该参数就是当时的可分配的内存上限值。实际的分数值(points)要根据oom_score_adj进行调整,例如如果oom_score_adj设定-500,那么表示实际分数要打五折(基数是totalpages),也就是说该任务实际使用的内存要减去可分配的内存上限值的一半。
了解了oom_score_adj和oom_score之后,应该是尘埃落定了,oom_adj是一个旧的接口参数,其功能类似oom_score_adj,为了兼容,目前仍然保留这个参数,当操作这个参数的时候,kernel实际上是会换算成oom_score_adj,有兴趣的同学可以自行了解,这里不再细述了。
四、参考文献
1、Documentation/vm/numa_memory_policy.txt
2、Documentation/sysctl/vm.txt
3、Documentation/cgroup/cpusets.txt
4、Documentation/cgroup/memory.txt
5、Documentation/filesystems/proc.txt
 
 

OOM killer

当物理内存和交换空间都被用完时,如果还有进程来申请内存,内核将触发OOM killer,其行为如下:

1.检查文件/proc/sys/vm/panic_on_oom,如果里面的值为2,那么系统一定会触发panic
2.如果/proc/sys/vm/panic_on_oom的值为1,那么系统有可能触发panic(见后面的介绍)
3.如果/proc/sys/vm/panic_on_oom的值为0,或者上一步没有触发panic,那么内核继续检查文件/proc/sys/vm/oom_kill_allocating_task
3.如果/proc/sys/vm/oom_kill_allocating_task为1,那么内核将kill掉当前申请内存的进程
4.如果/proc/sys/vm/oom_kill_allocating_task为0,内核将检查每个进程的分数,分数最高的进程将被kill掉(见后面介绍)

进程被kill掉之后,如果/proc/sys/vm/oom_dump_tasks为1,且系统的rlimit中设置了core文件大小,将会由/proc/sys/kernel/core_pattern里面指定的程序生成core dump文件,这个文件里将包含
pid, uid, tgid, vm size, rss, nr_ptes, nr_pmds, swapents, oom_score_adj
score, name等内容,拿到这个core文件之后,可以做一些分析,看为什么这个进程被选中kill掉。

这里可以看看ubuntu默认的配置:

  1.  
    #OOM后不panic
  2.  
    dev@ubuntu:~$ cat /proc/sys/vm/panic_on_oom
  3.  
  4.  
     
  5.  
    #OOM后kill掉分数最高的进程
  6.  
    dev@ubuntu:~$ cat /proc/sys/vm/oom_kill_allocating_task
  7.  
  8.  
     
  9.  
    #进程由于OOM被kill掉后将生成core dump文件
  10.  
    dev@ubuntu:~$ cat /proc/sys/vm/oom_dump_tasks
  11.  
    1
  12.  
     
  13.  
    #默认max core file size是0, 所以系统不会生成core文件
  14.  
    dev@ubuntu:~$ prlimit|grep CORE
  15.  
    CORE max core file size 0 unlimited blocks
  16.  
     
  17.  
    #core dump文件的生成交给了apport,相关的设置可以参考apport的资料
  18.  
    dev@ubuntu:~$ cat /proc/sys/kernel/core_pattern
  19.  
    |/usr/share/apport/apport %p %s %c %P

参考:apport

panic_on_oom

正如上面所介绍的那样,该文件的值可以取0/1/2,0是不触发panlic,2是一定触发panlic,如果为1的话就要看mempolicycpusets,这篇不介绍这方面的内容。

panic后内核的默认行
为是死在那里,目的是给开发人员一个连上去debug的机会。但对于大多数应用层开发人员来说没啥用,倒是希望它赶紧重启。为了让内核panic后重启,可以修改文件/proc/sys/kernel/panic,里面表示的是panic多少秒后系统将重启,这个文件的默认值是0,表示永远不重启。

  1.  
    #设置panic后3秒重启系统
  2.  
    dev@ubuntu:~$ sudo sh -c "echo 3 > /proc/sys/kernel/panic"

调整分数

当oom_kill_allocating_task的值为0时(系统默认配置),系统会kill掉系统中分数最高的那个进程,这里的分数是怎么来的呢?该值由内核维护,并存储在每个进程的/proc/<pid>/oom_score文件中。

每个进程的分数受多方面的影响,比如进程运行的时间,时间越长表明这个程序越重要,所以分数越低;进程从启动后分配的内存越多,表示越占内存,分数会越高;这里只是列举了一两个影响分数的因素,实际情况要复杂的多,需要看内核代码,这里有篇文章可以参考:Taming the OOM killer

由于分数计算复杂,比较难控制,于是内核提供了另一个文件用来调控分数,那就是文件/proc/<pid>/oom_adj,这个文件的默认值是0,但它可以配置为-17到15中间的任何一个值,内核在计算了进程的分数后,会和这个文件的值进行一个计算,得到的结果会作为进程的最终分数写入/proc/<pid>/oom_score。计算方式大概如下:

  • 如果/proc/<pid>/oom_adj的值为正数,那么分数将会被乘以2的n次方,这里n是文件里面的值

  • 如果/proc/<pid>/oom_adj的值为负数,那么分数将会被除以2的n次方,这里n是文件里面的值

由于进程的分数在内核中是一个16位的整数,所以-17就意味着最终进程的分数永远是0,也即永远不会被kill掉。

当然这种控制方式也不是非常精确,但至少比没有强多了。

修改配置

上面的这些文件都可以通过下面三种方式来修改,这里以panic_on_oom为例做个示范:

  • 直接写文件(重启后失效)

    dev@ubuntu:~$ sudo sh -c "echo 2> /proc/sys/vm/panic_on_oom"
     
  • 通过控制命令(重启后失效)

    dev@dev:~$ sudo sysctl vm.panic_on_oom=2
     
  • 修改配置文件(重启后继续生效)

    1.  
      #通过编辑器将vm.panic_on_oom=2添加到文件sysctl.conf中(如果已经存在,修改该配置项即可)
    2.  
      dev@dev:~$ sudo vim /etc/sysctl.conf
    3.  
       
    4.  
      #重新加载sysctl.conf,使修改立即生效
    5.  
      dev@dev:~$ sudo sysctl -p

日志

一旦OOM killer被触发,内核将会生成相应的日志,一般可以在/var/log/messages里面看到,如果配置了syslog,日志可能在/var/log/syslog里面,这里是ubuntu里的日志样例

  1.  
    dev@dev:~$ grep oom /var/log/syslog
  2.  
    Jan 23 21:30:29 dev kernel: [ 490.006836] eat_memory invoked oom-killer: gfp_mask=0x24280ca, order=0, oom_score_adj=0
  3.  
    Jan 23 21:30:29 dev kernel: [ 490.006871] [<ffffffff81191442>] oom_kill_process+0x202/0x3c0

cgroup的OOM killer

除了系统的OOM killer之外,如果配置了memory cgroup,那么进程还将受到自己所属memory cgroup的限制,如果超过了cgroup的限制,将会触发cgroup的OOM killer,cgroup的OOM killer和系统的OOM killer行为略有不同,详情请参考Linux Cgroup系列(04):限制cgroup的内存使用

malloc

malloc是libc的函数,C/C++程序员对这个函数应该都很熟悉,它里面实际上调用的是内核的sbrkmmap,为了避免频繁的调用内核函数和优化性能,它里面在内核函数的基础上实现了一套自己的内存管理功能。

既然内存不够时有OOM killer帮我们kill进程,那么这时调用的malloc还会返回NULL给应用进程吗?答案是不会,因为这时只有两种情况:

  1. 当前申请内存的进程被kill掉:都被kill掉了,返回什么都没有意义了

  2. 其它进程被kill掉:释放出了空闲的内存,于是内核就能给当前进程分配内存了

那什么时候我们调用malloc的时候会返回NULL呢,从malloc函数的帮助文件可以看出,下面两种情况会返回NULL:

  • 使用的虚拟地址空间超过了RLIMIT_AS的限制

  • 使用的数据空间超过了RLIMIT_DATA的限制,这里的数据空间包括程序的数据段,BSS段以及heap

关于虚拟地址空间和heap之类的介绍请参考Linux进程的内存使用情况,这两个参数的默认值为unlimited,所以只要不修改它们的默认配置,限制就不会被触发。有一种极端情况需要注意,那就是代码写的有问题,超过了系统的虚拟地址空间范围,比如32位系统的虚拟地址空间范围只有4G,这种情况下不确定系统会以一种什么样的方式返回错误。

rlimit

上面提到的RLIMIT_AS和RLIMIT_DATA都可以通过函数getrlimit和setrlimit来设置和读取,同时linux还提供了一个prlimit程序来设置和读取rlimit的配置。

prlimit是用来替代
ulimit的一个程序,除了能设置上面的那两个参数之外,还有其它的一些参数,比如core文件的大小。关于prlimit的用法请参考它的帮助文件

  1.  
    #默认情况下,RLIMIT_AS和RLIMIT_DATA的值都是unlimited
  2.  
    dev@dev:~$ prlimit |egrep "DATA|AS"
  3.  
    AS address space limit unlimited unlimited bytes
  4.  
    DATA max data size unlimited unlimited bytes

测试代码

C语言的程序会受到libc的影响,可能在触发OOM killer之前就触发了segmentfault错误,如果要用C语言程序来测试触发OOM killer,一定要注意malloc的行为受MMAP_THRESHOLD影响,一次申请分配太多内存的话,malloc会调用mmap映射内存,从而不一定触发OOM killer,具体细节目前还不太清楚。这里是一个触发oom killer的例子,供参考:

  1.  
    #include <stdio.h>
  2.  
    #include <stdlib.h>
  3.  
    #include <string.h>
  4.  
    #include <unistd.h>
  5.  
     
  6.  
    #define M (1024 * 1024)
  7.  
    #define K 1024
  8.  
     
  9.  
    int main(int argc, char *argv[])
  10.  
    {
  11.  
    char *p;
  12.  
    int size =0;
  13.  
    while(1) {
  14.  
    p = (char *)malloc(K);
  15.  
    if (p == NULL){
  16.  
    printf("memory allocate failed!\n");
  17.  
    return -1;
  18.  
    }
  19.  
    memset(p, 0, K);
  20.  
    size += K;
  21.  
    if (size%(100*M) == 0){
  22.  
    printf("%d00M memory allocated\n", size/(100*M));
  23.  
    sleep(1);
  24.  
    }
  25.  
    }
  26.  
     
  27.  
    return 0;
  28.  
    }

结束语

对一个进程来说,内存的使用受多种因素的限制,可能在系统内存不足之前就达到了rlimit和memory cgroup的限制,同时它还可能受不同编程语言所使用的相关内存管理库的影响,就算系统处于内存不足状态,申请新内存也不一定会触发OOM killer,需要具体问题具体分析。

参考

转载自0:

https://blog.csdn.net/u011677209/article/details/52769225
https://blog.csdn.net/oDaiLiDong/article/details/81907752
上一篇:树莓派进阶之路 (020) - 基于24位AD转换模块HX711的重量称量实验


下一篇:【前端】:jQuery下