打印
[应用相关]

lwip2.1.2 存在的内存泄漏调试

[复制链接]
1305|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
wiba|  楼主 | 2023-10-18 15:12 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
第一次使用lwip,调试的时候故意将信号量以及内存给小,便于发现可能存在的泄漏,通过测试发现了两种泄漏情况,一个是接收数据包(应用层不处理接收,相当于只发送)存在溢出风险,一个是新建连接的时候,消息邮箱存在溢出,这2个溢出查了很久,中间的过程我就不一一道明了,只讲结果,就是sys_mbox_valid()接口的问题,这个接口是用于检查一个消息邮箱是否有效,通过这次调试也算是把lwip机制弄清楚了,一切都是围绕着这个消息邮箱的,在数据接收的时候,会申请内存,将内存指针写入到消息邮箱队列,lwip核心线程会读取队列,处理数据,当你只发送不接收,或者接收数据量太大,不足以处理的时候,这个时候消息队列是可能存在满了的,而这个sys_mbox_valid()接口很关键,他只有2个状态,有效或者无效,当邮箱满了的时候,必须返回无效,让lwip知道现在不能再写入消息了,这个时候lwip会将收到的数据释放掉,不会再写入到消息邮箱中了,流程如下:

首先是,完成了从网卡接收数据包,申请内存pbuf,写入到连接所在的邮箱队列

if (isLwIP_Init == TRUE)                                //LWIP初始化完成了才进行数据接收处理
        {

        p = pbuf_alloc(PBUF_RAW, RxLen, PBUF_POOL);   //从内存池分配内存


        if (p != NULL)
        {
            memcpy((u8_t*)((u8_t*)p->payload), pData, RxLen);
            p->tot_len = p->len = RxLen;                    //重新设置接收数据长度
            ethernetif_input(p, &xnetif);                   //调用lwip数据接收处理
        }
        else
        {
            DEBUG("内存不足\r\n");
        }
        }

如果数据被正常的应用层接收读取后,就会被释放(这只是个接收数据的例子,但是数据pbuf肯定要释放掉)



//接收数据回调,注意:接收数据必须存放在RTU_FRAME_BUFF中
int lwip_ReadData(u8** pDataBuff, u8 ByteTimeOut, u16 TimeOutMs, u16* pReceiveDelay)
{
        err_t err;
        struct netbuf* buf;
        void* data;
        u16_t len;

        if (pReceiveDelay != NULL) *pReceiveDelay = 0;
        err = netconn_recv(sg_conn, &buf);        //阻塞等待接收数据
        if (err == ERR_OK) //接收成功
        {
                if ((err = netbuf_data(buf, &data, &len)) == ERR_OK)
                {
                        memcpy(RTU_FRAME_BUFF, (u8*)data, len);
                        netbuf_delete(buf);
                        *pDataBuff = RTU_FRAME_BUFF;

                        return len;
                }
                else
                {
                        printf("err:netbuf_data(buf, &data, &len):%d.\r\n", err);

                        return 0;
                }


        }
        else if (ERR_TIMEOUT == err) //接收超时了
        {
                return 0;
        }
        else
        {
                printf("连接出现了错误:%d.\r\n", err);
                return -1;
        }
}

正常是不存在什么问题的,但是一旦接收数据量大了,或者你不处理接收的数据,这个时候消息邮箱满了,sys_mbox_valid()函数就会返回无效(不能返回有效,返回有效会导致邮箱溢出,内存泄漏更严重),这个时候接收到的数据,在lwip的api_msg.c文件的 static err_t recv_tcp(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) 函数内,如果有效无效,就释放掉了;

if (!NETCONN_MBOX_VALID(conn, &conn->recvmbox)) {
    /* recvmbox already deleted */
    if (p != NULL) {
      tcp_recved(pcb, p->tot_len);
      pbuf_free(p);
    }
    return ERR_OK;
  }
所以你的邮箱满了,必须返回无效,如果返回有效,就会将带有pbuf指针的消息写入进去,而实际上并未写入成功,因此***也读取不出来,也不可能被释放,内存就这么泄漏掉了,随着运行时间的增加,内存泄漏会变多,但是实际上由于sys_mbox_valid()的返回无效,就避免了内存的泄漏;

