检测内存泄漏的有用工具
在本教程中,将介绍不同的方法和工具来检测和查找Linux中不同进程的内存泄漏。
作为开发人员,我们经常会遇到诸如httpd apache之类的问题,java开始消耗大量内存,从而导致OOM(内存不足)情况。
因此,始终监视关键进程的内存使用情况总是很健康的。
我为一个占用大量内存的应用程序工作,因此确保其他进程不会不必要地消耗内存是我的工作。
在此过程中,我在实时环境中使用了不同的工具来检测内存泄漏,然后将其报告给负责的开发人员。
什么是内存泄漏?
使用" malloc()"或者其变体之一按需分配内存,并在不再需要时释放内存。
当分配了内存但不再需要释放内存时,发生内存泄漏。
泄漏显然是由没有相应的free()的malloc()引起的,但是如果删除,丢失或者覆盖了动态分配的内存的指针,泄漏也可能无意中引起。
缓冲区超限(是由于在分配的内存块的末尾写入而导致的)经常损坏内存。
内存泄漏绝不是嵌入式系统所独有的,但之所以成为一个问题,部分是因为目标一开始就没有太多的内存,部分是由于目标通常长时间运行而没有重新启动,因此泄漏可能会很大水坑。
不管根本原因是什么,内存管理错误都可能对应用程序和系统行为产生意想不到的甚至是毁灭性的影响。
随着可用内存的减少,进程和整个系统可能会陷入停顿,而损坏的内存通常会导致虚假的崩溃。
在继续之前,我建议我们还阅读一下" Linux内存管理",以便我们熟悉Linux内核在内存方面使用的不同术语。
1. Memwatch
约翰·林德(Johan Lindh)编写的MEMWATCH是用于C的开源内存错误检测工具。
可以从https://sourceforge.net/projects/memwatch下载
只需在代码中添加头文件并在
gcc
命令中定义MEMWATCH,就可以跟踪程序中的内存泄漏和损坏。MEMWATCH支持ANSI C;提供结果日志;并检测两次释放,错误释放,未释放的内存,上溢和下溢等。
我已经在Linux服务器上下载并提取了memwatch,因为我们可以在屏幕截图中进行检查:
接下来,在编译软件之前,我们必须注释掉test.c的以下行,该行是memwatch存档的一部分。
/* Comment out the following line to compile. */ //error "Hey! Don't just compile this program, read the comments first!"
接下来编译软件:
[root@server memwatch-2.71]# make cc -DMEMWATCH -DMW_STDIO test.c memwatch.c
提示:
确保已在Linux服务器上安装了所有编译软件,例如gcc
,make
等。
接下来,我们将创建一个虚拟C程序" memory.c",并在第3行上添加" memwatch.h" include,以便可以启用MEMWATCH。
另外,需要为程序中的每个源文件的编译语句添加两个编译时标志-DMEMWATCH和-DMW_STDIO。
[root@server memwatch-2.71]# cat memory.c 1 #include 2 #include 3 #include "memwatch.h" 4 5 int main(void) 6 { 7 char *ptr1; 8 char *ptr2; 9 10 ptr1 = malloc(512); 11 ptr2 = malloc(512); 12 13 ptr2 = ptr1; 14 free(ptr2); 15 free(ptr1); 16 }
中显示的代码分配了两个512字节的内存块(第10和11行),然后将指向第一个块的指针设置为第二个块(第13行)。
结果,第二块的地址丢失,并且发生存储器泄漏。
现在,使用示例源代码(memory1.c
)编译" memwatch.c"文件,该文件是MEMWATCH软件包的一部分。
以下是用于构建" memory1.c"的示例" makefile"。memory1
是此makefile
产生的可执行文件:
[root@server memwatch-2.71]# gcc -DMEMWATCH -DMW_STDIO memory1.c memwatch.c -o memory1
接下来,我们执行程序" memory1",该程序捕获了两个内存管理异常
[root@server memwatch-2.71]# ./memory1 MEMWATCH detected 2 anomalies
MEMWATCH创建一个名为memwatch.log
的日志。
如我们在下面看到的,它是通过运行memory1
程序创建的。
带有内存泄漏信息的memwatch日志文件
MEMWATCH会告诉我们哪一行有问题。
要释放已释放的指针,它将标识该条件。
对于未释放的内存也是如此。
日志末尾的部分显示统计信息,包括泄漏的内存量,已使用的内存量以及分配的总量。
在上图中,我们可以看到在第15行发生了内存管理错误,这表明存在双倍的可用内存。
下一个错误是512字节的内存泄漏,并且该内存在第11行分配。
2.Valgrind
Valgrind是Intel x86专用工具,可模拟x86级CPU直接观察所有内存访问并分析数据流
优点之一是我们不必重新编译要检查的程序和库,尽管如果使用
-g
选项,以便它们包括调试符号表。
它通过在仿真环境中运行程序并在各个点捕获执行来工作。
这导致Valgrind的一个很大缺点,那就是该程序以正常速度的一小部分运行,这使得它在测试具有实时约束的任何事物时都不太有用。
Valgrind可以检测到以下问题:
使用未初始化的内存
释放内存后对其进行读写
从内存读取和写入超过分配的大小
在堆栈上读写不适当的区域
内存泄漏
传递未初始化和/或者无法寻址的内存
不正确使用
malloc
/new/new()与free/delete/delete()
Valgrind在大多数Linux发行版中都可用,因此我们可以直接继续安装该工具
# rpm -Uvh /tmp/valgrind-3.15.0-11.el7.x86_64.rpm Preparing... ################################# [100%] Updating/installing... 1:valgrind-1:3.15.0-11.el7 ################################# [100%]
接下来,我们可以对要检查内存泄漏的进程执行valgrind
。
例如,我希望检查使用-f
选项执行的amsHelper
进程的内存泄漏。
按Ctrl + C停止监视
# valgrind amsHelper -f ==30159== Memcheck, a memory error detector ==30159== Copyright (C) 2002-2016, and GNU GPL'd, by Julian Seward et al. ==30159== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==30159== Command: amsHelper -f ==30159== NET-SNMP version 5.7.3 AgentX subagent connected ^C==30159== ==30159== HEAP SUMMARY: ==30159== in use at exit: 853,777 bytes in 11,106 blocks ==30159== total heap usage: 21,779 allocs, 10,673 frees, 28,226,706 bytes allocated ==30159== ==30159== LEAK SUMMARY: ==30159== definitely lost: 73 bytes in 2 blocks ==30159== indirectly lost: 0 bytes in 0 blocks ==30159== possibly lost: 32,341 bytes in 120 blocks ==30159== still reachable: 821,363 bytes in 10,984 blocks ==30159== suppressed: 0 bytes in 0 blocks ==30159== Rerun with --leak-check=full to see details of leaked memory ==30159== ==30159== For lists of detected and suppressed errors, rerun with: -s ==30159== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
要将输出保存到日志文件并收集有关泄漏的更多详细信息,请使用--leak-check-full和--log-file =/path/og/log/file。
按Ctrl + C停止监视
# valgrind --leak-check=full --log-file=/tmp/mem-leak-amsHelper.log amsHelper -f NET-SNMP version 5.7.3 AgentX subagent connected
现在,我们可以检查/tmp/mem-leak-amsHelper.log
的内容。
发现问题时,Valgrind输出具有以下格式:
==771== 72 bytes in 1 blocks are definitely lost in loss record 2,251 of 3,462 ==771== at 0x4C2B975: calloc (vg_replace_malloc.c:711) ==771== by 0x6087603: ??? (in /usr/lib64/libnss3.so) ==771== by 0x60876A8: ??? (in /usr/lib64/libnss3.so) ==771== by 0x6087268: ??? (in /usr/lib64/libnss3.so) ==771== by 0x607C11A: ??? (in /usr/lib64/libnss3.so) ==771== by 0x60808C5: ??? (in /usr/lib64/libnss3.so) ==771== by 0x602A269: ??? (in /usr/lib64/libnss3.so) ==771== by 0x602AA80: NSS_InitContext (in /usr/lib64/libnss3.so) ==771== by 0x550F3BA: rpmInitCrypto (in /usr/lib64/librpmio.so.3.2.2) ==771== by 0x52CBF8D: rpmReadConfigFiles (in /usr/lib64/librpm.so.3.2.2) ==771== by 0x473C74: ??? (in /usr/sbin/amsHelper) ==771== by 0x441E24: ??? (in /usr/sbin/amsHelper)
3. Memleax
Valgrind的缺点之一是,我们无法检查现有进程的内存泄漏,而内存泄漏正是通过该进程进行救援的。
我曾经遇到过实例,其中amsHelper进程的内存泄漏非常零星,因此有时我看到该进程保留了内存,我想调试该特定的PID,而不是创建一个新的PID进行分析。
memleax通过添加它来调试正在运行的进程的内存泄漏。
它钩住目标进程对内存分配和释放的调用,并实时报告由于内存泄漏而存在足够长的内存块。
默认的过期阈值为10秒,但是我们应始终根据情况通过-e选项进行设置。
我们可以从官方的github仓库下载memleax。
接下来的memleax期望在安装memleax rpm之前必须先安装一些依赖项。
# rpm -Uvh /tmp/memleax-1.1.1-1.el7.centos.x86_64.rpm error: Failed dependencies: libdwarf.so.0()(64bit) is needed by memleax-1.1.1-1.el7.centos.x86_64 libunwind-x86_64.so.8()(64bit) is needed by memleax-1.1.1-1.el7.centos.x86_64 libunwind.so.8()(64bit) is needed by memleax-1.1.1-1.el7.centos.x86_64
因此,我从官方存储库中手动复制了这些rpm,因为这是一个专用网络,无法使用yum或者dnf
# rpm -Uvh /tmp/libdwarf-20130207-4.el7.x86_64.rpm Preparing... ################################# [100%] Updating/installing... 1:libdwarf-20130207-4.el7 ################################# [100%] # rpm -Uvh /tmp/libunwind-1.2-2.el7.x86_64.rpm Preparing... ################################# [100%] Updating/installing... 1:libunwind-2:1.2-2.el7 ################################# [100%]
现在,由于我已经安装了两个依赖项,因此我将继续安装memleax rpm:
# rpm -Uvh /tmp/memleax-1.1.1-1.el7.centos.x86_64.rpm Preparing... ################################# [100%] Updating/installing... 1:memleax-1.1.1-1.el7.centos ################################# [100%]
接下来,我们需要要监视的过程的PID。
我们可以从ps -ef
输出中获取过程的PID
root 2102 1 0 12:29 ? 00:00:01 /sbin/amsHelper -f root 45256 1 0 13:13 ? 00:00:00 amsHelper root 49372 44811 0 13:23 pts/0 00:00:00 grep amsH
现在我们要检查" 45256" PID的内存泄漏
# memleax 45256 Warning: no debug-line found for /usr/sbin/amsHelper == Begin monitoring process 45256... CallStack[1]: memory expires with 688 bytes, backtrace: 0x00007f0bc87010d0 libc-2.17.so calloc()+0 0x00000000004079d3 amsHelper 0x0000000000409249 amsHelper 0x0000000000407077 amsHelper 0x00000000004a7a60 amsHelper 0x00000000004a8c4c amsHelper 0x00000000004afd90 amsHelper 0x00000000004ac97a amsHelper table_helper_handler()+2842 0x00000000004afd90 amsHelper 0x00000000004bae09 amsHelper 0x00000000004bb707 amsHelper 0x00000000004bb880 amsHelper 0x00000000004bbca2 amsHelper 0x00000000004e7eb1 amsHelper 0x00000000004e8a3e amsHelper 0x00000000004e98a9 amsHelper 0x00000000004e98fb amsHelper 0x00000000004051b4 amsHelper 0x00007f0bc869d555 libc-2.17.so __libc_start_main()+245 0x00000000004053e2 amsHelper CallStack[1]: memory expires with 688 bytes, 2 times again CallStack[1]: memory expires with 688 bytes, 3 times again CallStack[1]: memory expires with 688 bytes, 4 times again CallStack[1]: memory expires with 688 bytes, 5 times again CallStack[2]: memory expires with 15 bytes, backtrace: 0x00007f0bc87006b0 libc-2.17.so malloc()+0 0x00007f0bc8707afa libc-2.17.so __GI___strdup()+26 0x00007f0bc8731141 libc-2.17.so tzset_internal()+161 0x00007f0bc8731b03 libc-2.17.so __tz_convert()+99
在应用程序过程中发生内存泄漏的情况下,我们可能会获得与上述类似的输出。
按Ctrl + C停止监视
4.收集核心转储
有时,我们可以共享正在泄漏内存的进程的核心转储,这对开发人员有帮助。
在Red Hat/CentOS中,我们可以使用abrt
和abrt-addon-ccpp
来收集核心转储,然后再开始确保通过消除核心限制来设置系统以生成应用程序核心:
# ulimit -c unlimited
接下来在环境中安装这些rpm
# yum install abrt abrt-addon-ccpp abrt-tui
确保已安装ccpp挂钩:
# abrt-install-ccpp-hook install # abrt-install-ccpp-hook is-installed; echo $?;
确保此服务已启动,并且已启用用于捕获核心转储的ccpp挂钩:
# systemctl enable abrtd.service --now # systemctl enable abrt-ccpp.service --now
启用挂钩
# abrt-auto-reporting enabled
要在命令行上获取崩溃列表,请发出以下命令:
# abrt-cli list
但是由于没有崩溃,输出将为空。
接下来,获取我们要为其收集核心转储的PID,例如,其中我将为PID 45256收集
root 2102 1 0 12:29 ? 00:00:01 /sbin/amsHelper -f root 45256 1 0 13:13 ? 00:00:00 amsHelper root 49372 44811 0 13:23 pts/0 00:00:00 grep amsH
接下来我们必须向该PID发送SIGABRT即-6终止信号以生成核心转储
# kill -6 45256
接下来,我们可以检查可用转储的列表,现在我们可以看到该PID的新条目。
此转储将包含分析此过程的泄漏所需的所有信息
# abrt-cli list id 2b9bb9702d83e344bc940b813b43262ede9d9521 reason: amsHelper killed by SIGABRT time: Thu 13 Aug 2017 02:29:27 PM +0630 cmdline: amsHelper package: hp-ams-2.10.0-861.6.rhel7 uid: 0 (root) Directory: /var/spool/abrt/ccpp-2017-08-13-14:29:27-45256
5.如何使用默认的Linux工具识别内存泄漏
我们讨论了可用于检测内存泄漏的第三方工具,并提供了代码中的更多信息,这些信息可帮助开发人员分析和修复错误。
但是,如果我们的要求只是寻找无缘无故地保留内存的进程,那么我们将不得不依靠诸如sar,vmstat,pmap,meminfo等系统工具。
因此,让我们了解如何使用这些工具来确定可能的内存泄漏情况。
开始之前,我们必须熟悉以下领域
如何检查单个进程消耗的实际内存
应用程序正常保留多少内存
如果我们对以上问题有答案,那么分析问题将更加容易。
例如,在我的情况下,我知道amsHelper的内存使用量不应超过几MB,但是如果发生内存泄漏,则内存预留量会超过几MB。
如果我们阅读了我以前的文章,其中我解释了检查实际内存使用情况的不同工具,那么我们会知道Pss给了我们有关进程消耗的内存的实际想法。
pmap将为我们提供由各个地址段和进程库消耗的内存的更详细的输出,如下所示
# pmap -X $(pgrep amsHelper -f) 15046: /sbin/amsHelper -f Address Perm Offset Device Inode Size Rss Pss Referenced Anonymous Swap Locked Mapping 00400000 r-xp 00000000 fd:02 7558 1636 1152 1152 1152 0 0 0 amsHelper 00799000 r--p 00199000 fd:02 7558 4 4 4 4 4 0 0 amsHelper 0079a000 rw-p 0019a000 fd:02 7558 52 48 48 48 20 0 0 amsHelper 007a7000 rw-p 00000000 00:00 0 356 48 48 48 48 0 0 01962000 rw-p 00000000 00:00 0 9716 9716 9716 9716 9716 0 0 [heap] 7fd75048b000 r-xp 00000000 fd:02 3406 524 320 44 320 0 0 0 libfreeblpriv3.so 7fd75050e000 ---p 00083000 fd:02 3406 2048 0 0 0 0 0 0 libfreeblpriv3.so 7fd75070e000 r--p 00083000 fd:02 3406 8 8 8 8 8 0 0 libfreeblpriv3.so 7fd750710000 rw-p 00085000 fd:02 3406 4 4 4 4 4 0 0 libfreeblpriv3.so 7fd750711000 rw-p 00000000 00:00 0 16 16 16 16 16 0 0 <output trimmed> 7fd75ba5f000 rw-p 00022000 fd:02 4011 4 4 4 4 4 0 0 ld-2.17.so 7fd75ba60000 rw-p 00000000 00:00 0 4 4 4 4 4 0 0 7ffdeb75d000 rw-p 00000000 00:00 0 132 32 32 32 32 0 0 [stack] 7ffdeb79a000 r-xp 00000000 00:00 0 8 4 0 4 0 0 0 [vdso] ffffffffff600000 r-xp 00000000 00:00 0 4 0 0 0 0 0 0 [vsyscall] ====== ===== ===== ========== ========= ==== ====== 196632 15896 13896 15896 10384 0 0 KB
另外,我们可以使用相应过程的" smaps"获得相同的信息以及更多详细信息。
其中我编写了一个小脚本来合并内存并获得总数,但是我们也可以删除管道并分解命令以获取更多详细信息
# cat /proc/$(pgrep amsHelper)/smaps | grep -i pss | awk '{Total+=} END {print Total/1024" MB"}' 14.4092 MB
因此,我们可以放置一个cron作业或者创建一个守护程序,以使用这些工具确定应用程序是否随着时间消耗过多的内存,从而及时监视应用程序的内存消耗。