Physical Address:
ChongQing,China.
WebSite:
在日常工作中,偶尔会遇到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_ptes | Page table entries |
swapents | Page table entries |
oom_score_adj | oom得分,分值越高越容易被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,希望这篇文章能够帮助到大家,我们下次再见。