内存泄露分析----native 层

概念

概念

在wikipedia这样解读内存泄漏的:

  • 在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

详情请看wikipedia内存泄漏。

内存不是无穷无尽的,是有限的,如果申请了,使用了,用完没有释放,那么这块内存就一直无法被重新利用,最后再申请内存就找不到空闲的了(称为out of memory,OOM),可能导致程序逻辑错误崩溃。

以下用一个简单的例子说明内存泄漏:

#include 
#include 

int main(int argc, char *argv[])
{
    char *p[1000];
    int i;

    for (i = 0; i < 1000; i++) {
        p[i] = malloc(i * 2 + 100);
        if (p[i])
            memset(p[i], 0, i * 2 + 100);
    }
    for (i = 0; i < 1000; i++) {
        printf("%p\n", p[i]);
    }
    return 0;
}

上面的程序malloc申请的内存直至退出都没有被释放,这个就是内存泄漏了。

分类

内存泄漏可以根据发生的方式来分类:

  • 常发性泄漏
    • 故障代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  • 偶发性泄漏
    • 故障代码只有在特定场景或操作过程下才会被执行。常发性和偶发性是相对的。对于特定场景,偶发性的也许就变成了常发性的。所以测试场景和方法对检测内存泄漏至关重要。
  • 一次性泄漏
    • 故障代码只会被执行一次或者由于逻辑的缺陷导致只有一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存。 
  • 隐式泄漏
    • 程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但对于daemon或需要长时间运行的程序来讲,不及时释放可能最终耗尽所有内存。

另外不同的软件层都有可能存在内存泄漏:kernel层、native层和java层。不同的软件层泄漏的原理都差不多。

危害

内存泄漏是较难检测的异常之一,除了常发性泄漏,其他都是难以检查到的。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为用户无法感觉到内存泄漏的存在,除非泄漏大量内存或一直累积直至消耗光内存,这时就有各种明显表现:

  • 性能渐渐变差,因为要做各种内存回收工作。
  • 直接出现逻辑错误崩溃,没有做好异常处理(error handling)。
  • 本身没有问题,却引起其他程序异常。

既然内存泄漏这么难处理,那么是否有办法自动回收,不需要编程人员管理申请的内存呢?有的,java语言就有内存回收机制(垃圾回收,gc)。

不过即使用java编写的程序也可能出现泄漏,后面会讲解java内存泄漏的原理和调试方法。

通用的调试方法

需要对每一块申请的内存做标记,记录调用栈等信息,然后监控内存用量信息,感觉已经超出正常的内存用量很多时(相差不大时不见得能查到问题),提取这些信息,然后分析信息,找出可能的泄漏点,检查代码,分析逻辑,修复问题。

 

 

通过malloc分配的内存泄漏

对于native内存泄漏,比较常见的一类是C堆内存泄漏,即调用malloc申请的内存没有及时释放造成的内存泄漏。

调试方法

1.对于O版本及之后的版本,打开android malloc debug机制复现问题

如何开启android malloc debug机制请参考

MediaTek On-Line > Quick Start > 踩内存专题分析 > native踩内存 > 调试方法(O版本及之后) 

2.对于N版本及之前的版本,打开malloc debug15机制复现问题

如何开启malloc debug15请参考:

MediaTek On-Line> Quick Start> 踩内存专题分析 > native踩内存 > 调试方法(N版本及之前版本)

2.检查是否存在内存泄漏。

打开mtklogger,不断通过adb shell procrank -u > procrank.txt查看当前系统内存的使用情况。当看到被监控的进程占用内存(USS字段)超过了正常值很多,则可能存在内存泄漏。

3. 得到该进程内存使用分布情况($pid为被监控进程pid)
(1)adb shell dumpsys meminfo $pid >meminfo_$pid.txt

(2)adb shell procmem $pid > procmem_$pid.txt

(3)adb shell kill -11 $pid>生成DB

 分析问题

 复现问题后使用E-Consulter分析,分析完coredump会提供分析报告。如果存在超过128M以上的泄漏,那么分析报告会提示如下:

 == C堆检查 ==
分配器: dlmalloc, 最多允许使用: 4GB, 最多使用: 188MB, 当前使用: 166MB, 泄露阈值: 128MB, 调试等级: 15
 
