Android/Linux系统OOM问题解析

简单解析Android/Linux系统中OOM机制。
Views: 840
3 0
Read Time:6 Minute, 41 Second

在日常工作中,偶尔会遇到OOM的问题,这类问题是我们日常开发中最难以处理最难以定位的一类问题。尤其是在Android Native开发时,我们很难通过有效的手段去诊断这类问题。更可悲的是,国内的技术平台中关于这块儿的问题讲解也很少。恰巧最近遇到了一个OOM的问题,在一边解决问题一边学习的过程中积累了些许知识,于是想到把这些记录下来,希望能给遇到类似问题的朋友一些帮助。

原理简介

OOM全称为OutOfMemeory,是指当应用程序无法申请到足够内存进而导致系统异常的问题。在Android系统中,基于OOM衍生出了Low Memory Killer,而Linux系统对应的为Out Of Memory Killer.Android系统的Low Memory Killer与Linux系统中的Out Of Memory Killer在策略上有细微不同,简单来说:Low Memory Killer会周期性检查系统的内存,提前Kill掉得分较高的应用以确保内存使用处于健康的状态,而Out Of Memory Killer则是在分配内存不足时Kill掉得分最高的应用,而此时系统可能已经处于异常状态了。

关于Android系统中的Low Memory Killer,源码路径为:kernel/drivers/misc/lowmemorykiller.c ,具体代码细节本文中不做过多讲解,感兴趣的朋友可以自行查阅源码。

在Android系统中,Low Memory Killer会依照两个原则来判定是否需要Kill掉某个进程从而来释放其资源。即:进程的重要性与释放该进程可获取的空闲的内存空间大小。

1)进程的重要性:进程的重要性将通过获取task_struct结构体中的oom_score_adj成员来进行判定。核心代码如下:

struct task_struct *p;
		int oom_score_adj;
		if (tsk->flags & PF_KTHREAD)
			continue;
		p = find_lock_task_mm(tsk);
		if (!p)
			continue;
		if (test_tsk_thread_flag(p, TIF_MEMDIE) &&
		    time_before_eq(jiffies, lowmem_deathpending_timeout)) {
			task_unlock(p);
			rcu_read_unlock();
			return 0;
		}
		oom_score_adj = p->signal->oom_score_adj;

2)可获取的空闲的内存空间大小:这部分的内容将通过get_mm_rss API进行获取。

在获取上述两个元素后,系统会进行判定,如判定满足满足条件后,将会发送信号终止进程。

关键代码如下:

	if (selected) {
		lowmem_print(1, "send sigkill to %d (%s), adj %d, size %d\n",
			     selected->pid, selected->comm,
			     selected_oom_score_adj, selected_tasksize);
		lowmem_deathpending_timeout = jiffies + HZ;
		send_sig(SIGKILL, selected, 0);
		set_tsk_thread_flag(selected, TIF_MEMDIE);
		rem -= selected_tasksize;
	}
	lowmem_print(4, "lowmem_shrink %lu, %x, return %d\n",
		     sc->nr_to_scan, sc->gfp_mask, rem);

关于这部分的全部代码,摘录如下:

