oom killer

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了oom killer相关的知识,希望对你有一定的参考价值。

参考技术A Linux内核为了提高内存的使用效率采用过度分配内存(over-commit memory)的办法,造成物理内存过度紧张进而触发OOM机制来杀死一些进程回收内存。

该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽会把该进程杀掉。

Linux在内存分配路径上会对内存余量做检查,(1)如果检查到内存不足,则触发OOM机制。(2)OOM首先会对系统所有进程(出init和内核线程等特殊进程)进行打分,并选出最bad的进程;然后杀死该进程。(3)同时会触发内核oom_reaper进行内存收割。(4)同时内核还提供了sysfs接口系统OOM行为,以及进程OOM行为。然后借用一个示例来分析OOM时内存状态。

1. 关于OOM

内核检测到系统内存不足,在内存分配路径上触发 out_of_memory() ,然后调用 select_bad_process() 选择一个'bad'进程 oom_kill_process() 杀掉,判断和选择一个‘bad'进程的过程由 oom_badness() 决定。

Linux下每个进程都有自己的OOM权重,在/proc/<pid>/oom_adj里面,范围是-17到+15,取值越高,越容易被杀掉。

2. OOM触发路径

在内存分配路径上,当内存不足的时候会触发kswapd、或者内存规整,极端情况会触发OOM,来获取更多内存。

在内存回收失败之后,__alloc_pages_may_oom是OOM的入口,但是主要工作在out_of_memory中进行处理。

由于Linux内存都是以页为单位,所以__alloc_pages_nodemask是必经之处。

static inline struct page *

__alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,

    const struct alloc_context *ac, unsigned long *did_some_progress)



    struct oom_control oc = ---------------------------------------------------------OOM控制参数。

        .zonelist = ac->zonelist,

        .nodemask = ac->nodemask,

        .memcg = NULL,

        .gfp_mask = gfp_mask,

        .order = order,

    ;

    struct page *page;

    *did_some_progress = 0;

    /*

    * Acquire the oom lock.  If that fails, somebody else is

    * making progress for us.

    */

    if (!mutex_trylock(&oom_lock))

        *did_some_progress = 1;

        schedule_timeout_uninterruptible(1);

        return NULL;

   

    page = get_page_from_freelist(gfp_mask | __GFP_HARDWALL, order,

                    ALLOC_WMARK_HIGH|ALLOC_CPUSET, ac);-----------------------------再次使用高水位检查一次,是否需要启动OOM流程。

    if (page)

        goto out;

    if (!(gfp_mask & __GFP_NOFAIL)) ----------------------------------------------__GFP_NOFAIL是不允许内存申请失败的情况,下面都是允许失败的处理。

        /* Coredumps can quickly deplete all memory reserves */

        if (current->flags & PF_DUMPCORE)

            goto out;

        /* The OOM killer will not help higher order allocs */

        if (order > PAGE_ALLOC_COSTLY_ORDER)---------------------------------------order超过3的申请失败,不会启动OOM回收。

            goto out;

        /* The OOM killer does not needlessly kill tasks for lowmem */

        if (ac->high_zoneidx < ZONE_NORMAL)

            goto out;

        if (pm_suspended_storage())

            goto out;

        /* The OOM killer may not free memory on a specific node */

        if (gfp_mask & __GFP_THISNODE)

            goto out;

   

    /* Exhausted what can be done so it's blamo time */

    if (out_of_memory(&oc) || WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL)) -------------经过上面各种情况,任然需要进行OOM处理。调用out_of_memory()。

        *did_some_progress = 1;

        if (gfp_mask & __GFP_NOFAIL)

            page = get_page_from_freelist(gfp_mask, order,

                    ALLOC_NO_WATERMARKS|ALLOC_CPUSET, ac);-------------------------对于__GFP_NOFAIL的分配情况,降低分配条件从ALLOC_WMARK_HIGH|ALLOC_CPUSET降低到ALLOC_NO_WATERMARKS|ALLOC_CPUSET。

            /*

            * fallback to ignore cpuset restriction if our nodes

            * are depleted

            */

            if (!page)

                page = get_page_from_freelist(gfp_mask, order,

                    ALLOC_NO_WATERMARKS, ac);--------------------------------------如果还是分配失败,再次降低分配标准,从ALLOC_NO_WATERMARKS|ALLOC_CPUSET降低到ALLOC_NO_WATERMARKS。真的是为了成功,节操越来越低啊。

       

   

out:

    mutex_unlock(&oom_lock);

    return page;



4.3 OOM处理:对进程打分以及杀死最高评分进程

