首页 > linux/unix > 内核 > Linux内核OOM机制
2017
08-08

Linux内核OOM机制

Linux 内核有个机制叫OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程杀掉。典型的情况是:某天一台机器突然ssh远程登录不了,但能ping通,说明不是网络的故障,原因是sshd进程被OOM killer杀掉了(多次遇到这样的假死状况)。重启机器后查看系统日志/var/log/messages会发现Out of Memory: Kill process 类似的错误信息。

...
Out of memory: Kill process 9682 (mysqld) score 9 or sacrifice child
Killed process 9682, UID 27, (mysqld) total-vm:47388kB, anon-rss:3744kB, file-rss:80kB
httpd invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
httpd cpuset=/ mems_allowed=0
Pid: 8911, comm: httpd Not tainted 2.6.32-279.1.1.el6.i686 #1
...
21556 total pagecache pages
21049 pages in swap cache
Swap cache stats: add 12819103, delete 12798054, find 3188096/4634617
Free swap  = 0kB
Total swap = 524280kB
131071 pages RAM
0 pages HighMem
3673 pages reserved
67960 pages shared
124940 pages non-shared

 

When(什么时候出现):

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

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

首先看一下call trace调用关系:

             __alloc_pages_nodemask分配内存 -> 发现内存不足(或低于low memory)out_of_memory -> 选中一个得分最高的processor进行select_bad_process -> kill

/** 
 * out_of_memory – kill the "best" process when we run out of memory 
 */  
void out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask,  
        int order, nodemask_t *nodemask, bool force_kill)  
{  
    // 等待notifier调用链返回,如果有内存了则返回  
    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;  
    }  
  
    // 如果设置了sysctl的panic_on_oom,则内核直接panic  
    check_panic_on_oom(constraint, gfp_mask, order, mpol_mask);  
  
    // 如果设置了oom_kill_allocating_task  
    // 则杀死正在申请内存的process  
    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;  
    }  
  
    // 用select_bad_process()选择badness指  
    // 数(oom_score)最高的进程  
    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");  
    }  
    if (p != (void *)-1UL) {  
        // 查看child process, 是否是要被killed,则直接影响当前这个parent进程   
        oom_kill_process(p, gfp_mask, order, points, totalpages, NULL,  
                 nodemask, "Out of memory");  
        killed = 1;  
    }  
out:  
  
    if (killed)  
        schedule_timeout_killable(1);  
}  

 

select_bad_process() 调用oom_badness计算权值:

/** 
 * oom_badness - heuristic function to determine which candidate task to kill 
 *  
 */  
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,  
              const nodemask_t *nodemask, unsigned long totalpages)  
{  
    long points;  
    long adj;  
  
    // 内部判断是否是pid为1的initd进程,是否是kthread内核进程,是否是其他cgroup,如果是则跳过  
    if (oom_unkillable_task(p, memcg, nodemask))  
        return 0;  
  
    p = find_lock_task_mm(p);  
    if (!p)  
        return 0;  
  
    // 获得/proc/[pid]/oom_adj权值,如果是OOM_SCORE_ADJ_MIN则返回  
    adj = (long)p->signal->oom_score_adj;  
    if (adj == OOM_SCORE_ADJ_MIN) {  
        task_unlock(p);  
        return 0;  
    }  
  
    // 获得进程RSS和swap内存占用  
    points = get_mm_rss(p->mm) + p->mm->nr_ptes +  
         get_mm_counter(p->mm, MM_SWAPENTS);  
    task_unlock(p);  
  
    // 计算步骤如下,【计算逻辑比较简单,不赘述了】  
    if (has_capability_noaudit(p, CAP_SYS_ADMIN))  
        adj -= 30;  
    adj *= totalpages / 1000;  
    points += adj;  
  
    return points > 0 ? points : 1;  
  
  
}  

 

 

配置 OOM killer

当然,如果触发了OOM机制,系统会杀掉某些进程,那么什么进程会被处理掉呢?kernel提供给用户态的/proc下的一些参数:
1./proc/[pid]/oom_adj,该pid进程被oom killer杀掉的权重,介于 [-17,15](具体具体权重的范围需要查看内核确认)之间,越高的权重,意味着更可能被oom killer选中,-17表示禁止被kill掉。

通过2个步骤可以确认,具体权重的范围:

①uname -a查看Linux内核版本

②进入/usr/src/kernels/内核版本/include/linux/oom.h确认具体的权重范围

Linux内核OOM机制 - 第1张  | 刘建广学习笔记
2./proc/[pid]/oom_score,当前该pid进程的被kill的分数,越高的分数意味着越可能被kill,这个数值是根据oom_adj运算(2ⁿ,n就是oom_adj的值)后的结果。

oom_adj,oom_score是oom_killer的主要参考值

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

参数/proc/sys/vm/overcommit_memory可以控制进程对内存过量使用的应对策略
a.当overcommit_memory=0 允许进程轻微过量使用内存,但对于大量过载请求则不允许,也就是当内存消耗过大就是触发OOM killer。
b.当overcommit_memory=1 永远允许进程overcommit,不会触发OOM killer。
c.当overcommit_memory=2 永远禁止overcommit,不会触发OOM killer。

 

1.保护我们重要的进程,避免被处理掉

实例:

ps -ef|grep GameServer(获得重要进程的PID)

echo -17 > /proc/PID/oom_score_adj(输入-17,禁止被OOM机制处理)

2.关闭OOM机制(不推荐,如果不启动OOM机制,内存使用过大,会让系统产生很多异常数据)

echo "vm.panic_on_oom=1" >> /etc/sysctl.conf

systcl -p

 

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

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

# cat /proc/981/oom_score
18

# echo -15 > /proc/981/oom_score_adj
# cat /proc/981/oom_score
3

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

# vi 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

# chmod +x oomscore.sh
# ./oomscore.sh
18   981 /usr/sbin/mysqld
 4 31359 -bash
 4 31056 -bash
 1 31358 sshd: root@pts/6
 1 31244 sshd: vpsee [priv]
 1 31159 -bash
 1 31158 sudo -i
 1 31055 sshd: root@pts/3
 1 30912 sshd: vpsee [priv]
 1 29547 /usr/sbin/sshd -D

 

最后编辑:
作者:liujg
真实-不弄虚,不做假,做自己,不违心; 踏实-不浮躁,不盲从,不急功,不近利; 实学-不投机,不取巧,勤于学,精于业。