static int lowmem_shrink(struct shrinker *s, struct shrink_control *sc)
{
	struct task_struct *tsk;
	struct task_struct *selected = NULL;
	int rem = 0;
	int tasksize;
	int i;
	int min_score_adj = OOM_SCORE_ADJ_MAX + 1;
	int selected_tasksize = 0;
	int selected_oom_score_adj;
	int array_size = ARRAY_SIZE(lowmem_adj);

    //获取当前剩余内存大小
	int other_free = global_page_state(NR_FREE_PAGES);
	int other_file = global_page_state(NR_FILE_PAGES) -
						global_page_state(NR_SHMEM);
	tune_lmk_param(&other_free, &other_file, sc);

    //获取lowmem_adj和lowmem_minfree数组大小
	if (lowmem_adj_size < array_size)
		array_size = lowmem_adj_size;
	if (lowmem_minfree_size < array_size)
		array_size = lowmem_minfree_size;
	for (i = 0; i < array_size; i++) {
		if (other_free < lowmem_minfree[i] &&
		    other_file < lowmem_minfree[i]) {
			min_score_adj = lowmem_adj[i];
			break;
		}
	}
	if (sc->nr_to_scan > 0)
		lowmem_print(3, "lowmem_shrink %lu, %x, ofree %d %d, ma %d\n",
				sc->nr_to_scan, sc->gfp_mask, other_free,
				other_file, min_score_adj);
	rem = global_page_state(NR_ACTIVE_ANON) +
		global_page_state(NR_ACTIVE_FILE) +
		global_page_state(NR_INACTIVE_ANON) +
		global_page_state(NR_INACTIVE_FILE);
	if (sc->nr_to_scan <= 0 || min_score_adj == OOM_SCORE_ADJ_MAX + 1) {
		lowmem_print(5, "lowmem_shrink %lu, %x, return %d\n",
			     sc->nr_to_scan, sc->gfp_mask, rem);
		return rem;
	}
	selected_oom_score_adj = min_score_adj;
	rcu_read_lock();
	for_each_process(tsk) {
		struct task_struct *p;
		int oom_score_adj;
		if (tsk->flags & PF_KTHREAD)
			continue;
		p = find_lock_task_mm(tsk);
		if (!p)
			continue;
		if (test_tsk_thread_flag(p, TIF_MEMDIE) &&
		    time_before_eq(jiffies, lowmem_deathpending_timeout)) {
			task_unlock(p);
			rcu_read_unlock();
			return 0;
		}
		oom_score_adj = p->signal->oom_score_adj;

       //如果得分小于设定值则忽略
		if (oom_score_adj < min_score_adj) {
			task_unlock(p);
			continue;
		}

        //获取进程所占用的Resident Set Size大小
        //需要注意是的包含了共享库占用的共享内存
		tasksize = get_mm_rss(p->mm);
		task_unlock(p);
		if (tasksize <= 0)
			continue;

        //选择oom_score_adj得分最高且rss内存最大的进程
		if (selected) {
			if (oom_score_adj < selected_oom_score_adj)
				continue;
			if (oom_score_adj == selected_oom_score_adj &&
			    tasksize <= selected_tasksize)
				continue;
		}
		selected = p;
		selected_tasksize = tasksize;
		selected_oom_score_adj = oom_score_adj;
		lowmem_print(2, "select %d (%s), adj %d, size %d, to kill\n",
			     p->pid, p->comm, oom_score_adj, tasksize);
	}

    //选中之后发送SIGKILL信号强制KILL进程
	if (selected) {
		lowmem_print(1, "send sigkill to %d (%s), adj %d, size %d\n",
			     selected->pid, selected->comm,
			     selected_oom_score_adj, selected_tasksize);
		lowmem_deathpending_timeout = jiffies + HZ;
		send_sig(SIGKILL, selected, 0);
		set_tsk_thread_flag(selected, TIF_MEMDIE);
		rem -= selected_tasksize;
	}
	lowmem_print(4, "lowmem_shrink %lu, %x, return %d\n",
		     sc->nr_to_scan, sc->gfp_mask, rem);
	rcu_read_unlock();
	return rem;
}

需要注意的是,当发送信号KILL进程中发送的信号为SIGKILL(signal 9)是无法被捕捉的,也就是说进程无法通过自行捕获信号来逃过被强杀的命运。

在上述代码中,我们还需要关注的是两个数组:lowmem_minfree数组与lowmem_adj数组,前者存放内容为minfree的警戒值,而后者存放着oom_adj的阈值。

oom_adj的取值范围为[-16,15],数值越大,优先级越低,越容易被杀掉。

oom_score_adj的取值范围为[-1000,1000],数值越大得分越高越容易被杀。

日志解析

接下来我们将通过一份问题日志来看看该如何去解读我们的OOM日志。