out_of_memory函数是OOM机制的核心,他可以分为两部分。一是调挑选最’bad‘的进程,二是杀死它。

bool out_of_memory(struct oom_control *oc)



    unsigned long freed = 0;

    enum oom_constraint constraint = CONSTRAINT_NONE;

    if (oom_killer_disabled)----------------------------------------------------在freeze_processes会将其置位,即禁止OOM;在thaw_processes会将其清零,即打开OOM。所以,如果在冻结过程,不允许OOM。

        return false;

    if (!is_memcg_oom(oc))

        blocking_notifier_call_chain(&oom_notify_list, 0, &freed);

        if (freed > 0)

            /* Got some memory back in the last second. */

            return true;

   

    if (task_will_free_mem(current)) ----------------------------------------如果当前进程正因为各种原因将要退出,或者释放内存,将当前进程作为OOM候选者,然后唤醒OOM reaper去收割进而释放内存。

        mark_oom_victim(current);

        wake_oom_reaper(current);

        return true;---------------------当前进程由于自身原因将要推出,OOM则将其标注为TIF_MEMDIE状态;然后唤醒OOM Reaper去处理。不需要经过下面的打分和杀死流程。

   

    if (oc->gfp_mask && !(oc->gfp_mask & (__GFP_FS|__GFP_NOFAIL)))-----------如果内存申请掩码包括__GFP_DS或__GFP_NOFAIL,则不进行OOM收割。

        return true;

    constraint = constrained_alloc(oc);--------------------------------------未定义CONFIG_NUMA返回CONSTRAINT_NONE。

    if (constraint != CONSTRAINT_MEMORY_POLICY)

        oc->nodemask = NULL;

    check_panic_on_oom(oc, constraint);--------------------------------------检查sysctl_panic_on_oom设置,以及是否由sysrq触发,来决定是否触发panic。

    if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task &&--------------如果设置了sysctl_oom_kill_allocating_task,那么当内存耗尽时,会把当前申请内存分配的进程杀掉。

        current->mm && !oom_unkillable_task(current, NULL, oc->nodemask) &&

        current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN)

        get_task_struct(current);

        oc->chosen = current;

        oom_kill_process(oc, "Out of memory (oom_kill_allocating_task)");

        return true;

   

    select_bad_process(oc);-------------------------------------------------遍历所有进程,进程下的线程,查找合适的候选进程。即得分最高的候选进程。

    /* Found nothing?!?! Either we hang forever, or we panic. */

    if (!oc->chosen && !is_sysrq_oom(oc) && !is_memcg_oom(oc)) ------------如果没有合适候选进程,并且OOM不是由sysrq触发的,进入panic。

        dump_header(oc, NULL);

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

   

    if (oc->chosen && oc->chosen != (void *)-1UL)

        oom_kill_process(oc, !is_memcg_oom(oc) ? "Out of memory" :

                "Memory cgroup out of memory");----------------------------杀死选中的进程。

        schedule_timeout_killable(1);

   

    return !!oc->chosen;



select_bad_process()通过oom_evaluate_task()来评估每个进程的得分,对于进程1、内核线程、得分低的进程直接跳过。

static void select_bad_process(struct oom_control *oc)



    if (is_memcg_oom(oc))

        mem_cgroup_scan_tasks(oc->memcg, oom_evaluate_task, oc);

    else

        struct task_struct *p;

        rcu_read_lock();

        for_each_process(p)----------------------------------------------遍历系统范围内所有进程线程。

            if (oom_evaluate_task(p, oc))

                break;

        rcu_read_unlock();

   

    oc->chosen_points = oc->chosen_points * 1000 / oc->totalpages;



static int oom_evaluate_task(struct task_struct *task, void *arg)



    struct oom_control *oc = arg;

    unsigned long points;

    if (oom_unkillable_task(task, NULL, oc->nodemask))-------------------进程1以及内核线程等等不能被kill的线程跳过。

        goto next;

    if (!is_sysrq_oom(oc) && tsk_is_oom_victim(task))

        if (test_bit(MMF_OOM_SKIP, &task->signal->oom_mm->flags))

            goto next;

        goto abort;

   

    if (oom_task_origin(task))

        points = ULONG_MAX;

        goto select;

   

    points = oom_badness(task, NULL, oc->nodemask, oc->totalpages);------对进程task进行打分。

    if (!points || points < oc->chosen_points)---------------------------这里保证只取最高分的进程,所以分数最高者被选中。其他情况则直接跳过。

        goto next;

    /* Prefer thread group leaders for display purposes */

    if (points == oc->chosen_points && thread_group_leader(oc->chosen))

        goto next;