该堆已分配超过128MB (可能存在内存泄露), 以下列出分配最大尺寸和次数的调用栈:
大小: 1184字节, 已分配: 134604次
分配调用栈:
 libc_malloc_debug_leak.so 0x40D5ECD4() + 16
 libc_malloc_debug_leak.so leak_malloc() + 43
 libc.so malloc() + 18
 libsqlite.so sqlite3MemMalloc() + 14 
 libsqlite.so mallocWithAlarm() + 86 
 libsqlite.so sqlite3Malloc() + 16 
 ......
 == 栈结束 ==
 
大小: 1184字节, 已分配: 5149次
分配调用栈:
 libc_malloc_debug_leak.so 0x40D5ECD4() + 16
 libc_malloc_debug_leak.so leak_malloc() + 43
 libc.so malloc() + 18
 libsqlite.so sqlite3MemMalloc() + 14 
 libsqlite.so mallocWithAlarm() + 86 
 ......
 == 栈结束 ==
 
大小: 1048584字节, 已分配: 1次
分配调用栈:
 libc_malloc_debug_leak.so 0x40D5ECD4() + 16
 libc_malloc_debug_leak.so leak_malloc() + 43
 libc.so malloc() + 18
 libsqlite.so sqlite3MemMalloc() + 14 
 libsqlite.so mallocWithAlarm() + 86 
 libsqlite.so sqlite3Malloc() + 16 
 ......
 == 栈结束 ==

 剩下就是检查代码逻辑,修复问题了

 

通过mmap分配的内存泄漏

native进程申请内存的方法很多,除了可以通过传统的堆分配函数malloc分配内存,还可以直接通过mmap映射内存。对于通过mmap分配的内存泄漏该如何调试呢?

调试方法

1.打开mmap debug机制复现

在vendor/mediatek/proprietary/external/aee/config_external/init.aee.customer.rc添加:

  on init

    export LD_PRELOAD libsigchain.so:libudf.so:...

"..."是原先LD_PRELOAD的内容,如果原先LD_PRELOAD没有内容,则":..."就不需要了。

重新打包bootimage并下载开机, 用adb输入:

adb shell setprop persist.debug.mmap.program app_process
adb shell setprop persist.debug.mmap.config 0x22002010
adb shell setprop persist.debug.mmap 1
adb reboot

注意:此类问题必须要抓到coredump,如何开启coredump请参考FAQ:[FAQ18022]L版本及之后的版本如何开启direct coredump? 

2.检查是否存在内存泄漏。

打开mtklogger,不断通过adb shell procrank -u > procrank.txt查看当前系统内存的使用情况。当看到被监控的进程占用内存(USS字段)超过了正常值很多,则可能存在内存泄漏。

3. 得到该进程内存使用分布情况($pid为被监控进程pid)
(1)adb shell dumpsys meminfo $pid >meminfo_$pid.txt

(2)adb shell procmem $pid > procmem_$pid.txt

(3)adb shell kill -11 $pid>生成DB

 分析问题

 复现问题后使用E-Consulter分析,分析完coredump会提供分析报告。如果存在超过200M以上的泄漏,那么分析报告会提示如下:

 == mmap泄漏检查 ==
anon mmap: 818732KB

mmap已分配超过200MB (可能存在内存泄露), 以下列出分配最大尺寸和次数的调用栈:
大小: 1040384字节, 已分配: 505次
分配调用栈:
 libudf.so mmap() + 138
 libc.so pthread_create() + 586
 libutils.so androidCreateRawThreadEtc() + 138
 libutils.so androidCreateThreadEtc() + 14
 libutils.so android::Thread::run() + 290
 libstagefright_foundation.so android::ALooper::start() + 390
 libmediandk.so 0x0000007F78EE4C24() + 162
 libsoundpool.so android::Sample::doLoad() + 890
 libsoundpool.so android::SoundPoolThread::doLoadSample() + 46
 libsoundpool.so android::SoundPoolThread::run() + 78
 == 栈结束 ==

大小: 104857600字节, 已分配: 1次
分配调用栈:
 libudf.so mmap() + 138
 libwebviewchromium_loader.so 0x0000007F72EA6FBC() + 38
 libart.so 0x0000007F7DA33E10() + 150
 ...... 0x00000000063FFFFA()
 == 栈结束 ==

