内存问题定位方法 - 内存泄漏
前言
Linux 内存是嵌入式开发人员,需要深入了解的计算机资源。合理的使用内存,有助于提升机器的性能和稳定性。 Linux下内存问题可分为内存泄漏,踩内存,内存溢出,内存碎片,性能调优等。本文主要介绍工作中常用的几类内存问题的原因以及常见排查方法和工具,希望对大家有所帮助。
Linux下经常遇到内存泄漏的问题,尤其对C/C++开发人员来说是一个亘古不变的话题,现在介绍解决Linux内存泄漏问题的方法层出不穷,让人眼花缭乱,但是作为开发人员应该从本质上了解为何会发生内存泄漏,在面对内存泄漏的问题时应当知道相关的技术细节,在解决问题时应当有个固定的排查思路,要善用Linux系统本身提供的工具来定位和解决,而不是一味的通过各种各样不常用的、不熟悉的工具来排查问题,这样不仅耗时,最终不一定能够解决问题。
内存泄漏指在程序运行过程中,分配的内存没有被正确释放,导致内存占用不断增加,最终耗尽系统的可用内存。定位内存泄漏问题通常需要使用内存分析工具,例如Valgrind、GDB或者专门用于内存泄漏检测的库,如mtrace。
wrap malloc
GUN链接器实际提供了一个好用的方法--wrap= symbol
。函数名定义为__wrap_symbol
,symbol也是一个函数,那么编译的时候如果添加了链接参数,函数调用symbol时,会调用到__wrap_symbol
函数,另外还有一个相关函数__real_symbol
,只声明不定义的时候,会对其调用到真正的symbol函数。
举一个简单的例子:
#define _GNU_SOURCE #include <string.h> #include <dlfcn.h> #include <stddef.h> #include <stdio.h> #include <execinfo.h> static void *(*real_malloc)(size_t) = NULL; static void *(*real_calloc)(size_t,size_t) = NULL; static void *(*real_realloc)(size_t,size_t) = NULL; static void (*real_free)(void *) = NULL; /*init 函数被标记为 __attribute__((constructor)) 属性,这表示它会在库加载时自动调用*/ static void __attribute__((constructor)) init(void) { /* dlfcn 库的 dlsym 函数获取原始 malloc 和 free 函数的地址 */ real_malloc = (void *(*)(size_t))dlsym(RTLD_NEXT, "malloc"); real_calloc = (void *(*)(size_t))dlsym(RTLD_NEXT, "calloc"); real_realloc = (void *(*)(size_t))dlsym(RTLD_NEXT, "realloc"); real_free = (void (*)(void *))dlsym(RTLD_NEXT,"free"); } void *malloc(size_t len) { static __thread int no_hook = 0; /*hook 是否启用 */ if (no_hook) { return (*real_malloc)(len); } /*__builtin_return_address(0) 获取调用者的地址 */ void * caller = (void *)(long)__builtin_return_address(0); /*将 no_hook 设置为1,以在 printf 调用中禁用进一步的挂钩,防止对 malloc 的递归调用和潜在的无限循环。*/ no_hook = 1; printf("malloc call from %p,len:%zu\n", caller, len); //printf call malloc internally void * ret = (*real_malloc)(len); return ret; } void *calloc(size_t len, size_t size) { static __thread int no_hook = 0; /*hook 是否启用 */ if (no_hook) { return (*real_calloc)(len, size); } /*__builtin_return_address(0) 获取调用者的地址 */ void * caller = (void *)(long)__builtin_return_address(0); /*将 no_hook 设置为1,以在 printf 调用中禁用进一步的挂钩,防止对 malloc 的递归调用和潜在的无限循环。*/ no_hook = 1; printf("calloc call from %p,len:%zu,size:%zu\n",caller, len, size); //printf call malloc internally no_hook = 0; void * ret = (*real_calloc)(len, size); return ret; } void *realloc(size_t len, size_t size) { static __thread int no_hook = 0; /*hook 是否启用 */ if (no_hook) { return (*real_realloc)(len, size); } /*__builtin_return_address(0) 获取调用者的地址 */ void * caller = (void *)(long)__builtin_return_address(0); /*将 no_hook 设置为1,以在 printf 调用中禁用进一步的挂钩,防止对 malloc 的递归调用和潜在的无限循环。*/ no_hook = 1; printf("realloc call from %p,len:%zu,size:%zu\n",caller, len, size); //printf call malloc internally no_hook = 0; void * ret = (*real_realloc)(len, size); return ret; } void free(void *ptr){ void * caller = (void *)(long)__builtin_return_address(0); printf("free call %p from %p\n", ptr, caller); (*real_free)(ptr); }
编译命令 gcc -g -O0 -fPIC -shared mymalloc.c -o libmymalloc.so -ldl
#include <stdio.h> #include <stdlib.h> int main(){ printf("main func addr:%p\n", main); printf("start malloc\n"); char * pc1 = malloc(10); char * pc2 = malloc(10); char * pc3 = calloc(10,1); pc2 = realloc(pc2,20); printf("start free\n"); free(pc1); free(pc2); free(pc3); }
编译命令 gcc test.c -g -O0 -L/home/zhongyi/code/module/wrapmalloc -lmymalloc -o test
输出
malloc call from 0x7f55235fc13c,len:1024 free call 0x55ad70263260 from 0x7f552360c29b main func addr:0x55ad6fdf087a start malloc calloc call from 0x55ad6fdf08d1,len:10,size:1 realloc call from 0x55ad6fdf08e6,len:94203399256736,size:20 start free free call 0x55ad70263a80 from 0x55ad6fdf0902 free call 0x55ad70263aa0 from 0x55ad6fdf090e free call 0x55ad70263ac0 from 0x55ad6fdf091a
测试代码中会打印出调用者的地址,分配的内存地址以及分配的大小。定位内存泄漏问题,最重要的有两点,(1)如何知道内存发生了泄漏。(2)如何定位代码哪一行引起了内存泄漏。
针对(1),我们可以使用链表或者其他数据结构,每次分配内存时,就将分配的内存地址和大小等信息存入链表中,释放时根据内存地址和大小对其相应的节点进行释放,最后在检测链表是否为空来判断是否存在内存泄漏。但是数据结构的缺点是内存发生泄漏时不能明显的展示出来。如果使用文件的方式来表示是否发生了内存泄漏,具体假如使用一个单独的文件夹来存放内存检测组件生成的所有文件,运行程序时先清空文件夹的文件,系统调用一次malloc会生成一个文件,以malloc生成的内存地址为文件名,free时释放malloc对应生成的文件,最后如果文件夹存在文件时,就说明存在内存泄漏(malloc和free不匹配造成的)。
针对(2)可以使用C语言的__FILE__
、FUNCTION
、__LINE__
宏定义或者builtin_return_address()
API定位是哪一行引起了内存泄漏。
mtrace
mtrace(memory trace),是 GNU Glibc 自带的内存问题检测工具,它可以用来协助定位内存泄露问题。它的实现源码在glibc源码的malloc目录下,其基本设计原理为设计一个函数 void mtrace (),函数对 libc 库中的 malloc/free 等函数的调用进行追踪,由此来检测内存是否存在泄漏的情况。
下面我们举一个例子。
#include <mcheck.h> #include <stdlib.h> #include <stdio.h> int main(int argc, char **argv) { mtrace(); // 开始跟踪 char *p = (char *)malloc(100); free(p); p = NULL; p = (char *)malloc(100); muntrace(); // 结束跟踪,并生成日志信息 return 0; }
编译命令,-rdynamic 的意思是来告诉链接器将所有符号导出到动态符号表中。
gcc -g gcc -rdynamic mtrace.c -o mtrace
mtrace 机制需要我们实际运行一下程序,然后才能生成跟踪的日志,但在实际运行程序之前还有一件要做的事情是需要告诉 mtrace (即前文提到的 hook 函数)生成日志文件的路径。具体的方法是通过定义并导出一个环境变量 "MALLOC_TRACE",如下所示。
export MALLOC_TRACE=./test.log // 当前目录下
程序运行结束,会在当前目录生成 test.log 文件,打开可以看到一下内容:
➜ mtrace ./mtrace ➜ mtrace cat test.log = Start @ ./mtrace:(main+0x1e)[0x555555554908] + 0x5555557566a0 0x64 @ ./mtrace:(main+0x2e)[0x555555554918] - 0x5555557566a0 @ ./mtrace:(main+0x40)[0x55555555492a] + 0x5555557566a0 0x64 = End ➜ mtrace nm mtrace| grep main U __libc_start_main@@GLIBC_2.2.5 00000000000008ea T main ➜ mtrace
从这个文件中可以看出中间三行分别对应源码中的 malloc -> free -> malloc 操作;解读:[0x1e] 是第一次调用 malloc 函数机器码中的地址信息,+ 表示申请内存( - 表示释放),0x5555557566a0是 malloc 函数申请到的地址信息,0x64 表示的是申请的内存大小。由此分析第一次申请已经释放,第二次申请没有释放,存在内存泄漏的问题。main函数的地址可以使用nm命令差点。
通过使用 "addr2line" 命令工具,得到源文件的行数,这里的地址是main+偏移。
➜ mtrace addr2line -e mtrace 0x908 /home/zhongyi/code/module/mtrace/mtrace.c:9
上述指令仅仅只是可以定位源码位置,但是没法分析存在内存泄漏的问题,因此需要通过下述指令分析日志信息(mtrace + 可执行文件路径 + 日志文件路径)。
➜ mtrace mtrace mtrace test.log Memory not freed: ----------------- Address Size Caller 0x00005555557566a0 0x64 at 0x55555555492a
这里的caller 显示的是调用者地址,0x55555555492a 和test.log对应起来可以找到内存泄漏的位置。
➜ mtrace addr2line -e mtrace 0x92A /home/zhongyi/code/module/mtrace/mtrace.c:14
小结
mtrace
采用 malloc_hook
+ return_addr
这两个机制来实现的。
mtrace
会记录所有的分配、释放,包括所有的模块、线程。内存使用记录必将很多,所以官方推荐使用SIGUSR1
或SIGUSR2
来进行开启和关闭内存记录功能。- 从
mtrace
记录和分析结果可以看到,内存记录日志只记录到malloc
层面。而实际项目开发时,很多接口都是封装多层才会实际调用到malloc
,对于上面几层的地址,mtrace
没有记录。而上面几层的调用关系才是追踪内存泄漏问题的关键所在。所以在实际开发的项目中,使用mtrace
不是一个特别好的方法。这里推荐使用valgrind
工具进行跑流程的方式追踪内存泄漏。如果想要自己记录内存使用情况,可以考虑以下两种方式:- 封装一层内存分配、释放的接口函数来记录内存使用情况。项目开发时,统一使用这个接口来进行内存管理。适用于项目尚未开始。
- 使用 malloc_hook 的方式进行记录,项目代码可以不变。适合于项目已经比较庞大了。
根据系统信息定位内存泄漏
free
通过free命令,我们对内存的整体使用有个初步了解,并快速是否存在内存泄漏可能。
root@firefly:~# free total used free shared buff/cache available Mem: 3669672 198184 3234064 8228 237424 3432672 Swap: 0 0 0
used占用过高,free很低,存在内存泄漏可能,需要继续向下分析。但是buff/cache和available挺高。这种情况一般不是内存泄漏,而是可用内存被系统缓存起来了。
ps
root@firefly:~# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.6 0.2 161012 7872 ? Ss 23:48 0:02 /sbin/init root 2 0.0 0.0 0 0 ? S 23:48 0:00 [kthreadd] root 3 0.0 0.0 0 0 ? S 23:48 0:00 [ksoftirqd/0] root 5 0.0 0.0 0 0 ? S< 23:48 0:00 [kworker/0:0H] ........................ root 1078 0.0 0.0 0 0 ? S 23:48 0:00 [kworker/u12:5] root 1082 0.0 0.0 0 0 ? S< 23:49 0:00 [kworker/2:1H] root 1089 0.0 0.0 7036 2792 ttyFIQ0 R+ 23:53 0:00 ps aux
ps命令可以帮助我们定位分析应用进程内存泄漏。观察RSS列,RSS是这个进程占用的实际物理内存空间。如果有进程RSS占用偏高,则存在内存泄漏可能。
meminfo
root@firefly:~# cat /proc/meminfo MemTotal: 3669672 kB MemFree: 3234148 kB MemAvailable: 3433028 kB Buffers: 10336 kB Cached: 194856 kB SwapCached: 0 kB Active: 194580 kB Inactive: 152744 kB Active(anon): 142848 kB Inactive(anon): 7508 kB Active(file): 51732 kB Inactive(file): 145236 kB Unevictable: 0 kB Mlocked: 0 kB SwapTotal: 0 kB SwapFree: 0 kB Dirty: 0 kB Writeback: 0 kB AnonPages: 142204 kB Mapped: 116216 kB Shmem: 8228 kB Slab: 55612 kB SReclaimable: 32504 kB SUnreclaim: 23108 kB KernelStack: 4912 kB PageTables: 4440 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 1834836 kB Committed_AS: 1408388 kB VmallocTotal: 258867136 kB VmallocUsed: 0 kB VmallocChunk: 0 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB
MemTotal
系统从加电开始到引导完成,firmware/BIOS要保留一些内存,kernel本身要占用一些内存,最后剩下可供kernel支配的内存就是MemTotal。这个值在系统运行期间一般是固定不变的。可参阅解读DMESG中的内存初始化信息。
MemFree
表示系统尚未使用的内存。[MemTotal-MemFree]就是已被用掉的内存。
MemAvailable
有些应用程序会根据系统的可用内存大小自动调整内存申请的多少,所以需要一个记录当前可用内存数量的统计值,MemFree并不适用,因为MemFree不能代表全部可用的内存,系统中有些内存虽然已被使用但是可以回收的,比如cache/buffer、slab都有一部分可以回收,所以这部分可回收的内存加上MemFree才是系统可用的内存,即MemAvailable。/proc/meminfo中的MemAvailable是内核使用特定的算法估算出来的,要注意这是一个估计值,并不精确。
meminfo中对内存的使用进行了详细的统计,我们主要关注Slab和VmllocUsed。
Slab
Slab = SReclaimable + SUnreclaim
如果内存占用偏高,且SReclaimable偏高,则表示可用内存缓存在slab系统内,需要时可回收。
如果内存占用偏高,且SUnreclaim偏高,则表示可能存在内存泄漏。需要进一步观察slabinfo。
如果VmallocUsed内存占用偏高,则存在内存泄漏可能。需要进一步分析vmallocinfo。
vmallocinfo
root@firefly:~# cat /proc/vmallocinfo 0xffffff8008000000-0xffffff8008011000 69632 of_iomap+0x48/0x5c phys=fee00000 ioremap 0xffffff8008012000-0xffffff8008014000 8192 of_iomap+0x48/0x5c phys=ff750000 ioremap 0xffffff8008014000-0xffffff8008016000 8192 of_iomap+0x48/0x5c phys=ff760000 ioremap .............................. 0xffffffbdbff72000-0xffffffbdbfff0000 516096 pcpu_get_vm_areas+0x0/0x500 vmalloc
通过vmalloc分配的内存都统计在/proc/meminfo的 VmallocUsed 值中,但是要注意这个值不止包括了分配的物理内存,还统计了VM_IOREMAP、VM_MAP等操作的值,譬如VM_IOREMAP是把IO地址映射到内核空间、并未消耗物理内存,所以我们要把它们排除在外。从物理内存分配的角度,我们只关心VM_ALLOC操作,这可以从/proc/vmallocinfo中的vmalloc记录看到。
注:/proc/vmallocinfo中能看到vmalloc来自哪个调用者(caller),那是vmalloc()记录下来的,相应的源代码可见:
mm/vmalloc.c: vmalloc > __vmalloc_node_flags > __vmalloc_node > __vmalloc_node_range > __get_vm_area_node > setup_vmalloc_vm
通过运行和销毁程序,我们就可以看到对应的内存的分配和释放情况。正常来说,程序运行时,我们会看到vmalloc记录了所有调用vmalloc的调用栈及其分配到的虚拟内存地址。而当销毁时,这些内存都会被释放掉,而消失在proc中。
如果当我们运行并退出程序后,vmallocinfo中还存在着我们程序中的函数分配信息,那么基本上可以确认,就是这个函数所分配的内存没有释放。
监控脚本
内存泄漏往往是长时间才会出现的,因此,可以尝试添加一些监控脚本,观察系统内存的变化。
#!/bin/bash interval=600 function MonitorInit() { current_time=$(date +"%Y-%m-%d %H:%M:%S") echo "$current_time MonitorInit!" } function MemoryInformationMonitoring() { while true; do echo "cat /proc/zoneinfo" cat /proc/zoneinfo echo "cat /proc/pagetypeinfo" cat /proc/pagetypeinfo echo "cat /proc/meminfo" cat /proc/meminfo echo "cat /proc/buddyinfo" cat /proc/buddyinfo echo "cat /proc/slabinfo" cat /proc/slabinfo echo "cat /proc/vmallocinfo" cat /proc/vmallocinfo echo "cat /proc/vmstat" cat /proc/vmstat echo "cat /proc/self/statm" cat /proc/self/statm echo "cat /proc/self/maps" cat /proc/self/maps echo "cat /proc/swaps" cat /proc/swaps sleep $interval done } function ProcessInformationMonitoring() { PROCESS=$1 PID=$(ps | grep $PROCESS | grep -v 'grep' | awk '{print $1;}') if [ "$PID" != "" ]; then cat /proc/$PID/status sleep $interval fi } MonitorInit ProcessInformationMonitoring process_test MemoryInformationMonitoring
常用工具
valgrind
mtrace
Kmemleak
perf
ASAN
KASAN
KFENCE
后续逐一补充…….
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)