select:

    if (oc->chosen)

        put_task_struct(oc->chosen);

    get_task_struct(task);

    oc->chosen = task;--------------------------------------------------更新OOM选中的进程和当前最高分。

    oc->chosen_points = points;

next:

    return 0;

abort:

    if (oc->chosen)

        put_task_struct(oc->chosen);

    oc->chosen = (void *)-1UL;

    return 1;



在oom_badness()中计算当前进程的得分,返回选中进程的结构体,以及进程得分ppoints。

oom_badness()是给进程打分的函数,可以说是核心中的核心。最终结果受oom_score_adj和当前进程内存使用量综合影响。

oom_score_adj为OOM_SCORE_ADJ_MIN的进程不参加评选。进程的oom_score_adj值在/proc/xxx/oom_score_adj中。

mm->flags为MMF_OOM_SKIP的进程不参加评选。

处于vfork()中的进程不参加评选。

进程的得分取决于其消耗的RSS部分内存(文件映射内存MM_FILEPAGES、匿名映射内存MM_ANONPAGES、shmem内存MM_SHMEMPAGES)、匿名交换内存MM_SWAPENTS、PTE页表所占内存、PMD页表所占内存。

具有root权限的进程只取其97%的得分参加评选。

所以进程得分points=process_pages + oom_score_adj*totalpages/1000;如果是root权限的进程points=process_pages*0.97 + oom_score_adj*totalpages/1000。

在oom_score_adj都为0(默认值)的情况下,最终得分跟进程自身消耗的内存有关;消耗的内存越大越容易被Kill。

oom_score_adj每降低1,可以多获得系统内存资源的1/1000使用量。反之,每增加1,则少获得系统内存资源1/1000使用量。

unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,

              const nodemask_t *nodemask, unsigned long totalpages)



    long points;

    long adj;

    if (oom_unkillable_task(p, memcg, nodemask))-------------------------------如果进程不可被杀,直接跳过。

        return 0;

    p = find_lock_task_mm(p);------------找到进程p,并使用task_lock()锁上。

    if (!p)

        return 0;

    /*

    * Do not even consider tasks which are explicitly marked oom

    * unkillable or have been already oom reaped or the are in

    * the middle of vfork

    */

    adj = (long)p->signal->oom_score_adj;--------------------------------------获取当前进程的oom_score_adj参数。

    if (adj == OOM_SCORE_ADJ_MIN ||

            test_bit(MMF_OOM_SKIP, &p->mm->flags) ||

            in_vfork(p))

        task_unlock(p);

        return 0;--------------------------------------------------------------如果当前进程oom_score_adj为OOM_SCORE_ADJ_MIN的话,就返回0.等于告诉OOM,此进程不参数'bad'评比。

   

    /*

    * The baseline for the badness score is the proportion of RAM that each

    * task's rss, pagetable and swap space use.

    */

    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);-----------------可以看出points综合了内存占用情况,包括RSS部分、swap file或者swap device占用内存、以及页表占用内存。

    task_unlock(p);

    /*

    * Root processes get 3% bonus, just like the __vm_enough_memory()

    * implementation used by LSMs.

    */

    if (has_capability_noaudit(p, CAP_SYS_ADMIN))------------------------------如果是root用户,增加3%的使用特权。

        points -= (points * 3) / 100;

    /* Normalize to oom_score_adj units */

    adj *= totalpages / 1000;--------------------------------------------------这里可以看出oom_score_adj对最终分数的影响,如果oom_score_adj小于0,则最终points就会变小,进程更加不会被选中。

    points += adj;-------------------------------------------------------------将归一化后的adj和points求和,作为当前进程的分数。

    /*

    * Never return 0 for an eligible task regardless of the root bonus and

    * oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).

    */

    return points > 0 ? points : 1;



https://www.cnblogs.com/arnoldlu/p/8567559.html#oom_badness

理解和配置 Linux 下的 OOM Killer

    今天公司的其中1台mysql 重启了,登陆到终端看了一下,都是常见的 Out of memory 问题。这通常是因为某时刻应用程序大量请求内存导致系统内存不足造成的,这通常会触发 Linux 内核里的 Out of Memory (OOM) killer,OOM killer 会杀掉某个进程以腾出内存留给系统用,不致于让系统立刻崩溃。如果检查相关的日志文件(/var/log/messages)就会看到下面类似的 Out of memory: Kill process 信息:

......
Jan 17 07:47:51 kernel: 543 total pagecache pages
Jan 17 07:47:51 kernel: 0 pages in swap cache
Jan 17 07:47:51 kernel: Swap cache stats: add 0, delete 0, find 0/0
Jan 17 07:47:51 kernel: Free swap  = 0kB
Jan 17 07:47:51 kernel: Total swap = 0kB
Jan 17 07:47:51 kernel: 16777215 pages RAM
Jan 17 07:47:51 kernel: 297000 pages reserved
Jan 17 07:47:51 kernel: 2922 pages shared
Jan 17 07:47:51 kernel: 16387622 pages non-shared
Jan 17 07:47:51 kernel: [ pid ]   uid  tgid total_vm      rss cpu oom_adj oom_score_adj name
Jan 17 07:47:51 kernel: [  987]     0   987     2664      105   1     -17         -1000 udevd
Jan 17 07:47:51 kernel: [ 2119]     0  2119    23289       77  14     -17         -1000 auditd
Jan 17 07:47:51 kernel: [ 2139]     0  2139    62465     1663   1       0             0 rsyslogd
Jan 17 07:47:51 kernel: [ 2169]     0  2169     4592       73   0       0             0 irqbalance
Jan 17 07:47:51 kernel: [ 2185]    32  2185     4744       74   0       0             0 rpcbind
Jan 17 07:47:51 kernel: [ 2226]    29  2226     6373      132  17       0             0 rpc.statd
......
Jan 17 07:47:51 kernel: Out of memory: Kill process 20280 (mysqld) score 985 or sacrifice child
Jan 17 07:47:51 kernel: Killed process 20280, UID 800, (mysqld) total-vm:78445976kB, anon-rss:64818984kB, file-rss:36kB
Jan 17 07:47:51 ipmievd: Get SEL Info command failed: Node busy

    Linux 内核根据应用程序的要求分配内存,通常来说应用程序分配了内存但是并没有实际全部使用,为了提高性能,这部分没用的内存可以留作它用,这部分内存是属于每个进程的,内核直接回收利用的话比较麻烦,所以内核采用一种过度分配内存(over-commit memory)的办法来间接利用这部分 “空闲” 的内存,提高整体内存的使用效率。一般来说这样做没有问题,但当大多数应用程序都消耗完自己的内存的时候麻烦就来了,因为这些应用程序的内存需求加起来超出了物理内存(包括 swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。用银行的例子来讲可能更容易懂一些,部分人取钱的时候银行不怕,银行有足够的存款应付,当全国人民(或者绝大多数)都取钱而且每个人都想把自己钱取完的时候银行的麻烦就来了,银行实际上是没有这么多钱给大家取的。


    内核检测到系统内存不足、挑选并杀掉某个进程的过程可以参考内核源代码 linux/mm/oom_kill.c,当系统内存不足的时候,out_of_memory() 被触发,然后调用 select_bad_process() 选择一个 “bad” 进程杀掉,如何判断和选择一个 “bad” 进程呢,总不能随机选吧?挑选的过程由 oom_badness() 决定,挑选的算法和想法都很简单很朴实:最 bad 的那个进程就是那个最占用内存的进程


/**
 * oom_badness - heuristic function to determine which candidate task to kill
 * @p: task struct of which task we should calculate
 * @totalpages: total present RAM allowed for page allocation
 *
 * The heuristic for determining which task to kill is made to be as simple and
 * predictable as possible.  The goal is to return the highest value for the
 * task consuming the most memory to avoid subsequent oom failures.
 */
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
			  const nodemask_t *nodemask, unsigned long totalpages)
{
	long points;
	long adj;

	if (oom_unkillable_task(p, memcg, nodemask))
		return 0;

	p = find_lock_task_mm(p);
	if (!p)
		return 0;

	adj = (long)p->signal->oom_score_adj;
	if (adj == OOM_SCORE_ADJ_MIN) {
		task_unlock(p);
		return 0;
	}

	/*
	 * The baseline for the badness score is the proportion of RAM that each
	 * task's rss, pagetable and swap space use.
	 */
	points = get_mm_rss(p->mm) + p->mm->nr_ptes +
		 get_mm_counter(p->mm, MM_SWAPENTS);
	task_unlock(p);

	/*
	 * Root processes get 3% bonus, just like the __vm_enough_memory()
	 * implementation used by LSMs.
	 */
	if (has_capability_noaudit(p, CAP_SYS_ADMIN))
		adj -= 30;

	/* Normalize to oom_score_adj units */
	adj *= totalpages / 1000;
	points += adj;

	/*
	 * Never return 0 for an eligible task regardless of the root bonus and
	 * oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
	 */
	return points > 0 ? points : 1;
}

    上面代码里的注释写的很明白,理解了这个算法我们就理解了为啥 MySQL 躺着也能中枪了,因为它的体积总是最大(一般来说它在系统上占用内存最多),所以如果 Out of Memeory (OOM) 的话总是不幸第一个被 kill 掉。解决这个问题最简单的办法就是增加内存,或者想办法优化 MySQL 使其占用更少的内存,除了优化 MySQL 外还可以优化系统(优化 CentOS 6.x),让系统尽可能使用少的内存以便应用程序(如 MySQL) 能使用更多的内存,还有一个临时的办法就是调整内核参数,让 MySQL 进程不容易被 OOM killer 发现。