但是也正是因为 sys_mbox_valid()在消息队列满了后,返回无效,也带来了两个方面的泄漏,1是没有取出正处于消息邮箱队列中的数据,这些数据都是申请额内存,会随着netconn_delete(sg_conn);调用而泄漏,还有一个就是你的conn创建的时候一起创建的消息邮箱,也会随着netconn_delete(sg_conn);调用而一起泄漏,原因就是netconn_delete(sg_conn);会检查消息邮箱是否有效,如果有效,就会一一读取出当前消息邮箱队列中的数据,并将内存一一释放,最后将消息邮箱删除,释放内存,但是如果你的消息邮箱当前处于满状态, sys_mbox_valid()返回无效,就会导致netconn_delete(sg_conn);不再释放内存,也不会删除掉邮箱,这个时候就造成了泄漏;

netconn_delete()函数调用的netconn_prepare_delete(),这个函数调用的netconn_apimsg(),里面又调用err = tcpip_send_msg_wait_sem(fn, apimsg, LWIP_API_MSG_SEM(apimsg));是个回调函数里面进行释放内存的,通过仿真可以找到位置,在api_msg.c的netconn_drain()函数里面完成的;

  /* Delete and drain the recvmbox. */
  if (sys_mbox_valid(&conn->recvmbox)) {
    while (sys_mbox_tryfetch(&conn->recvmbox, &mem) != SYS_MBOX_EMPTY) {
#if LWIP_NETCONN_FULLDUPLEX
      if (!lwip_netconn_is_deallocated_msg(mem))
#endif /* LWIP_NETCONN_FULLDUPLEX */
      {
#if LWIP_TCP
        if (NETCONNTYPE_GROUP(conn->type) == NETCONN_TCP) {
          err_t err;
          if (!lwip_netconn_is_err_msg(mem, &err)) {
            pbuf_free((struct pbuf *)mem);
          }
        } else
#endif /* LWIP_TCP */
        {
          netbuf_delete((struct netbuf *)mem);
        }
      }
    }
    sys_mbox_free(&conn->recvmbox);
    sys_mbox_set_invalid(&conn->recvmbox);
  }

上面就是核心了,先判断邮箱是否有效

if (sys_mbox_valid(&conn->recvmbox))

由于你的邮箱当前处于满状态,所以返回的无效,那么不好意思,你的邮箱内的数据我不会管了,你的这个邮箱我都不会删除了;

目前我的解决办法是,在竟可能不改动lwip代码的情况下,自定义的消息邮箱结构体中增加一个标记,如下:


//LWIP消息邮箱结构体
typedef struct
{
    OS_EVENT* pQ;                                    //UCOS中指向事件控制块的指针
    void* pvQEntries[LWIP_ONE_QUEUE_SIZE];//消息队列 MAX_QUEUE_ENTRIES消息队列中最多消息数
    bool isValid;                   //自定义一个标志位,用于连接关闭后设置为无效,以便当邮箱溢出时依旧可以告诉netconn_delete()邮箱有效,好让netconn_delete()释放内存,删除邮箱
}lwip_mbox;
这个 isValid就是我自定义添加的,当消息邮箱被创建的时候,将isValid初始化为TRUE;

//创建一个消息邮箱
//*mbox:消息邮箱
//size:邮箱大小
//返回值:ERR_OK,创建成功
//         其他,创建失败
err_t sys_mbox_new(sys_mbox_t* mbox, int size)
{
    static u32 cnt = 0;

    //uart_printf("OSQCreate:%d\r\n", cnt++);


    if (size > LWIP_ONE_QUEUE_SIZE)size = LWIP_ONE_QUEUE_SIZE;                //消息队列最多容纳LWIP_MAX_QUEUES_COUNT消息数目
    mbox->pQ = OSQCreate(&(mbox->pvQEntries[0]), size);                          //使用UCOS创建一个消息队列


    uart_printf("OSQCreate:0x%X  %d\r\n",(u32)mbox->pQ, cnt++);

    LWIP_ASSERT("OSQCreate", mbox->pQ != NULL);
    if (mbox->pQ != NULL)
    {
        mbox->isValid = TRUE;                           //有效状态
        return ERR_OK;                                 //返回ERR_OK,表示消息队列创建成功 ERR_OK=0
    }
    else
    {
        mbox->isValid = FALSE;                          //无效状态
        return ERR_MEM;                                                                  //消息队列创建错误
    }
}