大小: 1069056字节, 已分配: 61次
分配调用栈:
 libudf.so mmap() + 138
 libc.so pthread_create() + 586
 libart.so art::Thread::CreateNativeThread() + 406
 libart.so 0x0000007F7DA33E10() + 150
 /dev/ashmem/dalvik-zygote space (deleted) 0x000000007226DFB2()
 == 栈结束 ==

  剩下就是检查代码逻辑,修复问题了。

 

内存回收机制

kernel的内存泄漏和native类似,不过原生的kernel就有集成内存回收和调试的功能。
kernel设计的原则是不到万不得已就不做任何事情,比如内存尽量分配出去使用,实在没有内存才启动回收。

网络有些文章讲解内存回收机制,可以参考:

  • linux kernel内存回收机制

内存回收机制

内存水位

对于内存是否回收是针对每个zone考量的。而考量的参数有high、low和min,各个zone各一套,这3个参数的关系如下:

  • high > low > min
  • min以下的内存属于系统的保留内存,用以满足特殊使用,不会分配给用户态的申请

当内存可用量小于low时,kernel开始启动内核线程kswapd进行内存回收,当内存可用量降至min时,kernel直接进行直接回收(direct reclaim),即直接在应用程序的进程上下文中进行回收,再用回收上来的空闲页满足内存申请,因此会阻塞应用程序,带来一定的响应延迟,如果回收不到则会启动oom-killer通过杀程序回收内存,如果还是回收失败则会触发OOM。

kernel要么不回收内存,一旦开始回收内存,它将持续回收一直到可用的内存大于high。参考下图:

kswapd

kswapd是内存回收进程,且每个zone有一个,会定期监控和回收内存。

OOM killer

实在要不到内存时,则启动oom killer,就是通过杀死比较肥的进程来回收内存。

如何评判要杀哪个进程呢?就是要给进程打分,根据分数排行,选择杀哪个进程,这里有篇文章讲解,请参考:

  • OOM相关的参数

隐含的规则

too small to fail

用GFP_KERNEL申请低阶(< 3) 连续内存(8 个连续页)分配的话,就不会返回NULL,如果不够内存,会通过回收机制回收,如果回收不到直接panic。

 

 

泄漏分析判断

检测

OOM killer被启动时,会打印系统内存使用情况,有zone的情况,有swap的情况,等等。

以下举例OOM killer的log:

[ 92.101342] (4)[2246:Binder_1]Binder_1 invoked oom-killer: gfp_mask=0x200da, order=0, oom_score_adj=-705
[ 92.101379] (4)[2246:Binder_1]CPU: 4 PID: 2246 Comm: Binder_1 Tainted: P W O 3.18.22+ #1
[ 92.101384] (4)[2246:Binder_1]Hardware name: MT6797 (DT)
[ 92.101390] (4)[2246:Binder_1]Call trace:
[ 92.101405] (4)[2246:Binder_1][] dump_backtrace+0x0/0x15c
[ 92.101412] (4)[2246:Binder_1][] show_stack+0x10/0x1c
[ 92.101423] (4)[2246:Binder_1][] dump_stack+0x74/0xb8
[ 92.101432] (4)[2246:Binder_1][] dump_header.isra.14+0x78/0x1bc
[ 92.101439] (4)[2246:Binder_1][] oom_kill_process+0x2e4/0x468
[ 92.101446] (4)[2246:Binder_1][] out_of_memory+0x2fc/0x338
[ 92.101454] (4)[2246:Binder_1][] __alloc_pages_nodemask+0x8cc/0x8e0
[ 92.101462] (4)[2246:Binder_1][] read_swap_cache_async+0x10c/0x1f8
[ 92.101468] (4)[2246:Binder_1][] swapin_readahead+0x13c/0x1a8
[ 92.101476] (4)[2246:Binder_1][] handle_mm_fault+0x65c/0xa5c
[ 92.101485] (4)[2246:Binder_1][] do_page_fault+0x234/0x348
[ 92.101491] (4)[2246:Binder_1][] do_mem_abort+0x38/0x9c
......
[ 92.101571] (4)[2246:Binder_1]Mem-Info:
[ 92.101577] (4)[2246:Binder_1]DMA per-cpu:
[ 92.101582] (4)[2246:Binder_1]CPU 4: hi: 186, btch: 31 usd: 16
[ 92.101587] (4)[2246:Binder_1]CPU 5: hi: 186, btch: 31 usd: 34
[ 92.101592] (4)[2246:Binder_1]CPU 6: hi: 186, btch: 31 usd: 141
[ 92.101597] (4)[2246:Binder_1]CPU 7: hi: 186, btch: 31 usd: 33
[ 92.101603] (4)[2246:Binder_1]Movable per-cpu:
[ 92.101608] (4)[2246:Binder_1]CPU 4: hi: 186, btch: 31 usd: 0
[ 92.101614] (4)[2246:Binder_1]CPU 5: hi: 186, btch: 31 usd: 0
[ 92.101619] (4)[2246:Binder_1]CPU 6: hi: 186, btch: 31 usd: 30
[ 92.101625] (4)[2246:Binder_1]CPU 7: hi: 186, btch: 31 usd: 30
[ 92.101635] (4)[2246:Binder_1]active_anon:429 inactive_anon:395 isolated_anon:0
[ 92.101635] active_file:334 inactive_file:480 isolated_file:0
[ 92.101635] unevictable:606889 dirty:0 writeback:0 unstable:0
[ 92.101635] free:1939 slab_reclaimable:4521 slab_unreclaimable:11934
[ 92.101635] mapped:764 shmem:9 pagetables:4640 bounce:0
[ 92.101635] free_cma:226
[ 92.101654] (4)[2246:Binder_1]DMA free:6776kB min:4996kB low:25820kB high:27068kB active_anon:1324kB inactive_anon:868kB active_file:872kB inactive_file:1264kB unevictable:1907520kB isolated(anon):0kB isolated(file):0kB present:2418920kB managed:2338728kB mlocked:1901900kB dirty:0kB writeback:0kB mapped:2832kB shmem:20kB slab_reclaimable:18084kB slab_unreclaimable:47736kB kernel_stack:18288kB pagetables:18560kB unstable:0kB bounce:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:28744 all_unreclaimable? yes
[ 92.101660] (4)[2246:Binder_1]lowmem_reserve[]: 0 0 512
[ 92.101682] (4)[2246:Binder_1]Movable free:980kB min:1116kB low:5780kB high:6060kB active_anon:392kB inactive_anon:712kB active_file:464kB inactive_file:656kB unevictable:520036kB isolated(anon):0kB isolated(file):0kB present:524288kB managed:524288kB mlocked:520036kB dirty:0kB writeback:0kB mapped:224kB shmem:16kB slab_reclaimable:0kB slab_unreclaimable:0kB kernel_stack:0kB pagetables:0kB unstable:0kB bounce:0kB free_cma:904kB writeback_tmp:0kB pages_scanned:16848 all_unreclaimable? yes
[ 92.101687] (4)[2246:Binder_1]lowmem_reserve[]: 0 0 0
[ 92.101698] (4)[2246:Binder_1]DMA: 107*4kB (EMR) 21*8kB (UER) 10*16kB (UR) 6*32kB (R) 3*64kB (R) 1*128kB (R) 0*256kB 0*512kB 1*1024kB (R) 0*2048kB 1*4096kB (R) = 6388kB
[ 92.101742] (4)[2246:Binder_1]Movable: 239*4kB (C) 38*8kB (C) 6*16kB (C) 4*32kB (C) 1*64kB (C) 0*128kB 1*256kB (C) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 1804kB
[ 92.101784] (4)[2246:Binder_1]2636 total pagecache pages
[ 92.101789] (4)[2246:Binder_1]286 pages in swap cache
[ 92.101794] (4)[2246:Binder_1]Swap cache stats: add 206768, delete 206482, find 8339/19618
[ 92.101799] (4)[2246:Binder_1]Free swap = 970128kB
[ 92.101804] (4)[2246:Binder_1]Total swap = 1431504kB
[ 92.101808] (4)[2246:Binder_1]735802 pages RAM
[ 92.101813] (4)[2246:Binder_1]0 pages HighMem/MovableOnly
[ 92.101817] (4)[2246:Binder_1]20048 pages reserved
[ 92.101822] (4)[2246:Binder_1][ pid ] uid tgid total_vm rss nr_ptes swapents oom_score_adj name
......
[ 92.102022] (4)[2246:Binder_1][ 422] 0 422 7551 0 13 290 -1000 netd
[ 92.102056] (4)[2246:Binder_1][ 426] 1013 426 110324 0 127 22197 -1000 mediaserver
[ 92.102064] (4)[2246:Binder_1][ 427] 0 427 3549 8 5 141 -1000 installd
[ 92.102090] (4)[2246:Binder_1][ 432] 2000 432 3286 0 4 113 -1000 mobile_log_d
[ 92.102319] (4)[2246:Binder_1][ 465] 1000 465 29620 84 47 2022 -1000 program_binary_
[ 92.102327] (4)[2246:Binder_1][ 466] 1000 466 17093 0 29 913 -1000 em_svr
[ 92.102336] (4)[2246:Binder_1][ 467] 0 467 3344 3 5 82 -1000 stp_dump3
[ 92.102352] (4)[2246:Binder_1][ 1004] 2000 1004 4507 0 6 156 -1000 emdlogger1
[ 92.102360] (4)[2246:Binder_1][ 1005] 1001 1005 6331 0 10 129 -1000 gsm0710muxd
[ 92.102368] (4)[2246:Binder_1][ 1025] 2000 1025 4503 0 7 155 -1000 emdlogger3
[ 92.102385] (4)[2246:Binder_1][ 1202] 1001 1202 13363 0 24 555 -1000 mtkrild
[ 92.102422] (4)[2246:Binder_1][ 1762] 10030 1724 559698 1665 241 20279 -705 Binder_1
[ 92.102433] (4)[2246:Binder_1][ 1916] 1010 1916 5084 2 8 226 -1000 wpa_supplicant
[ 92.102456] (4)[2246:Binder_1][ 2091] 1001 2091 422449 0 123 11588 -705 om.mediatek.gba
[ 92.102464] (4)[2246:Binder_1][ 2097] 1001 2097 425070 0 131 11916 -705 om.mediatek.ims
[ 92.102511] (4)[2246:Binder_1][ 2159] 1000 2159 423010 0 124 11659 -705 atek.nlpservice
[ 92.102526] (4)[2246:Binder_1][ 2179] 1000 2179 445607 0 170 12468 -705 com.dolby
[ 92.102533] (4)[2246:Binder_1][ 2612] 1000 2612 6395 0 10 717 -1000 volte_stack
[ 92.102598] (4)[2246:Binder_1][ 6219] 2000 6219 608867 606056 1189 179 -1000 tewilovesyouy
[ 92.102630] (4)[2246:Binder_1] client( dbg_name) pid size address
[ 92.102635] (4)[2246:Binder_1]----------------------------------------------------
[ 92.102647] (4)[2246:Binder_1] ndroid.systemui( gralloc) 1724 14204928 0xffffffc02a72ff00
[ 92.102654] (4)[2246:Binder_1] m.android.phone( gralloc) 2084 13107200 0xffffffc058359840
[ 92.102668] (4)[2246:Binder_1] surfaceflinger( gralloc) 402 51232768 0xffffffc08aed5e40
[ 92.102675] (4)[2246:Binder_1] system_server( gralloc) 1254 13107200 0xffffffc092df0840
[ 92.102682] (4)[2246:Binder_1] display( from_kernel) 1 8630272 0xffffffc099a9a240
[ 92.102690] (4)[2246:Binder_1] disp_decouple( from_kernel) 116 6221824 0xffffffc09a620b40
[ 92.102697] (4)[2246:Binder_1] disp_decouple( from_kernel) 116 6221824 0xffffffc09a620cc0
[ 92.102705] (4)[2246:Binder_1] disp_decouple( from_kernel) 116 6221824 0xffffffc09a620e40
[ 92.102710] (4)[2246:Binder_1]----------------------------------------------------
[ 92.102714] (4)[2246:Binder_1]orphaned allocations (info is from last known client):
[ 92.102722] (4)[2246:Binder_1]----------------------------------------------------
[ 92.102727] (4)[2246:Binder_1] total orphaned 0
[ 92.102731] (4)[2246:Binder_1] total 83005440
[ 92.102736] (4)[2246:Binder_1]----------------------------------------------------
[ 92.102742] (4)[2246:Binder_1]7 PID Memory by Page
[ 92.102746] (4)[2246:Binder_1]7============================
[ 92.102751] (4)[2246:Binder_1]7 2084 936
[ 92.102756] (4)[2246:Binder_1]7 4765 5055
[ 92.102761] (4)[2246:Binder_1]7 2237 1078
[ 92.102765] (4)[2246:Binder_1]7 2200 199
[ 92.102770] (4)[2246:Binder_1]7 2013 189
[ 92.102774] (4)[2246:Binder_1]7 1724 1681
[ 92.102779] (4)[2246:Binder_1]7 1254 114
[ 92.102784] (4)[2246:Binder_1]7 465 114
[ 92.102789] (4)[2246:Binder_1]7 402 3903
[ 92.102793] (4)[2246:Binder_1]7============================
[ 92.102798] (4)[2246:Binder_1]7 Total 13269
[ 92.102802] (4)[2246:Binder_1]7============================
[ 92.102809] (4)[2246:Binder_1]Out of memory: Kill process 2003 (ek.voicecommand) score 0 or sacrifice child
[ 92.102821] (4)[2246:Binder_1]Killed process 2003 (ek.voicecommand) total-vm:1189876kB, anon-rss:0kB, file-rss:0kB

