记一次解决MQTT软件包内存泄露的心路历程

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个字节),查看内容也无法识别。

记一次解决MQTT软件包内存泄露的心路历程_第1张图片

       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软件包内,

 记一次解决MQTT软件包内存泄露的心路历程_第2张图片

      问题范围进一步缩小了,已经定位到了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仓库,软件的已经完善的十分稳定可靠,经过了产品开发的实际验证,可以做为产品开的各种实际应用了。

你可能感兴趣的:(RT,Thread,编程经验,物联网,MQTT,内存泄露)