前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入了解Linux OOM Killer:一次可怕的内核事件

深入了解Linux OOM Killer:一次可怕的内核事件

原创
作者头像
嵌入式Linux内核
发布2023-08-08 21:54:39
1.1K0
发布2023-08-08 21:54:39
举报

一、简介

The OOM Killer 是内核中的一个进程,当系统出现严重内存不足时,它就会启用自己的算法去选择某一个进程并杀掉. 之所以会发生这种情况,是因为Linux内核在给某个进程分配内存时,会比进程申请的内存多分配一些. 这是为了保证进程在真正使用的时候有足够的内存,因为进程在申请内存后并不一定立即使用,当真正使用的时候,可能部分内存已经被回收了。

比如 当一个进程申请2G内存时,内核可能会分配2.5G的内存给它.通常这不会导致什么问题.然而一旦系统内大量的进程在使用内存时,就会出现内存供不应求.很快就会导致内存耗尽. 这时就会触发这个oom killer,它会选择性的杀掉某个进程以保证系统能够正常运行。

二、OOM Killer

理解OOM Killer:

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

配置OOM Killer:

我们可以通过一些内核参数来调整 OOM killer 的行为,避免系统在那里不停的杀进程。

1)Linux下每个进程都有个OOM权重,在/proc/<pid>/oom_adj里面,取值是-17到+15,取值越高,越容易被干掉;

2)linux内核会通过特定的算法给每个进程计算一个分数来决定杀哪个进程,每个进程的oom分数可以/proc/PID/oom_score中找到(分数越高,越容易被干掉);

3)我们可以通过调控每个进程的/proc/<pid>/oom_adj来影响到每个进程的/proc/PID/oom_score;(正比例关系,oom_adj越大,oom_score分数越高,越容易被干掉)

当物理内存和交换空间都被用完时,如果还有进程来申请内存,内核将触发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默认的配置:

代码语言:javascript
复制
#OOM后不panic
dev@ubuntu:~$ cat /proc/sys/vm/panic_on_oom
0

#OOM后kill掉分数最高的进程
dev@ubuntu:~$ cat /proc/sys/vm/oom_kill_allocating_task
0

#进程由于OOM被kill掉后将生成core dump文件
dev@ubuntu:~$ cat /proc/sys/vm/oom_dump_tasks
1

#默认max core file size是0, 所以系统不会生成core文件
dev@ubuntu:~$ prlimit|grep CORE
CORE max core file size 0 unlimited blocks

#core dump文件的生成交给了apport,相关的设置可以参考apport的资料
dev@ubuntu:~$ cat /proc/sys/kernel/core_pattern
|/usr/share/apport/apport %p %s %c %P

panic_on_oom

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

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

代码语言:javascript
复制
#设置panic后3秒重启系统
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为例做个示范:

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

代码语言:javascript
复制
dev@ubuntu:~$ sudo sh -c "echo 2> /proc/sys/vm/panic_on_oom"

通过控制命令(重启后失效)

代码语言:javascript
复制
dev@dev:~$ sudo sysctl vm.panic_on_oom=2

修改配置文件(重启后继续生效)

代码语言:javascript
复制
#通过编辑器将vm.panic_on_oom=2添加到文件sysctl.conf中(如果已经存在,修改该配置项即可)
dev@dev:~$ sudo vim /etc/sysctl.conf

#重新加载sysctl.conf,使修改立即生效
dev@dev:~$ sudo sysctl -p

日志

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

代码语言:javascript
复制
dev@dev:~$ grep oom /var/log/syslog
Jan 23 21:30:29 dev kernel: [  490.006836] eat_memory invoked oom-killer: gfp_mask=0x24280ca, order=0, oom_score_adj=0
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行为略有不同。

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之类,这两个参数的默认值为unlimited,所以只要不修改它们的默认配置,限制就不会被触发。有一种极端情况需要注意,那就是代码写的有问题,超过了系统的虚拟地址空间范围,比如32位系统的虚拟地址空间范围只有4G,这种情况下不确定系统会以一种什么样的方式返回错误。

rlimit

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

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

代码语言:javascript
复制
#默认情况下,RLIMIT_AS和RLIMIT_DATA的值都是unlimited
dev@dev:~$ prlimit |egrep "DATA|AS"
AS         address space limit                unlimited unlimited bytes
DATA       max data size                      unlimited unlimited bytes

测试代码

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

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define M (1024 * 1024)
#define K 1024

int main(int argc, char *argv[])
{
    char *p;
    int size =0;
    while(1) {
        p = (char *)malloc(K);
        if  (p == NULL){
            printf("memory allocate failed!\n");
            return -1;
        }
        memset(p, 0, K);
        size += K;
        if (size%(100*M) == 0){
            printf("%d00M memory allocated\n", size/(100*M));
            sleep(1);
        }
    }

    return 0;
}

三、Linux内核OOM killer机制

【0】何时触发?

内核在触发OOM机制时会调用到out_of_memory()函数,此函数的调用顺序以下:

代码语言:javascript
复制
_alloc_pages  //内存分配时调用

    |-->__alloc_pages_nodemask

       |--> __alloc_pages_slowpath

           |--> __alloc_pages_may_oom

              | --> out_of_memory   //触发