先讲解各个栏位的含义:

  • active_anon、inactive_anon:native匿名内存。
  • active_file、inactive_file:native file cache,如果OOM一般会释放file cache,基本不用看这个栏位。
  • unevictable:无法回收的内存,包含了mlocked的数据。
  • mlocked:native通过mlock()函数将页面锁住,禁止被swap出去。
  • slab_reclaimable、slab_unreclaimable:kernel slub内存。
  • kernel_stack:内核栈内存,通过这个数据可以算出kernel中存在多少进程。
    • ARM64里进程数 = kernel_stack / 16K,ARM里进程数= kernel_stack / 8K

拿到这样的log,首先判断下是kernel泄漏还是native泄漏。看哪个栏位的值最大。最大的栏位就知道是native还是kernel泄漏了。如果是native泄漏,还可以看下面的进程的rss栏位,最多的那个进程很可能存在native内存泄漏了。

如果是kernel泄漏,要如何调试呢?

 

 

调试方法

kmemleak

kernel 2.6.31引入的工具,用于检查内存泄漏。

原理

创建一个进程每10分钟(默认)扫描系统内存然后打印泄漏的内存的信息。

打开方法

在config(位于arch/arm64/configs/xxx_defconfig或arch/arm/configs/xxx_defconfig)里设定:

CONFIG_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE=1200
# CONFIG_DEBUG_KMEMLEAK_TEST is not set
# CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF is not set

或者用menuconfig配置:

  • Kernel Hacking> Kernel Memory Leak Detector
  • 然后调大Maximum kmemleak early log entires

检查

编译下载后开机查看是否存在/sys/kernel/debug/kmemleak,如果不存在需要手动挂载

mount -t debugfs nodev /sys/kernel/debug/

查看内存泄漏信息

cat /sys/kernel/debug/kmemleak

得到的信息如下:

unreferenced object 0xf9061000 (size 512):
comm "insmod", pid 12750, jiffies 14401507 (age 110.217s)
hex dump (first 32 bytes):
1c 0f 00 00 01 12 00 00 2a 0f 00 00 01 12 00 00 ........*.......
38 0f 00 00 01 12 00 00 bc 0f 00 00 01 12 00 00 8...............
backtrace:
< c10b0001> create_object+0x114/0x1db
< c148b4d0> kmemleak_alloc+0x21/0x3f
< c10a43e9> __vmalloc_node+0x83/0x90
< c10a44b9> vmalloc+0x1c/0x1e
< f9055021> myfunc+0x21/0x23 hello_kernel
< f9058012> 0xf9058012
< c1001226> do_one_initcall+0x71/0x113
< c1056c48> sys_init_module+0x1241/0x1430
< c100284c> sysenter_do_call+0x12/0x22
< ffffffff> 0xffffffff

使用

开始做测试,然后

第一次scan:

  • echo scan > sys/kernel/debug/kmemleak

然后

  • cat sys/kernel/debug/kmemleak

会得到很多backtrace,但是这其中有些是误抓的(kmemleak存在误报情况),然后

  • echo clear > sys/kernel/debug/kmemleak   清除log 

第二次scan:

  • echo scan > sys/kernel/debug/kmemleak

过段时间等待leak的积累,然后

  • cat sys/kernel/debug/kmemleak  

很多第一次误报的backtrace没有了,会得到很多重复的backtrace,假设这样的backtrace称为A

kmemleak的特征是A backtrace会越来越多,不断增长,而且这里就是泄漏的点

 

 

 

dev/ashmem memory leak

问题背景