Apr 14 14:13:32 sha2uvp-gert01 kernel: telegraf invoked OOM-killer: gfp_mask=0x201da, order=0, oom_score_adj=0
.............
Apr 14 14:13:36 sha2uvp-gert01 kernel: Free swap  = 0kB
Apr 14 14:13:36 sha2uvp-gert01 kernel: Total swap = 8191996kB
Apr 14 14:13:36 sha2uvp-gert01 kernel: 9437070 pages RAM
Apr 14 14:13:36 sha2uvp-gert01 kernel: 0 pages HighMem/MovableOnly
............
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ pid ]   uid  tgid total_vm      rss nr_ptes swapents oom_score_adj name
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  682]     0   682    11356        7      25      417         -1000 systemd-udevd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  685]     0   685    68064       71      32       82             0 lvmetad
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  865]     0   865    13877       31      26       83         -1000 auditd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  889]     0   889     6594       48      18       37             0 systemd-logind
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  890]     0   890     5421       55      15       47             0 irqbalance
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  891]    81   891    14549       77      32       86          -900 dbus-daemon
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  894]   999   894   134633      113      60     1542             0 polkitd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  917]     0   917   119059      209      87      378             0 NetworkManager
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  918]     0   918    24911        0      42      402             0 VGAuthService
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  919]     0   919    74673      264      59      221             0 vmtoolsd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  937]     0   937    31571       27      20      130             0 crond
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  944]     0   944     6476        0      18       52             0 atd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [  948]     0   948    27522        1      10       30             0 agetty
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 1862]  1005  1862    28426        2      15      203             0 watchdog
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 1869]  1005  1869    28457        2      13      206             0 watchdog
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 2180]     0  2180    28202       28      57      232         -1000 sshd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 2190]     0  2190   143452      719      96     2592             0 tuned
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 2198]     0  2198    28924       24      13       19             0 rhsmcertd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 2200]     0  2200     6791       16      18       47             0 xinetd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 2212]     0  2212    62286      499      49      131             0 rsyslogd
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 2223]  1005  2223  1983226    54375     364    52615             0 java
Apr 14 14:13:36 sha2uvp-gert01 kernel: [ 2231]  1005  2231  7698184   381153    2212   328643             0 java
*********
Apr 14 14:13:36 sha2uvp-gert01 kernel: Out of memory: Kill process 6033 (java) score 367 or sacrifice child
Apr 14 14:13:36 sha2uvp-gert01 kernel: Killed process 6033 (java) total-vm:29930040kB, anon-rss:10625048kB, file-rss:0kB, shmem-rss:24kB

关于这份日志,纵向来看,分为如下几列:

pid进程id
uid用户id
tgid线程组id
total_vm总的虚拟内存使用(4KB/页为计量单位)
rss总的物理内存使用 (4KB/页为计量单位)
nr_ptesPage table entries
swapentsPage table entries
oom_score_adjoom得分,分值越高越容易被Kill

在这份日志中我们可以得到以下信息:

telegraf invoked OOM-killer 表明是由于 telegraf 申请不到足够的内存触发了OOM机制。

总的RAM大小: 9437070 pages RAM ,换算一下是9437070*4÷1024÷1024=36GB。

总的SWAP大小: 8191996kB ,也就是8.2GB。

现在我们可以关注一下各个进程的资源使用情况,如进程ID为2231的进程:

total_vm: 7698184 ,换算一下是7698184*4÷1024÷1024=29GB,需要注意的是这部分计算的是虚拟内存。

rss:381153,换算一下是381153*4÷1024÷1024=1.45GB,而oom_score_adj也高达 381153。

这个内存使用本身是有些不正常的,但是根据日志,我们可以看到进程ID为6033的进程使用消耗更多,因此被KILL掉。

以上就是这篇博文的全部内容啦,作为开发人员还是要掌握OOM机制以及相关日志的解读,这样出现问题时才能进行分析诊断(进行甩锅)。不过这里也要提及一点,由于RSS本身包含共享库所占用的内存资源,当某个进程RSS数值异常判定为内存泄漏时,不一定表明是该进程自身的代码问题,也有可能是共享库发生的异常,这就需要我们开发人员仔细进行诊断了。

好了,话不多说,我是Coderfan,希望这篇文章能够帮助到大家,我们下次再见。

Happy
Happy
100 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %
FranzKafka95
FranzKafka95

极客,文学爱好者。如果你也喜欢我,那你大可不必害羞。

文章: 86

留下评论

您的电子邮箱地址不会被公开。 必填项已用*标注

zh_CNCN