背景

linux内核有个机制叫OOM killer(Out-Of-Memory killer),当系统需要申请内存却申请不到时,OOM killer会检查当前进程中占用内存最大者,将其杀掉,腾出内存保障系统正常运行。

一般而言,一个应用的内存逐渐增加,肯定是不正常的,这个时候可认为该应用存在内存泄漏,当系统内存被占用到一定的时候,将会触发OOM,此时系统将会找一个最合适的进程杀掉,以释放内存。

系统如何查找最合适的进程杀掉此处不深入介绍,只介绍出现OOM时,现场如何分析。

另外补充一下,不一定是进程存在内存泄漏,可能是内核驱动、内存碎片化严重等问题都会触发OOM。

现场

在系统触发OOM时,都会有现场信息打印,例如下面的:

[41311.854276] udevd invoked oom-killer: gfp_mask=0x27080c0(GFP_KERNEL_ACCOUNT|__GFP_ZERO|__GFP_NOTRACK), nodemask=0, order=1, oom_score_adj=-1000
[41311.873871] COMPACTION is disabled!!!
[41311.878101] CPU: 0 PID: 1069 Comm: udevd Not tainted 4.9.191 #147
[41311.885079] Hardware name: arm
[41311.889103] [<c010d3cc>] (unwind_backtrace) from [<c010a51c>] (show_stack+0x10/0x14)
[41311.897984] [<c010a51c>] (show_stack) from [<c01b2be8>] (dump_header.constprop.4+0x7c/0x1c8)
[41311.907480] [<c01b2be8>] (dump_header.constprop.4) from [<c0176404>] (oom_kill_process+0xf0/0x4d0)
[41311.917732] [<c0176404>] (oom_kill_process) from [<c0176cac>] (out_of_memory+0x348/0x3f0)
[41311.927687] [<c0176cac>] (out_of_memory) from [<c017ae5c>] (__alloc_pages_nodemask+0x884/0x928)
[41311.937826] [<c017ae5c>] (__alloc_pages_nodemask) from [<c0116428>] (copy_process.part.3+0x160/0x1574)
[41311.948461] [<c0116428>] (copy_process.part.3) from [<c011798c>] (_do_fork+0xb0/0x358)
[41311.957436] [<c011798c>] (_do_fork) from [<c0117ca4>] (sys_fork+0x20/0x28)
[41311.966475] [<c0117ca4>] (sys_fork) from [<c0106bc0>] (ret_fast_syscall+0x0/0x54)
[41311.974953] Mem-Info:
[41311.977605] active_anon:1158 inactive_anon:25 isolated_anon:0
[41311.977605]  active_file:49363 inactive_file:59329 isolated_file:0
[41311.977605]  unevictable:0 dirty:1 writeback:0 unstable:0
[41311.977605]  slab_reclaimable:1446 slab_unreclaimable:1541
[41311.977605]  mapped:988 shmem:26 pagetables:53 bounce:0
[41311.977605]  free:2975 free_pcp:174 free_cma:0
[41312.014748] Node 0 active_anon:4632kB inactive_anon:100kB active_file:197452kB inactive_file:237316kB unevictable:0kB isolated(anon):0kB isolated(file):0kB mapped:3952kB dirty:4kB writeback:0kB shmem:104kB writeback_tmp:0kB unstable:0kB pages_scanned:0 all_unreclaimable? no
[41312.044605] Normal free:11900kB min:2832kB low:3540kB high:4248kB active_anon:4632kB inactive_anon:100kB active_file:197452kB inactive_file:237316kB unevictable:0kB writepending:4kB present:519680kB managed:504736kB mlocked:0kB slab_reclaimable:5784kB slab_unreclaimable:6164kB kernel_stack:592kB pagetables:212kB bounce:0kB free_pcp:692kB local_pcp:692kB free_cma:0kB
[41312.081972] lowmem_reserve[]: 0 0 0
[41312.086149] Normal: 1445*4kB (UMH) 629*8kB (UMH) 48*16kB (UMH) 4*32kB (MH) 3*64kB (M) 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 11900kB
[41312.101337] 108718 total pagecache pages
[41312.105844] 0 pages in swap cache
[41312.109615] Swap cache stats: add 0, delete 0, find 0/0
[41312.115499] Free swap  = 0kB
[41312.118813] Total swap = 0kB
[41312.122117] 129920 pages RAM
[41312.126429] 0 pages HighMem/MovableOnly
[41312.130968] 3736 pages reserved
[41312.134502] 0 pages cma reserved
[41312.138192] [ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name
[41312.148942] [  999]     0   999      306      133       4       0        0             0 adbd
[41312.158846] [ 1026]     0  1026      208      128       3       0        0             0 powerkey_daemon
[41312.169696] [ 1028]     0  1028      174      127       3       0        0             0 swupdate-progre
[41312.180456] [ 1044]     0  1044      262      199       3       0        0             0 sh
[41312.189932] [ 1066]     0  1066      330      181       4       0        0             0 dbus-daemon
[41312.201026] [ 1069]     0  1069      352      228       4       0        0         -1000 udevd
[41312.210849] [ 1109]     0  1109     2033      216       8       0        0             0 wifi_deamon
[41312.221334] [ 1129]     0  1129      575      167       4       0        0             0 wpa_supplicant
[41312.233161] [ 1159]     0  1159     5664     1681      17       0        0             0 playerdemo
[41312.243587] Out of memory: Kill process 1159 (playerdemo) score 13 or sacrifice child
[41312.253545] Killed process 1159 (playerdemo) total-vm:22656kB, anon-rss:3680kB, file-rss:3044kB, shmem-rss:0kB
Killed
root@ARM:/# 

以前看到上面的信息,都会觉得害怕,因为麻烦又来了。现在也怕,不过最起码可以先看懂它是怎么回事吧,下面一个个来分析。

触发源

[41311.854276] udevd invoked oom-killer: gfp_mask=0x27080c0(GFP_KERNEL_ACCOUNT|__GFP_ZERO|__GFP_NOTRACK), nodemask=0, order=1, oom_score_adj=-1000

上面这行log,我们可以知道,udevd这个进程触发了oom-killer机制,触发的时候,申请4KB(page size) * 2^order大小的内存(order=1,也就是8KB),内存也不大,相对比较正常。而申请内存时的标志是(GFP_KERNEL_ACCOUNT|__GFP_ZERO|__GFP_NOTRACK),也中规中矩,没什么异常,oom_score_adj则是在oom-killer决定杀掉那个进程时的权重,该值越大越容易被杀。继续往下看。

申请内存的标志信息如下:

__GFP_ZERO:返回已经帮忙清0的内存。

__GFP_NOTRACK:避免使用kmemcheck进行跟踪。

GFP_KERNEL:典型的内核内部分配。调用者需要ZONE_NORMAL或一个较低的区域来直接访问,但可以直接回收。

GFP_KERNEL_ACCOUNT:与GFP_KERNEL相同,不同的是分配被记为kmemcg。

堆栈

[41311.878101] CPU: 0 PID: 1069 Comm: udevd Not tainted 4.9.191 #147
[41311.885079] Hardware name: arm
[41311.889103] [<c010d3cc>] (unwind_backtrace) from [<c010a51c>] (show_stack+0x10/0x14)
[41311.897984] [<c010a51c>] (show_stack) from [<c01b2be8>] (dump_header.constprop.4+0x7c/0x1c8)
[41311.907480] [<c01b2be8>] (dump_header.constprop.4) from [<c0176404>] (oom_kill_process+0xf0/0x4d0)
[41311.917732] [<c0176404>] (oom_kill_process) from [<c0176cac>] (out_of_memory+0x348/0x3f0)
[41311.927687] [<c0176cac>] (out_of_memory) from [<c017ae5c>] (__alloc_pages_nodemask+0x884/0x928)
[41311.937826] [<c017ae5c>] (__alloc_pages_nodemask) from [<c0116428>] (copy_process.part.3+0x160/0x1574)
[41311.948461] [<c0116428>] (copy_process.part.3) from [<c011798c>] (_do_fork+0xb0/0x358)
[41311.957436] [<c011798c>] (_do_fork) from [<c0117ca4>] (sys_fork+0x20/0x28)
[41311.966475] [<c0117ca4>] (sys_fork) from [<c0106bc0>] (ret_fast_syscall+0x0/0x54)

上面的信息则是代表发生OOM时,CPU的堆栈信息,从上面大概知道申请内存时触发OOM了,打印出来的堆栈位置也很常规,内核的函数,不是设备驱动,应该问题不大,继续往下。

相信内核不相信驱动:看堆栈信息时,确认最后堆栈的位置,确认是内核的常规函数还是某些驱动的函数,如果是驱动的,需要看看该驱动是否刚好有问题。

内存信息

看完堆栈之后就到触发OOM时的系统内存信息了,这个很关键。

[41311.977605] active_anon:1158 inactive_anon:25 isolated_anon:0
[41311.977605]  active_file:49363 inactive_file:59329 isolated_file:0
[41311.977605]  unevictable:0 dirty:1 writeback:0 unstable:0
[41311.977605]  slab_reclaimable:1446 slab_unreclaimable:1541
[41311.977605]  mapped:988 shmem:26 pagetables:53 bounce:0
[41311.977605]  free:2975 free_pcp:174 free_cma:0

anon,属于进程的数据,如 Stacks、Heaps 等。

active_anon:活动内存,1158 pages,也就是4632kB;

inactive_anon:非活动内存,25 pages,也就是100kB;

isolated_anon:从anon lru临时隔离页面

缓存存储器存储当前保存在内存中的磁盘数据。

active_file:活动内存,49363 pages,也就是197452kB;

inactive_file:非活动内存,59329 pages,也就是237316kB;

isolated_file:

slab_reclaimable:系统可回收内存,1446 pages,5784kB;

slab_unreclaimable:系统不可回收内存,1541 pages,6164kB;

mapped:映射的文件页,988 pages,3952kB;(比如share memory、动态库、mmap 等都统计在内)

shmem:share memory与tmpfs等内存,同free命令的shared,26 pages,104kB;

pagetables:页表内存,用于将虚拟地址翻译为物理地址,53 pages,212kB;

bounce:

free:空闲内存,2975 pages,11900kB;

free_pcp:CPU所占用的高速缓存内存,174 pages,696kB;

free_cma:空闲cma内存大小;

文件页:内存回收也就是系统释放掉可以回收的内存,比如缓存和缓冲区就属于可回收内存。在内存管理中,通常叫文件页,大部分文件页都可以直接回收,后续有需要再重磁盘重新读取即可(如属于进程的代码段等)。

脏页:被应用程序修改过,并暂时还没有写入磁盘的数据,得先写入磁盘才可以进行内存释放。

文件映射页:除了缓存和缓冲区,通过内存映射获取的文件映射页,也是一种常见的文件页。它也可以被释放掉,下次再访问的时候,从文件重新读取。

匿名页:应用程序动态分配的堆内存称为匿名页(Anonymous Page)。
[41312.014748] Node 0 active_anon:4632kB inactive_anon:100kB active_file:197452kB inactive_file:237316kB unevictable:0kB isolated(anon):0kB isolated(file):0kB mapped:3952kB dirty:4kB writeback:0kB shmem:104kB writeback_tmp:0kB unstable:0kB pages_scanned:0 all_unreclaimable? no
[41312.044605] Normal free:11900kB min:2832kB low:3540kB high:4248kB active_anon:4632kB inactive_anon:100kB active_file:197452kB inactive_file:237316kB unevictable:0kB writepending:4kB present:519680kB managed:504736kB mlocked:0kB slab_reclaimable:5784kB slab_unreclaimable:6164kB kernel_stack:592kB pagetables:212kB bounce:0kB free_pcp:692kB local_pcp:692kB free_cma:0kB
[41312.081972] lowmem_reserve[]: 0 0 0
[41312.086149] Normal: 1445*4kB (UMH) 629*8kB (UMH) 48*16kB (UMH) 4*32kB (MH) 3*64kB (M) 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 11900kB
[41312.101337] 108718 total pagecache pages
[41312.105844] 0 pages in swap cache
[41312.109615] Swap cache stats: add 0, delete 0, find 0/0
[41312.115499] Free swap  = 0kB
[41312.118813] Total swap = 0kB
[41312.122117] 129920 pages RAM
[41312.126429] 0 pages HighMem/MovableOnly
[41312.130968] 3736 pages reserved
[41312.134502] 0 pages cma reserved

上面的信息前面介绍的类似,可以更清晰的看到发生OOM时,各类内存信息。从上面可以清晰的看到,OOM时,active_file + inactive_file 共424MB,而系统的总内存是 129920 pages RAM 共512MB,显然,这个 active_file + inactive_file 是导致系统OOM的原因。

其他

[41312.138192] [ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name
[41312.148942] [  999]     0   999      306      133       4       0        0             0 adbd
[41312.158846] [ 1026]     0  1026      208      128       3       0        0             0 powerkey_daemon
[41312.169696] [ 1028]     0  1028      174      127       3       0        0             0 swupdate-progre
[41312.180456] [ 1044]     0  1044      262      199       3       0        0             0 sh
[41312.189932] [ 1066]     0  1066      330      181       4       0        0             0 dbus-daemon
[41312.201026] [ 1069]     0  1069      352      228       4       0        0         -1000 udevd
[41312.210849] [ 1109]     0  1109     2033      216       8       0        0             0 wifi_deamon
[41312.221334] [ 1129]     0  1129      575      167       4       0        0             0 wpa_supplicant
[41312.233161] [ 1159]     0  1159     5664     1681      17       0        0             0 playerdemo
[41312.243587] Out of memory: Kill process 1159 (playerdemo) score 13 or sacrifice child
[41312.253545] Killed process 1159 (playerdemo) total-vm:22656kB, anon-rss:3680kB, file-rss:3044kB, shmem-rss:0kB
Killed
root@ARM:/#

上面的信息则是系统计算应该kill哪个进程时的信息,最终 score 高的被kill,同时会限制被kill进程的内存信息,从上面的信息看,playerdemo占用的内存正常。

分析

出现OOM,可按下面的步骤进行分析:

  1. 先看触发OOM时申请内存的信息。
    例如上面的通过 __alloc_pages_nodemask申请的,gfp_mask 标志都是比较普通常见的。
    同时申请的大小是 order=1,也就8KB的大小,也不是大块内存,正常。
  2. 看现场的内存信息分布。
    通过看OOM时,内存是如何分布的。
    如果是 slab_unreclaimable 异常,不可回收内存比较大,可按照 slab 内存泄漏进行排查。
    如果是应用 rss 异常则按照应用内存泄漏。
    其他可考虑内存碎片化。

slab内存泄漏

如果在老化测试中发现系统的 slab_unreclaimable 持续增长,一般可理解为系统存在内存泄漏。此时可以按照下面的操作进行。

内核配置 CONFIG_SLUB_DEBUG 和 CONFIG_SLUB_DEBUG_ON 这两个配置,然后内核还需要增加下面的补丁:

diff --git a/mm/slub.c b/mm/slub.c
index 1600670e89ca..6f494b995092 100644
--- a/mm/slub.c
+++ b/mm/slub.c
@@ -4524,6 +4524,7 @@ static int list_locations(struct kmem_cache *s, char *buf,
        unsigned long *map = kmalloc(BITS_TO_LONGS(oo_objects(s->max)) *
                                     sizeof(unsigned long), GFP_KERNEL);
        struct kmem_cache_node *n;
+       bool print_buf = false;

        if (!map || !alloc_loc_track(&t, PAGE_SIZE / sizeof(struct location),
                                     GFP_TEMPORARY)) {
@@ -4551,8 +4552,16 @@ static int list_locations(struct kmem_cache *s, char *buf,
        for (i = 0; i < t.count; i++) {
                struct location *l = &t.loc[i];

-               if (len > PAGE_SIZE - KSYM_SYMBOL_LEN - 100)
-                       break;
+               if (len > PAGE_SIZE - KSYM_SYMBOL_LEN - 100) {
+                       print_buf = true;
+                       pr_err("%s\n", buf);
+                       len = 0;
+               }
+
+               /* ignore minority data */
+               if (l->count < 100)
+                       continue;
+
                len += sprintf(buf + len, "%7ld ", l->count);

                if (l->addr)
@@ -4591,6 +4600,12 @@ static int list_locations(struct kmem_cache *s, char *buf,

                len += sprintf(buf + len, "\n");
        }
+       if (print_buf) {
+               pr_info("%s\n", buf);
+               len = sprintf(buf, "sysfs node buffer size is PAGE_SIZE.");
+               len += sprintf(buf + len, "The message is more than 1 page.\n");
+               len += sprintf(buf + len, "Please get the message by kmsg\n");
+       }

        free_loc_track(&t);
        kfree(map);

上面的补丁避免在查看内存节点信息时打印不完整的问题,同时也忽略申请较少的内存对象。

在增加上面的补丁之后,将可以开始测试。在测试之前,执行 cat /proc/slabinfo 记录测试前的slabinfo,接着开始老化测试,测试到足够的时间之后,再次执行 cat /proc/slabinfo,然后对比前后两次的slabinfo,留意不同 slab object 的 num_objs 数值,如果前后明显存在差异的,则是该对象的使用存在内存泄漏(足够的测试时间是指已经明显看到系统内存有泄漏了的时间)。

假设说通过上面的操作,我们发现是 kmalloc-128 这个对象的申请释放存在泄漏,则可执行 cat /sys/kernel/slab/kmalloc-128/alloc_calls,确认都有哪些函数申请了 kmalloc-128,数值明显差异大的为怀疑对象(也可以对比测试前的情况)。

有了前面的基础,基本了解到了哪个内核函数的频繁调用导致了内存泄漏,但是该函数是被谁调用的还是不够清晰。此时可以在被调用的内存函数中增加调用 dump_stack(),使在调用该函数时打印堆栈,从而知道被谁调用(dump_stack()函数的调用可以增加判断条件,比如进入该函数多少次之后才会打印堆栈,避免过大的打印堆栈从而影响系统正常运行)。

来到这里的时候,已经是知道哪个驱动的函数调用导致内存泄漏,再跟进实际的情况修复即可。

应用内存泄漏

当出现了OOM,先确认现场是不是应用被kill时占用很大的内存(看rss),如果是,则属于应用内存泄漏,可通过 valgrind 工具进行排查。

valgrind 工具使用时,编译之后的可执行程序不能 strip,编译增加参数 -g,同时可以使用 valgrind --leak-check=full --show-reachable=yes TEST_CMD 进行分析,留意 /proc/PID/fd/ 下是否一直增长以及申请内存使用完之后是否有及时释放。

内核内存泄漏

内核内存泄漏可通过 kmemleak 工具进行检查。可查看 内核检查内存泄漏的工具 — kmemleak

其他 OOM 情况

出现 OOM 时,系统的 slab_unreclaimable 正常,而程序占用的 rss 也正常,那么可以考虑内存碎片导致系统申请内存时无法分配内存。可以关注系统的 /proc/pagetypeinfo 节点信息,系统内存碎片化严重的时候空闲内存全部集中在order=0(也就是最大的空闲连续内存块是一页)的情况。

这种情况下,确认是否没有开启内核的 CONFIG_COMPACTION 配置,该配置实际上是使用内核的compaction功能。类似于磁盘碎片整理:把碎的页移动整合到连续的一段空间,就留出一段连续的内存了,要注意它只能整理可移动的页面。

本文的例子log就是由于内核没有使能 CONFIG_COMPACTION 导致的内存碎片化严重,触发OOM。

有意思的文章

Linux中匿名页的访问分析

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