用Launcher跑monkey时存在泄露,2个小时USS从65M涨到205

分析过程

打开debug 15复测,adb shell dumpsys meminfo查看launcher memory用量如果大于300M时,
(1)adb shell dumpsys meminfo 抓取一份launcher memory使用信息
(2)adb shell kill -11 生成DB
将memory信息和mtklog 一起提供过来。

在测试中用procrank查看uss一直在涨,但用dumpsys meminfo查看Native heap和Dalvik Heap,都涨的很慢,Native heap一直没有涨到128M :

用procrank抓取的信息如下,USS已经到达250M:
C:\Windows\System32>adb shell procrank | findstr launcher3
1698 2801540K 368192K 257240K 250476K com.android.launcher3 

用dumpsys抓取的信息如下:
C:\Windows\System32>adb shell dumpsys meminfo 1698
Applications Memory Usage (kB):
Uptime: 12807842 Realtime: 21690528 

** MEMINFO in pid 1698 [com.android.launcher3] **
Pss Private Private Swapped Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 29363 28016 0 0 78080 70795 7284
Dalvik Heap 7911 7768 0 0 64595 61187 3408
Dalvik Other 43303 43072 0 0
Stack 1244 1244 0 0
Ashmem 155002 154972 0 0
Other dev 4 0 4 0
.so mmap 2307 516 92 0
.apk mmap 1454 0 96 0
.ttf mmap 106 0 28 0
.dex mmap 1112 4 1108 0
.oat mmap 923 0 116 0
.art mmap 3106 2804 24 0
Other mmap 32 8 0 0
EGL mtrack 7989 7989 0 0
GL mtrack 44356 44356 0 0
Unknown 11308 10660 0 0
TOTAL 309520 301409 1468 0 142675 131982 10692

从dump出来的memory info可以看到,总共309520,也即300多M,其中 Ashmem   155002 ,就占了151M,这个应该才是leak的原因。

ashmem是通过mmap分配的内存,于是按照以下方法打开mmap debug机制再复测提供mtklog:

在vendor/mediatek/proprietary/external/aee/config_external/init.aee.customer.rc添加:
on init
export LD_PRELOAD libsigchain.so:libudf.so

重新打包bootimage并下载开机, 用adb输入:
adb shell setprop persist.debug.mmap.program app_process
adb shell setprop persist.debug.mmap.config 0x22002010
adb shell setprop persist.debug.mmap 1
adb reboot

解析再次提供的DB,用工具分析怀疑SoundPoolThread存在内存泄漏:

== mmap泄漏检查 ==
anon mmap: 800848KB

mmap已分配超过200MB (可能存在内存泄露), 以下列出分配最大尺寸和次数的调用栈:
大小: 1040384字节, 已分配: 497次
分配调用栈:
libudf.so mmap() + 138
libc.so __allocate_thread() + 454
libc.so pthread_create() + 586
libutils.so androidCreateRawThreadEtc() + 138
libutils.so androidCreateThreadEtc() + 14
libutils.so android::createThreadEtc() + 150
libutils.so android::Thread::run() + 290
libstagefright_foundation.so android::ALooper::start() + 342
libmediandk.so createAMediaCodec() + 162
libsoundpool.so decode() + 698
libsoundpool.so android::Sample::doLoad() + 890
libsoundpool.so android::SoundPoolThread::doLoadSample() + 46
libsoundpool.so android::SoundPoolThread::run() + 78
== 栈结束 ==

review 相关code并没有发现泄漏点,在backtrace中的ALooper.cpp中对应的内存分配和释放的地方添加log复现,从复现的NE db 显示,这个内存分配了 505 次:

mmap已分配超过200MB (可能存在内存泄露), 以下列出分配最大尺寸和次数的调用栈:
大小: 1040384字节, 已分配: 505次
分配调用栈:
libudf.so mmap() + 138
libc.so pthread_create() + 586
libutils.so androidCreateRawThreadEtc() + 138
libutils.so androidCreateThreadEtc() + 14
libutils.so android::Thread::run() + 290
libstagefright_foundation.so android::ALooper::start() + 390
libmediandk.so 0x0000007FA130DC24() + 162
libsoundpool.so android::Sample::doLoad() + 890
libsoundpool.so android::SoundPoolThread::doLoadSample() + 46
libsoundpool.so android::SoundPoolThread::run() + 78