以上函数__alloc_pages_may_oom()在调用以前会先判断oom_killer_disabled的值,若是有值,则不会触发OOM机制;github

布尔型变量oom_killer_disabled定义在文件mm/page_alloc.c中,并无提供外部接口更改此值,可是在内核中此值默认为0,表示打开OOM-kill。算法

Linux中内存都是以页的形式管理的,因此无论是怎么申请内存,都会调用alloc_page()函数,最终调用到函数out_of_memory(),触发OOM机制。

【1】内核监测到系统内存不足时,该函数被触发执行:

代码语言:javascript
复制
/** * out_of_memory - kill the "best" process when we run out of memory * @oc: pointer to struct oom_control * * If we run out of memory, we have the choice between either * killing a random task (bad), letting the system crash (worse) * OR try to be smart about which process to kill. Note that we * don't have to be perfect here, we just have to be good. */
bool out_of_memory(struct oom_control *oc) {
	unsigned long freed = 0;
	enum oom_constraint constraint = CONSTRAINT_NONE;

	if (oom_killer_disabled)
		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 current has a pending SIGKILL or is exiting, then automatically * select it. The goal is to allow it to allocate so that it may * quickly exit and free its memory. */
	if (task_will_free_mem(current)) {
		mark_oom_victim(current);
		wake_oom_reaper(current);
		return true;
	}

	/* * The OOM killer does not compensate for IO-less reclaim. * pagefault_out_of_memory lost its gfp context so we have to * make sure exclude 0 mask - all other users should have at least * ___GFP_DIRECT_RECLAIM to get here. */
	if (oc->gfp_mask && !(oc->gfp_mask & __GFP_FS))
		return true;

	/* * Check if there were limitations on the allocation (only relevant for * NUMA and memcg) that may require different handling. */
	constraint = constrained_alloc(oc);
	if (constraint != CONSTRAINT_MEMORY_POLICY)
		oc->nodemask = NULL;
	check_panic_on_oom(oc, constraint);

	if (!is_memcg_oom(oc) && 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?!?! */
	if (!oc->chosen) {
		dump_header(oc, NULL);
		pr_warn("Out of memory and no killable processes...\n");
		/* * If we got here due to an actual allocation at the * system level, we cannot survive this and will enter * an endless loop in the allocator. Bail out now. */
		if (!is_sysrq_oom(oc) && !is_memcg_oom(oc))
			panic("System is deadlocked on memory\n");
	}
	if (oc->chosen && oc->chosen != (void *)-1UL)
		oom_kill_process(oc, !is_memcg_oom(oc) ? "Out of memory" :
				 "Memory cgroup out of memory");
	return !!oc->chosen;
}

【2】选择一个“最坏的”进程

代码语言:javascript
复制
* * Simple selection loop. We choose the process with the highest number of * 'points'. In case scan was aborted, oc->chosen is set to -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;
}

【3】杀掉进程

代码语言:javascript
复制
static void oom_kill_process(struct oom_control *oc, const char *message) {
	struct task_struct *victim = oc->chosen;
	struct mem_cgroup *oom_group;
	static DEFINE_RATELIMIT_STATE(oom_rs, DEFAULT_RATELIMIT_INTERVAL, DEFAULT_RATELIMIT_BURST);

	/* * If the task is already exiting, don't alarm the sysadmin or kill * its children or threads, just give it access to memory reserves * so it can die quickly */
	task_lock(victim);
	if (task_will_free_mem(victim)) {
		mark_oom_victim(victim);
		wake_oom_reaper(victim);
		task_unlock(victim);
		put_task_struct(victim);
		return;
	}
	task_unlock(victim);

	if (__ratelimit(&oom_rs))
		dump_header(oc, victim);

	/* * Do we need to kill the entire memory cgroup? * Or even one of the ancestor memory cgroups? * Check this out before killing the victim task. */
	oom_group = mem_cgroup_get_oom_group(victim, oc->memcg);

	__oom_kill_process(victim, message);

	/* * If necessary, kill all tasks in the selected memory cgroup. */
	if (oom_group) {
		mem_cgroup_print_oom_group(oom_group);
		mem_cgroup_scan_tasks(oom_group, oom_kill_memcg_member,
				      (void*)message);
		mem_cgroup_put(oom_group);
	}
}

查看系统日志方法:

运行egrep -i -r 'killed process' /var/log命令,结果以下:

代码语言:javascript
复制
/var/log/syslog.1:May  6 10:02:51 iZuf66b59tpzdaxbchl3d4Z kernel: [1467990.340288] Killed process 17909 (procon) total-vm:5312000kB, anon-rss:4543100kB, file-rss:0kB

也可运行dmesg命令,结果以下:

代码语言:javascript
复制
[1471454.635492] Out of memory: Kill process 17907 (procon) score 143 or sacrifice child
[1471454.636345] Killed process 17907 (procon) total-vm:5617060kB, anon-rss:4848752kB, file-rss:0kB

显示可读时间的话可用dmesg -T查看:

代码语言:javascript
复制
[Wed May 15 14:03:08 2019] Out of memory: Kill process 83446 (machine) score 250 or sacrifice child
[Wed May 15 14:03:08 2019] Killed process 83446 (machine) total-vm:1920560kB, anon-rss:11

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 二、OOM Killer
  • 三、Linux内核OOM killer机制
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com