最近一直在监测之前开发的一款数据采集器的性能,基于嵌入式linux系统开发,发现应用在运行过程中,虚拟内存(使用top监测)和物理内存(使用free监测)会一直增长,大约一个小时分别增长1M左右。
下面回顾一下对于该问题的分析过程。
凭经验,应该发生了内存泄漏,经过多次对比测试之后,基本确定导致该问题的功能模块。但是,经过分析代码,一直找不到导致问题的关键点,无奈,只能求助于工具,首先想到的就是赫赫有名的valgrind,该工具的一项重要功能就是监测内存泄漏。
由于linux的rootfs是通过buildroot构建出来的,所以,增加valgrind十分的简单。之后,通过valgrind长时间测试了应用的内存使用情况,结果让我大失所望,valgrind确实监测到了一些所谓的“内存泄漏(主要是一些已经申请,就不会释放的内存块)”,但是,没有看到导致开头所讲的内存泄漏的原因。
之后,又经过几天的对比测试,进一步锁定了导致该问题的功能部分。具体是这样的,该应用涉及到了MQTT协议通信需求,基于mosquitto开发MQTT相关的功能,多次测试结果表示:只开启一路mqtt连接,并且MQTT和服务端MQTT Broker之间的心跳为5s时,大约经过一个小时就会有1M内存的泄漏!
问题的关键点找到了,下一步就是定位,解决问题了,下面是具体的执行步骤:
- 快速复现问题
因为之前的测试结果是,一个MQTT连接,5s心跳,大约1小时,内存泄漏1M。为了能够快速的复现问题,缩短调试时间,我同时建立了50个连接,每个连接的心跳为5s,这样基本上大约1分钟,内存就会泄漏1M,结果和猜想的一致,大约55s,内存就会泄漏1M,经过进一步的分析发现是,如果向mosquitto注册了on_log回调函数,就会出问题,反之,这不会出现。好了,问题可以快速复现了,这极大的提高了调试效率。 - 实现参照模型,编写最小复现系统
由于产品功能比较复杂,为了排除其他功能模块干扰,最重要的是确定是否是mosquitto惹的祸,我实现了类似于上一步的最小测试模块,作为参照模型,该单元只有一个功能,就是建立50个连接,每个连接心跳5s,没有其他任何功能。不过,可惜的是,没有出现内存泄漏问题,心情无比崩溃 (: 。不过,虽然这个参照模型没有成功,对于实际问题的分析,这一步是必不可少的! - 确定问题原因
参照模型没有成功,但在应用使用环境下,问题确实存在,问题原因若隐若现。应用环境下,问题确实存在,参照模型下不存在,看来需要分析mosquitto的源码了。上面提到在注册on_log之后,会出现内存泄漏,那这个on_log的工作原理是怎样的呢,下面是简单的介绍:
如果应用向mosquitto注册了on_log,那么在mosquitto运行过程中会将一些调试信息通过该回调函数进行输出,用于调试。而这里的心跳包会在注册了on_log回调的情况下,执行如下的路径:
- send mqtt ping request packet->on_log
- recv mqtt ping response packet->on_log
调用on_log的代码如下:
int log__printf(struct mosquitto *mosq, unsigned int priority, const char *fmt, ...)
{
va_list va;
char *s;
size_t len;
pthread_mutex_lock(&mosq->log_callback_mutex);
if(mosq->on_log){
len = strlen(fmt) + 500;
s = mosquitto__malloc(len*sizeof(char));
if(!s){
pthread_mutex_unlock(&mosq->log_callback_mutex);
return MOSQ_ERR_NOMEM;
}
va_start(va, fmt);
vsnprintf(s, len, fmt, va);
va_end(va);
s[len-1] = '\0'; /* Ensure string is null terminated. */
mosq->on_log(mosq, mosq->userdata, (int)priority, s);
mosquitto__free(s);
}
pthread_mutex_unlock(&mosq->log_callback_mutex);
return MOSQ_ERR_SUCCESS;
}
这里可以看到,如果on_log有效就会将日志输出,也可以看到这里调用了mosquitto__malloc进行了缓冲区分配,在on_log返回之后,调用mosquitto__free释放了之前的缓存。
这里的代码没啥问题,内存都正常释放了,那怎么会出现内存泄漏呢? (:
- 解决问题
由于没有找到问题的原因,解决问题也就无从谈起,但是,正常情况下应该有这个步骤 😉😃 下面做些一些猜测:
- 由于log__printf日志输出缓冲区大小不定len = strlen(fmt) + 500,详见log_printf的具体实现,这样申请的内存容易造成内存碎片,可能导致系统无法回收这些内存,最终内存泄漏。
- 应用本身运行环境导致mosquitto出现该问题… 😉
- 革命尚未成功,同志仍需努力!
虽然,这次调试过程,最后也没有找到问题的原因,但是,上面提到的**4个分析步骤,**对于一般的问题解决都是通用的,后面可以加以借鉴,至于问题原因还需要进一步努力寻找了,静候佳音吧 😉