从 mobilelog 里面,可以看到相应的 Alooper 确实是有 new 505 次
但对应的,其析构方法也是有跑了 505 次,

01-01 02:40:02.063867 2570 2596 D ALooper : 70615 con: AloopConCounter =505, AloopDesConCounter = 504
01-01 02:40:02.063967 2570 2596 D ALooper : 70615 con: LooperThreadConCounter=505, LooperThreadDesConCounter=504 //loopthread 构造方法内打印,第一个是构造次数,第二个是析构次数
01-01 02:40:02.680956 2570 2596 D ALooper : 70615 Descon: LooperThreadConCounter=505, LooperThreadDesConCounter=505
01-01 02:40:02.681063 2570 2596 D ALooper : 70615 Descon: AloopConCounter =505, AloopDesConCounter = 505 //loopthread 析构方法内打印,第一个是构造次数,第二个是析构次数

那么问题来了,为什么工具解析的结果是未释放呢? 从backtrace出发,__allocate_thread() + 454 对应的code:

attr->stack_base = __create_thread_mapped_space(mmap_size, attr->guard_size);

也即提示是attr->stack_base没有释放,查找代码看它会在哪里被mummap。

libutils.so androidCreateRawThreadEtc() + 138 可知是在androidCreateRawThreadEtc()函数中调用pthread_create,该函数对attr的设置如下:

  pthread_attr_t attr;
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

而对于PTHREAD_CREATE_DETACHED属性的线程所占的资源在pthread_exit 时自动释放,pthread_exit()中可以看到attr->stack_base的释放是通过

_exit_with_stack_teardown(thread->attr.stack_base, thread->mmap_size);

这个函数是汇编代码实现的,而mmap debug机制是通过rehook mmap&mummap 来监测内存的分配和释放,所以对于这种是没有办法监测到释放的过程,因此误判成leak。

重新回到分析报告,既然是ashmem占用内存过多,但是奇怪的是mmap泄漏检查显示的backtrace根本就没有与之相关的内存块,将所有mmap的backtrace列出来也没有看到,这又是怎么回事呢?
review mmap debug机制,mmap_debug.c中:

原因在这里,mmap debug机制只会记录flags为MAP_ANONYMOUS匿名映射的backtrace,而ashmem是有名字的,并不会记录,所以打开mmap debug机制对于检查ashmem leak的case并没有作用。

根据DB解析开的PROCESS_MAPS可以看到,/dev/ashmem/MemoryHeapBase占的内存过高,在MemoryHeapBase.cpp中加log打印申请和释放的backtrace。
从复现提供过来mobilelog结合DB解析开的PROCESS_FILE_STATE中的fd可知:
01-01 02:47:37.378898 1568 2358 D MemoryHeapBase: #00 pc 0000000000032220 /system/lib64/libbinder.so (android::MemoryHeapBase::MemoryHeapBase(unsigned long, unsigned int, char const*)+416)
01-01 02:47:37.378974 1568 2358 D MemoryHeapBase: #01 pc 0000000000006814 /system/lib64/libsoundpool.so (android::Sample::doLoad()+84)
01-01 02:47:37.379019 1568 2358 D MemoryHeapBase: #02 pc 0000000000008b08 /system/lib64/libsoundpool.so (android::SoundPoolThread::doLoadSample(int)+48)
01-01 02:47:37.379062 1568 2358 D MemoryHeapBase: #03 pc 0000000000008ba0 /system/lib64/libsoundpool.so (android::SoundPoolThread::run()+80)
01-01 02:47:37.379103 1568 2358 D MemoryHeapBase: #04 pc 0000000000093410 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+96)
01-01 02:47:37.379144 1568 2358 D MemoryHeapBase: #05 pc 0000000000014fec /system/lib64/libutils.so
01-01 02:47:37.379184 1568 2358 D MemoryHeapBase: #06 pc 00000000000673c8 /system/lib64/libc.so (__pthread_start(void*)+52)
01-01 02:47:37.379224 1568 2358 D MemoryHeapBase: #07 pc 000000000001ed44 /system/lib64/libc.so (__start_thread+16)
这个backtrace申请的 MemoryHeapBase最终都没有释放,review相关代码发现泄漏点正是这里。

根本原因

客制化代码写的有问题,造成sound 的resouce 无法释放。

解决方案

修改代码

 

你可能感兴趣的:(移动开发综合)