Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
memory
MQTT
内存泄漏leak
记一次解决MQTT软件包内存泄露的心路历程
发布于 2021-09-27 15:01:12 浏览:3207
订阅该版
[tocm] ### 1、技术背景 物联网产品使用的mqtt连接功能采用的kawii-mqtt软件包,具体的软件包地址在:[kawii mqtt软件包地址](https://github.com/longtengmcu/kawaii-mqtt),当出基于此软件包开发时,解决了此软件包的许多问题(可查看git 提交记录),包括内存泄漏问题,现在已经成功应用在产品上,并且稳定运行。目前的产品应用是mqtt做的长连接,即创建连接后,应用程序不会主动断开连接,可以稳定运行。 最近开发产品由于要做低功耗,所以不能使用mqtt长连接,只能在收集了很多数据后,给4G模块上电,建立MQTT连接,发送数据,再断开MQTT连接,关闭4G供电,这个操作流程之前未使用了。 ### 2、遇到问题 首先说一下利器,查看内存是否出现泄漏的神奇命令就是free,如下,观察程序长时间运行时used merory是否稳定,程序如果没有发生内存泄漏的话,此值会保持在一个稳定的值。 ``` msh /> msh />free total memory: 173036 used memory : 75976 maximum allocated memory: 84580 ``` 产品业务数据生成代码编写完成后,测试运行这部分代码,程序运行的内存堆占用稳定,不会出现内存使用占用增加。 mqtt连网程序编写完成后,把产品业务数据通信mqtt连接发送到服务器。在收集一定数量的业务数据后,建立mqtt连接,发送数据 ,之后断开连接。本以为业务程序之前测试过,mqtt软件包在正式的产品上也稳定用过,觉得不会出什么大问题。下班进启动此程序测试,谁知在运行30分钟左右后,内存开始出现增长。此时我已经上了回家的地铁。 ``` msh />free total memory: 173036 used memory : 75392 maximum allocated memory: 83600 msh />free total memory: 173036 used memory : 75944 maximum allocated memory: 83600 msh /> msh />free total memory: 173036 used memory : 76144 maximum allocated memory: 83600 msh /> msh />free total memory: 173036 used memory : 76416 maximum allocated memory: 83600 ``` 周一上班看到了上面的打印信息, 这说明程序出现的内存泄漏,做为资深码农直觉感到这个问题不好查。同时也对这种高难度问题表现出极强的战斗力,似乎不能快速解决这种内存泄露问题,就不能证明自己多年来的技术实力。虚荣心在做怪呀!!!我内心变身超级飞侠说:“是时候要拿出法宝打boss(bug)了”。 ### 3、定位、分析、解决 遇到了内存泄露问题,首先要进行代码的定位,确定哪部分代码导致的内存泄露,现在一共新加了2部分代码,产品业务数据生成代码,mqtt联网代码,虽说产品业务数据生成代码经过单独测试未出现内存泄露,kawii-mqtt软件包也在产品上稳定运行过,所以还真的不好判断问题出现在哪里??? 1、那就先从打印信息入手,分析什么情况下会内存增长,通过仿真器观察未释放的内存中存储的内容,一上午的分析,获取了很多数据,但是内存堆中的数据难以通过存储的内容的来识别是哪个程序申请的。下图就是一段未释放的内存,根据内存堆的数据结构(内存堆头占12个字节),查看内容也无法识别。 ![内存未释放.jpg](https://oss-club.rt-thread.org/uploads/20210927/53060e3eb9510e1fc6f10a4c7515f4c5.jpg.webp) 2、查看产品业务数据生成代码,通过阅读代码,查看是否有逻辑上的问题,经过查看未发现。由于kawii-mqqt软件包已经用过很久,最近很久都没有查看过此代码,忘记了内部的逻辑,同时代码逻辑又多,所以偷懒就没有再去看,想着通过其他手段来查找问题的原因。 3、通过其他打印信息来发现问题,在发生内存泄漏时,list_fd, netstat命令查看到出现网络连接未释放,文件句柄也未释放。 ``` msh />list_fd fd type ref magic path -- ------ --- ----- ------ 0 file 1 fdfd /uart1 1 socket 1 fdfd 2 socket 1 fdfd 3 socket 1 fdfd 4 socket 1 fdfd msh />netstat Active PCB states: #0 192.168.0.101:49154 <==> 183.230.40.96:1883 snd_nxt 0x000BE6EB rcv_nxt 0x3C8A3653 state: ESTABLISHED #1 192.168.0.101:49155 <==> 183.230.40.96:1883 snd_nxt 0x000BE6EB rcv_nxt 0x3C8A3653 state: CLOSE_WAITE #2 192.168.0.101:49156 <==> 183.230.40.96:1883 snd_nxt 0x000BE6EB rcv_nxt 0x3C8A3653 state: TIME_WAITE ``` 内心一阵狂喜,看到了希望,后来才知道,这只是一个小怪。以上打印说明,存在未放的网络连接,文件socket导致内存增长,这与mqtt软件包直接相关,就是Mqtt软件包的问题了,看来确实需要硬着头皮来查看内部代码逻辑了。 查看kawii-mqtt代码,代码软件发现在调用mqtt_disconnect函数后,以太网的连接是在mqtt_yield_thread线程中执行network_disconnect后才完成的。所以这有一个时间差,导致在mqtt_disconnect执行后,连接的状态变成断开,而以太网连接还没有断开时,执行自动连接函数mqtt_try_reconnect时会建立新的连接,导致原来的连接socket未释放,而再创建了新的连接。 修改代码在 mqtt_connect_with_results函数中判断当连接状态为断开时(CLIENT_STATE_CLEAN_SESSION)返回失败,不再执行创建以太网连接。 ``` static int mqtt_connect_with_results(mqtt_client_t* c) { int len = 0; int rc = KAWAII_MQTT_CONNECT_FAILED_ERROR; platform_timer_t connect_timer; mqtt_connack_data_t connack_data = {0}; client_state_t state; MQTTPacket_connectData connect_data = MQTTPacket_connectData_initializer; if (NULL == c) { RETURN_ERROR(KAWAII_MQTT_NULL_VALUE_ERROR); } state = mqtt_get_client_state(c); if (CLIENT_STATE_CONNECTED == state) { RETURN_ERROR(KAWAII_MQTT_SUCCESS_ERROR); } if(CLIENT_STATE_CLEAN_SESSION == state) { RETURN_ERROR(KAWAII_MQTT_CLEAN_SESSION_ERROR); } #ifdef KAWAII_MQTT_NETWORK_TYPE_TLS rc = network_init(c->mqtt_network, c->mqtt_host, c->mqtt_port, c->mqtt_ca); #else rc = network_init(c->mqtt_network, c->mqtt_host, c->mqtt_port, NULL); #endif rc = network_connect(c->mqtt_network); if (KAWAII_MQTT_SUCCESS_ERROR != rc) { /*when connect faile, you should call network_release to release socket file descriptor zhaoshimin 20200629*/ network_release(c->mqtt_network); RETURN_ERROR(rc); } connect_data.keepAliveInterval = c->mqtt_keep_alive_interval; connect_data.cleansession = c->mqtt_clean_session; connect_data.MQTTVersion = c->mqtt_version; connect_data.clientID.cstring= c->mqtt_client_id; connect_data.username.cstring = c->mqtt_user_name; connect_data.password.cstring = c->mqtt_password; if (c->mqtt_will_flag) { connect_data.willFlag = c->mqtt_will_flag; connect_data.will.message.cstring = c->mqtt_will_options->will_message; connect_data.will.qos = c->mqtt_will_options->will_qos; connect_data.will.retained = c->mqtt_will_options->will_retained; connect_data.will.topicName.cstring = c->mqtt_will_options->will_topic; } platform_timer_cutdown(&c->mqtt_last_received, (c->mqtt_keep_alive_interval * 1000)); platform_mutex_lock(&c->mqtt_write_lock); /* serialize connect packet */ if ((len = MQTTSerialize_connect(c->mqtt_write_buf, c->mqtt_write_buf_size, &connect_data)) <= 0) goto exit; platform_timer_cutdown(&connect_timer, c->mqtt_cmd_timeout); /* send connect packet */ if ((rc = mqtt_send_packet(c, len, &connect_timer)) != KAWAII_MQTT_SUCCESS_ERROR) goto exit; if (mqtt_wait_packet(c, CONNACK, &connect_timer) == CONNACK) { if (MQTTDeserialize_connack(&connack_data.session_present, &connack_data.rc, c->mqtt_read_buf, c->mqtt_read_buf_size) == 1) rc = connack_data.rc; else rc = KAWAII_MQTT_CONNECT_FAILED_ERROR; } else rc = KAWAII_MQTT_CONNECT_FAILED_ERROR; exit: if (rc == KAWAII_MQTT_SUCCESS_ERROR) { if(NULL == c->mqtt_thread) { /* connect success, and need init mqtt thread, rt thread thread name max len is 8*/ c->mqtt_thread = platform_thread_init("mqtt", mqtt_yield_thread, c, KAWAII_MQTT_THREAD_STACK_SIZE, KAWAII_MQTT_THREAD_PRIO, KAWAII_MQTT_THREAD_TICK); if (NULL != c->mqtt_thread) { mqtt_set_client_state(c, CLIENT_STATE_CONNECTED); platform_thread_startup(c->mqtt_thread); /* start run mqtt thread */ } else { /*creat the thread fail and disconnect the mqtt socket connect*/ network_release(c->mqtt_network); rc = KAWAII_MQTT_CONNECT_FAILED_ERROR; KAWAII_MQTT_LOG_W("%s:%d %s()... mqtt yield thread creat faile...", __FILE__, __LINE__, __FUNCTION__); } } else { mqtt_set_client_state(c, CLIENT_STATE_CONNECTED); /* reconnect, mqtt thread is already exists */ } c->mqtt_ping_outstanding = 0; /* reset ping outstanding */ /* call the connect success callback function*/ if((rc == KAWAII_MQTT_SUCCESS_ERROR) && (c->mqtt_connect_handler)) { c->mqtt_connect_handler(c, c->mqtt_connect_data); } } else { /*when server ack error, it must close the mqtt socket zhaoshimin 20200724 */ network_release(c->mqtt_network); mqtt_set_client_state(c, CLIENT_STATE_INITIALIZED); /* connect failed */ } platform_mutex_unlock(&c->mqtt_write_lock); RETURN_ERROR(rc); } ``` 修改完成这些,已经到了晚上6点30多,开始测试,以太网的不在出现未释放的连接,文件句柄也正常了了。看来有希望正常下班回家吃饭了,老婆打来电话问什么时间回家,我说过一会7点出发。正准备收拾回家时,我去,内存占用又开始长了起来。看来这只是一只小怪呀! ### 4、打败终级boss 到此也不能完全确定内存泄露出现在mqtt软件包内, 只是它的嫌疑比较大,产品业务数据生成代码依然有可能内存泄露。这次要采用什么方法呢?就是把这两部分软件的申请内存,释放内存的代码部分全部加上打印,打印出申请内存和释放内存的指针。顶着饥肠辘辘,一顿神操作,抓取到了以下打印信息,每次创建连接申请5块内存,断开时应该释放5块,这里只释放了3块。到这里实锤了,内存泄露就是出在mqtt软件包内。 ![123.png](https://oss-club.rt-thread.org/uploads/20210927/cb5fbd09fdd86053adfe8e198f552deb.png.webp) 问题范围进一步缩小了,已经定位到了mqtt软件包,这里有2个长度为20的内存块未释放,通过查看代码,发现在向服务器订单主题时执行函数会mqtt_subscribe申请一个存储主题的结构体sizeof(message_handlers_t),它的长度是20字节,mqtt连接时会订阅2条主题,所以会申请2块20字节的内存。当连接断开时应该要释放此内存,看来代码问题出现在连接断开时对此的处理,断开连接的处理操作在mqtt_clean_session函数中进行。首先查看了一下这个函数中,里面释放mqtt_ack_handler_list链表上的内存,和mqtt_ack_handler_list链表上的内存。程序中其他地方都对此链表的操作进行了上锁,为了早点回家,其实已经晚了都8点多了,想是不是未连锁引起的,不管三七二十一先上锁保护再说。 ``` static void mqtt_clean_session(mqtt_client_t* c) { mqtt_list_t *curr, *next; ack_handlers_t *ack_handler; message_handlers_t *msg_handler; /* release all ack_handler_list memory */ if (!(mqtt_list_is_empty(&c->mqtt_ack_handler_list))) { LIST_FOR_EACH_SAFE(curr, next, &c->mqtt_ack_handler_list) { ack_handler = LIST_ENTRY(curr, ack_handlers_t, list); platform_memory_free(ack_handler); mqtt_subtract_ack_handler_num(c); } mqtt_list_del_init(&c->mqtt_ack_handler_list); } /* release all msg_handler_list memory */ if (!(mqtt_list_is_empty(&c->mqtt_msg_handler_list))) { LIST_FOR_EACH_SAFE(curr, next, &c->mqtt_msg_handler_list) { msg_handler = LIST_ENTRY(curr, message_handlers_t, list); msg_handler->topic_filter = NULL; platform_memory_free(msg_handler); } mqtt_list_del_init(&c->mqtt_msg_handler_list); } mqtt_set_client_state(c, CLIENT_STATE_INVALID); } ``` 我快速的给这段代码加上了锁保护,进行了观察打印信息,只是出现的内存增长的时间长了一些,问题还是未解决。一看表8点30,走吧,回家吧,晚饭再不吃就等着吃早饭了。看来问题就在mqtt_clean_session函数中了,范围已经小了很多,这里释放内存的还是有问题。 一路上无心看手机,打开手机手指滑动差,但是脑袋里在猜这段代码有什么问题?为什么猜呢?因为这段代码我没看过,强迫症的先猜测一番。 到家了,快速的吃过晚饭,已经9点了,坐在沙发上休息了一会,打开电脑,远程公司电脑,细细查看起来了这两个链接内存的申请,释放的所有代码,最后发现在mqtt_ack_handler_list链表上的节点中有一个handler变量也是占用的动态内存未释放。修改代码如下,运行调试。此时已经22:50,我在家里站桩静静等待,直到23:15,内存占用一直稳定在used memory : 75932。大boss找到,打败了。上床睡觉,头脑经过高速运转思考,在床上几无睡意,躺了1小时后,起来又看看一下程序运行情况,内存占用依然稳定,问题是解决了。 ``` static void mqtt_clean_session(mqtt_client_t* c) { mqtt_list_t *curr, *next; ack_handlers_t *ack_handler; message_handlers_t *msg_handler; platform_mutex_lock(&c->mqtt_write_lock); /* release all ack_handler_list memory */ if (!(mqtt_list_is_empty(&c->mqtt_ack_handler_list))) { LIST_FOR_EACH_SAFE(curr, next, &c->mqtt_ack_handler_list) { ack_handler = LIST_ENTRY(curr, ack_handlers_t, list); if(ack_handler->handler) { platform_memory_free(ack_handler->handler); ack_handler->handler = RT_NULL; } platform_memory_free(ack_handler); mqtt_subtract_ack_handler_num(c); } mqtt_list_del_init(&c->mqtt_ack_handler_list); } /* release all msg_handler_list memory */ if (!(mqtt_list_is_empty(&c->mqtt_msg_handler_list))) { LIST_FOR_EACH_SAFE(curr, next, &c->mqtt_msg_handler_list) { msg_handler = LIST_ENTRY(curr, message_handlers_t, list); msg_handler->topic_filter = NULL; platform_memory_free(msg_handler); } mqtt_list_del_init(&c->mqtt_msg_handler_list); } platform_mutex_unlock(&c->mqtt_write_lock); mqtt_set_client_state(c, CLIENT_STATE_INVALID); } ``` 去掉一些调试代码,再进行程序运行测试,此时已经1:30分,有些困了,上床睡觉吧。这一天从早上9:30开始到第二天1:30分,解决内存泄露的问题。 ### 5、事后总结 事后总结就是你看到的这篇文章,通过复盘,对于最开始使用的通过查看未释放的内存数据内容也有了新方法,开始时直接从内存数据查,确实很难看到是哪个程序申请未释放的。现在来看,可以给申请的内存代码加上标记,比较mqtt软件包申请内存时,再多申请4个字节内存,写入字符串“MQTT”,释放时把这个4个字节清零,这样在查看内存中数据时,看到未释放的内存中有MQTT字样就知道是谁申请的了。 至此MQTT软件包 (https://github.com/longtengmcu/kawaii-mqtt)已经做长连接,短连接的测试,发布了v1.3.0稳定版本,具体见GIT仓库,软件的已经完善的十分稳定可靠,经过了产品开发的实际验证,可以做为产品开的各种实际应用了。
5
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
fhqmcu
个人博客:https://blog.csdn.net/fhqlongteng
文章
16
回答
60
被采纳
1
关注TA
发私信
相关文章
1
umqtt 软件包使用后,连接上emqx服务器,过一会儿就掉线了
2
使用正点原子的 潘多拉 开发板 的例程19_iot_mqtt
3
mqtt软件包,不支持直接关闭?
4
kawaii_mqtt 申请内存崩溃
5
_signal_entry() 函数中dbg_enter在哪里定义呢?
6
start to connect mqtt server 失败
7
MQTT 在“ read 0:1, break “后断开重连
8
paho_mqtt线程相关疑问
9
RT thread studio kawaii mqtt 无法连接EMQ
10
调试bc26 ,断言错误failed at rt_thread_timeout
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
ulog
C++_cpp
at_device
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
13
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
11
个答案
1
次被采纳
本月文章贡献
程序员阿伟
8
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
5
次点赞
ThinkCode
1
篇文章
1
次点赞
Betrayer
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部