然后在netconn_delete()函数中,增加一行代码 conn->recvmbox.isValid = FALSE;  //消息邮箱不再被使用了

/**
* @ingroup netconn_common
* Close a netconn 'connection' and free its resources.
* UDP and RAW connection are completely closed, TCP pcbs might still be in a waitstate
* after this returns.
*
* @param conn the netconn to delete
* @return ERR_OK if the connection was deleted
*/
err_t
netconn_delete(struct netconn *conn)
{
  err_t err;

  /* No ASSERT here because possible to get a (conn == NULL) if we got an accept error */
  if (conn == NULL) {
    return ERR_OK;
  }

  //2021-08-13 增加一个状态标记
  conn->recvmbox.isValid = FALSE;  //消息邮箱不再被使用了

#if LWIP_NETCONN_FULLDUPLEX
  if (conn->flags & NETCONN_FLAG_MBOXINVALID) {
    /* Already called netconn_prepare_delete() before */
    err = ERR_OK;
  } else
#endif /* LWIP_NETCONN_FULLDUPLEX */
  {
    err = netconn_prepare_delete(conn);
  }
  if (err == ERR_OK) {
    netconn_free(conn);
  }
  return err;
}

然后修改sys_mbox_valid()函数,一旦消息邮箱是有效的,但是isValid==FALSE就直接返回有效,因为这个时候意味着连接已经被删除了,需要释放内存了,让netconn_delete()能知道邮箱还是有效的,将邮箱里面没有处理的数据全部读取出来,并进行释放,同时删除掉邮箱;


//检查一个消息邮箱是否有效
//*mbox:消息邮箱
//返回值:1,有效.
//      0,无效
int sys_mbox_valid(sys_mbox_t* mbox)
{
    sys_mbox_t* m_box = mbox;
    u8_t ucErr;
    int ret;
    OS_Q_DATA q_data;

    if (m_box==NULL || m_box->pQ == NULL)
    {
        //DEBUG("OSQQuery:异常\r\n");

        return 0;   //消息队列无效了
    }

    memset(&q_data, 0, sizeof(OS_Q_DATA));
    ucErr = OSQQuery(m_box->pQ, &q_data);
    if (ucErr == OS_ERR_NONE && mbox->isValid == FALSE)
    {
        printf("邮箱要被删除了,直接返回1!\r\n");
        return 1;   //邮箱需要被删除了,告诉lwip邮箱还是有效的
    }

   // uart_printf("消息数量:%d\r\n", q_data.OSNMsgs);
    ret = (ucErr < 2 && (q_data.OSNMsgs < q_data.OSQSize)) ? 1 : 0;
    if (q_data.OSNMsgs >= LWIP_ONE_QUEUE_SIZE)//超过队列大小了
    {
        printf("LWIP_ONE_QUEUE_SIZE(%d) full !please increase!!\r\n", q_data.OSNMsgs);//提示需要增大LWIP_ONE_QUEUE_SIZE.
    }
    return ret;
}

如果sys_mbox_valid()能返回3个状态,有效(可写),无效(不可写),满(不能写)三个状态,就不存在这个问题了,当然这个只要你的邮箱不满(个很大),也不存在这个问题了;
————————————————
版权声明:本文为CSDN博主「cp1300」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cp1300/article/details/119726408

使用特权

评论回复
沙发
Jacquetry| | 2023-10-18 19:44 | 只看该作者
这个问题是怎么产生的啊

使用特权

评论回复
板凳
Jacquetry| | 2023-10-18 19:44 | 只看该作者
这个问题是怎么产生的啊

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

78

主题

3313

帖子

3

粉丝