2020年4月12日星期日

记一次内存泄漏的定位

4月6日,我徒弟开发的一个程序在测试的过程中出现了异常退出问题。说好4月7日程序要发布的,如果不能按时发布,可能我们研发部又会被工程部诟病。我当时想着当天一定要把这个问题解决掉。
这是一个Linux下用C语言开发的程序,我之前没有看过代码。
我首先尝试复现问题,看程序的异常退出有没有什么规律。我发现程序每隔17分多一点就会异常退出,但具体多几秒不是一个准确的数值。程序退出前没有什么特别的操作。
我首先怀疑是不是程序执行时遇到了空指针。于是在命令行输入
ulimit -c unlimited
这条命令的目的是,当程序运行遭遇Segmentation Fault(段错误)时,Linux系统会将程序的内存空间存储到一个Core Dump文件中。假设出现故障的程序名为ibp, 生成的core dump文件名为core, 在随便一台装有gdb的电脑上运行gdb ibp core, 即可在gdb程序中看到出现问题的函数。在gdb中输入bt更可看到函数调用栈。如果编译程序的时候,给gcc添加-ggdb参数,用这种方法看到的信息更全面准确,可精确到代码行。
回到我这个问题上,令我惊讶的是,程序异常退出并未生成core dump文件。同时我注意到,程序遇到空指针等段错误被中止时,屏幕上会出现Segmetation Fault字样,而这个程序异常退出时屏幕上仅仅显示Killed.这是一块嵌入式板卡,谁会把这个进程给Kill掉呢?
会不会是程序给自己发了信号,自己又没有捕捉到? 问了开发人员,他说并没有用信号。
我在一个FreeBSD的讨论群里随口问了一句,有人说可能是oom错误,也有人建议我加一个函数,把收到的信号打印出来,看是谁发的。
我尝试用
kill -s 信号编号 进程号
给这个进程发各种信号,发现程序异常退出时终端上的输出都不一样。Killed基本上是收到了kill信号,因此我决定先从oom错误入手。其实这个时候,我并不知道oom错误是什么,马上上网搜索。
原来oom就是Out Of Memory的意思。Linux系统会给每个进程的内存占用情况打个分,当整个系统内存不够的时候,会把打分最高的进程kill掉。这个打分位于/proc/进程号/oom_score文件。
我看了一下,板卡上其他进程的oom_score都是稳定在1,而我眼前这个进程启动后,oom_score从1开始攀升到900,快到900的时候,会感到cat oom_score都明显卡顿,达到900后进程就被kill掉了。基本上确定是内存泄漏无疑了。
把这个发现告诉开发人员,他确实在一些异常分支上找到一些malloc后没有free的情况。改掉后一试,oom_score依然攀升。想想那些free都在异常分支上,程序不可能跑那么多异常分支,现在这种情况更像是程序的主流程上还有泄露。
要找到内存泄漏的地方,可能得借助第三方检测工具了。可是这个程序是运行在嵌入式板卡上的,这个板卡没有对应的检测工具。于是我尝试把程序跑在我的PC机上,还好,删掉一些IO操作的函数后,程序跑起来了。还好,valgrind这样强大的工具竟然是免费的。于是在命令行运行
valgrind 程序名
在程序运行一段时间后,按Ctrl-C中止程序运行。此时valgrind在屏幕上输出可能出现内存泄漏的地方,问题一目了然。
一是调用json-C库的一些函数时,需要自己释放。一般来说,不同于标准的C语言库,调用第三方的C函数库,如我接触过的glib库、Qt库、curl库、json-C库,这些库的函数都会进行一些malloc的操作,调用结束后一定要调用相应的库函数进行释放操作。基本上每个库都能在网上搜到很多文章介绍释放操作,比如这次的json-C库,搜索"json-C 内存泄露"就能搜到。这次对我们很有帮助的是这篇文章http://blog.coderhuo.tech/2017/04/08/usage_of_jsonc/
二是程序中每写一次日志,都会调用opendir函数,这个函数也会进行malloc的操作,需要调用closedir函数进行内存释放。
将上述问题修改后,再运行程序,可以看到oom_score稳定在1了,可以认为内存泄漏问题已经解决。

没有评论:

发表评论