配置 OOM killer

我们可以通过一些内核参数来调整 OOM killer 的行为,避免系统在那里不停的杀进程。比如我们可以在触发 OOM 后立刻触发 kernel panic,kernel panic 10秒后自动重启系统。

[email protected]/4 # sysctl -w vm.panic_on_oom=1
vm.panic_on_oom = 1

[email protected]/4 # sysctl -w kernel.panic=10
kernel.panic = 10

[email protected]/4 # echo "vm.panic_on_oom=1" >> /etc/sysctl.conf
[email protected]/4 # echo "kernel.panic=10" >> /etc/sysctl.conf


    从上面的 oom_kill.c 代码里可以看到 oom_badness() 给每个进程打分,根据 points 的高低来决定杀哪个进程,这个 points 可以根据 adj 调节,root 权限的进程通常被认为很重要,不应该被轻易杀掉,所以打分的时候可以得到 3% 的优惠(adj -= 30; 分数越低越不容易被杀掉)。我们可以在用户空间通过操作每个进程的 oom_adj 内核参数来决定哪些进程不这么容易被 OOM killer 选中杀掉。比如,如果不想 MySQL 进程被轻易杀掉的话可以找到 MySQL 运行的进程号后,调整 oom_score_adj 为 -15(注意 points 越小越不容易被杀):

[email protected]/4 # ps aux|grep ^mysql 
mysql    48926  113 52.2 61362436 34462480 ?   Sl   07:47 164:11 /usr/local/mysql/bin/mysqld --basedir=/usr/local/mys。。。
[email protected]/4 # cat /proc/48926/oom_score_adj
0
[email protected]/4 # echo -15 > /proc/48926/oom_score_adj

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

[email protected]/4 # sysctl -w vm.overcommit_memory=2
[email protected]/4 # echo "vm.overcommit_memory=2" >> /etc/sysctl.conf

找出最有可能被 OOM Killer 杀掉的进程

    我们知道了在用户空间可以通过操作每个进程的 oom_adj 内核参数来调整进程的分数,这个分数也可以通过 oom_score 这个内核参数看到,比如查看进程号为981的 omm_score,这个分数被上面提到的 omm_score_adj 参数调整后(-15),就变成了3:

[email protected]/4 # cat /proc/981/oom_score
18

[email protected]/4 # echo -15 > /proc/981/oom_score_adj
[email protected]/4 # cat /proc/981/oom_score
3

  下面这个 bash 脚本可用来打印当前系统上 oom_score 分数最高(最容易被 OOM Killer 杀掉)的进程:

[email protected]/4 # vim oomscore.sh
#!/bin/bash
for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do
    printf "%2d %5d %s\n"         "$(cat $proc/oom_score)"         "$(basename $proc)"         "$(cat $proc/cmdline | tr '\0' ' ' | head -c 50)"
done 2>/dev/null | sort -nr | head -n 10

[email protected]/4 # bash oomscore.sh
523 48926 /usr/local/mysql/bin/mysqld --basedir=/usr/local/m
 1  9267 -bash 
 1  9263 sshd: [email protected]/4  
 1  9065 mysql -uroot -px xxxxxx -h127.0.0.1 -P3306 topgun 
 1  9010 /bin/bash 
 1  9009 SCREEN -R restore 
 1  8990 mysql -uroot -px xxxxxx -h127.0.0.1 -P3306 topgun 
 1  8913 /bin/bash 
 1  8912 sudo -s 
 1  8856 -bash


以上是关于oom killer的主要内容,如果未能解决你的问题,请参考以下文章

oom解决方案

Linux OOM(out of memory)

Java OOM学习

内存快照排查OOM,加密时错误方法指定provider方式错误引起的OOM

OOM!又是OOM!Android 内存监控一定要注意!

OOM!又是OOM!Android 内存监控一定要注意!