记一次lwip中 遇到 pcb == pcb->next 的pcb死循环debug过程

2019-07-14 07:41发布

amoBBS 阿莫电子论坛

标题: 记一次lwip中 遇到 pcb == pcb->next 的pcb死循环debug过程 [打印本页]

作者: kayatsl    时间: 2013-10-11 17:44
标题: 记一次lwip中 遇到 pcb == pcb->next 的pcb死循环debug过程
本帖最后由 kayatsl 于 2013-10-11 17:50 编辑 

[attach]144489[/attach]

如图中, 在tcp_slowtmr() 中, 历遍 tcp_tw_pcbs 过程中, 其中一个 pcb -> next 指向pcb自身

导致while循环永久死在这里跳不出去, 程序死机..     这种问题, 虽然说必然会发生, 但发生的时间随机性很强, 很难捕捉到什么时候一定会出现, 所以这种bug很难解决

这种情况在国外论坛搜了下, 都是同一个回答: 当跑os的时候,有多个线程同时争抢就会导致这种问题产生

但到底怎么产生的, 和如何避免, 没有作详细介绍,也没有案例分析..

这现象以前也有遇到过, 其实这种情况通过写一个定时检测是否有pcb指向自身 和加入看门狗, 也能保证程序不会一直死在这,但这次一个偶然的机会,决定跟个所以然.


[attach]144490[/attach]
这次由于时间片之间程序运行的先后顺序不同, 会导致hardfault产生, hardfault没有截图, 系统是记录到我读取一个非法地址, 引致hardfault

从堆栈中跟踪得知, hardfault前跑的函数是 memp_malloc 中 memp_tab[type] = memp->next 引起的, 而memp 自身被指向了一个非内存地址, 再读取 ->next 必然导致出错

蓝 {MOD}圈是我加入的条件断点, 来捕捉异常状态.

这里得知memp_tab[2] 中的地址指向出问题了, 但这和 pcb的死循环有什么关系呢? 到这里还是看不出头绪.. 思路断了, 那就回去继续跟踪 pcb的问题

[attach]144491[/attach]

既然已经知道死循环的 pcb是属于 tcp_tw_pcbs 链表里面的, 那就 ctrl+shift+f 把所有对 tcp_tw_pcbs 有操作的地方都找出来, 统统下条件断点

看执行到哪一句后会出现死循环的现象

程序果然乖乖的死在陷阱里面, 这样看来, 明显是运行了 tcp_reg 后才出现的现象, 但为什么运行这个后会导致死循环呢? 没什么头绪? 那就跟另一条线索


[attach]144502[/attach]

跟踪一下 memp_tab的情况,

图中红字部分已经说得差不多了,  这里给了我们一个很重要的线索, 就是 memp_tab[]->next 和 tcp_tw_pcbs -> local_ip 为什么会重叠了..

试图跟着这个函数返回一下, 看看会有什么情况

[attach]144493[/attach]

没错, 这里又重新给这个pcb赋值了, 然后又重新递回到上层做处理..

也就是说, 系统给我们分配了一个 正在使用的内存区域, 来处理新的东西, 那必然会导致和旧的东西发生冲突.. 要验证这句话? 继续下条件断点

[attach]144494[/attach]

图中可以看到, 进来的pcb, 本来是属于 active_pcbs 链表的, 刚从 active 链表中移除, 准备放到 timewait链表里面去, 但此时发现, 这个pcb的内存空间,正是tw_pcbs的内存空间

其实 TCP_REG 很简单, 就是把指针指一下, 就注册过去了,  但就是因为太简单了, 没有做任何判断, 所以自己指回了自己也不知道.

其实到这里, 可以在这个定义里面写个判断, 遇到指向自己的就不进行操作, 但这样还是没找到最原始导致的原因..  所以必须继续跟踪下去

[attach]144495[/attach]

再截一个图, 就是 死循环的图..  大家可以比较一下这几个图里面红圈圈着的地址,  截图顺序是按照程序执行顺序来的


[attach]144496[/attach]

回想一下, malloc的时候, 分配到同一个地址空间, 那明显就是上一次分配的时候, ->next指针又指回自己了, 验证这个猜想, 继续下条件断点..

果然不出所料, memp == memp->next , 而这个是分配的时候导致的还是释放的时候导致的, 暂时还不确定,

但是这个出错的现场, 可以在堆栈中继续追溯是哪里调用的, 但这里还不是第一现场, 为了还原第一现场, 那就继续跟着 callstack回去找

[attach]144497[/attach]

这图中很有意思..  这里程序开始的时候, 我使用 tcp_new 创建一个pcb, 而 tcp_new 又调用 malloc 来取出一个 pcb的内存空间

而我在 tcp_abort 这里打了个断点, 按道理来说, 我还没有 abort掉这个链接, 空间是不应该被释放掉的, 但奇怪的是 memp_tab[2] 中指向的地址,确实是我正在使用的地址

memp_tab[] 链表是空闲内存的链表, 而这个内存块是我正在使用的, 怎么会出现在空闲链表中??

越来越接近真相了,  就是这块内存申请后, 在使用中, 意外被别人释放了, 那罪灰祸首到底是谁??

[attach]144508[/attach]

那就继续下条件断点去捕捉释放前的现象,. 

图中可以看到, memp_Free 地址是跟 memp_alloc一样的 ..  这里的原因是,我链接很频繁,一个pcb还没完全关闭, 又建立另一个链接, 所以会导致刚刚free出来的一个块, 马上又被用来使用了

所以这两个地址一样并不惊讶, 惊讶的是, 同一个地址, 为什么会出现在 memp_tab[2] 中???

这明显是 free完这个块, 然后 malloc到来用, 但用的过程中, 还没等我释放, 就被别的地方释放了, 所以导致这块正在使用的内存块会跑到了空闲内存块中去..

可以断定是free多了一次出问题了, 就好办了..

[attach]144499[/attach]

由上面的现象可以估计 是pcb的内存空间刚刚分配到, 很短时间又被释放了, 既然是这样, 就在 memp_free中下条件断点  当现在free的,刚好是刚刚malloc出来的内存块就断

因为正常情况下, free的肯定是旧的空间, 不可能刚刚malloc就被free掉的.  

等了一段时间, 中陷阱了..  其实看到这个现场没什么作用, 本来就已经猜到的事情,  而在这里下断点的目的, 是要跟踪, 到底是谁把它释放掉的.

看 call stack 就一目了然了..  继续跟回去 tcp_input中

[attach]144500[/attach]

这时候清晰了.. 这个pcb收到了关闭请求, 然后关闭掉pcb链接, 顺便把内存区域也 free掉了..




好了, 看到这里,  其实大家往回读就知道到底 pcb死循环是怎么引起的了...

我正在开启一条到服务器的链接, 但在等待确定链接已经保持的过程中, 意外被服务器关闭了, 而由于时间太短, 这时候, 我应用程序认为 服务器并没有连接上, 然后把这个tcp的控制块 abort掉

而 abort 的时候调用 free 内存的函数,  却不知道这块内存其实已经是free掉的了, 再free一次, 就会出现 memp_tab中 元素的 -next 指回自身.

而这时候再 malloc一个新内存空间的时候, 由于 next永远是指回同一块内存区域, 所以导致下次再分配的时候, 还是分配同一块空间给应用层

但应用层不知道分配的是同一块空间, 而继续使用, 然后就导致两条链接共用同一个内存区域.. 但lwip自己并不知道.

当两条链接最终注册入同一个 pcbs链表去管理的时候, 就会导致自己指回自己, 因为本来就是同一块内存,.. 最后就 pcb死循环了..

跟踪到此结束




跟踪完当然是解决问题了..

[attach]144503[/attach]

在 memp_free中 ,free之前做一个判断 由于type == 2 属于 tcp_pcb, 而 tcp_pcb 的 前四个字节是不可能为0x00的, 

所以可以判断为当 memp->next 为 NULL 的时候, 这片内存已经释放过了, 就不再重新释放, 直接跳到程序末尾, 解锁

经验证, 方法正常, 因为 出问题通常是 tcp_pcb 的问题,因为 tcp控制太复杂了, 所以目前只控制 type==2 的地方就可以保证程序正常了.

到此 结贴.
作者: kayatsl    时间: 2013-10-11 18:04
大家有什么好方法避免的..  提一下.. 集思广益啊...
作者: 圣骑士by    时间: 2013-10-11 18:06
我仅顶一下 表示我看不懂……
作者: kayatsl    时间: 2013-10-11 19:21
因为内存空间是直接分配出来的..  所以最后解决只能这么判断

不过呵..

/* No sanity checks
* We don't need to preserve the struct memp while not allocated, so we
* can save a little space and set MEMP_SIZE to 0.
*/
#define MEMP_SIZE           0

这个memp_sizze 其实可以定义一下, 这里定义了4的话 , 每个头部都会有4个空余字节.

然后每次 在malloc中 memp = (struct memp*)((u8_t*)memp + MEMP_SIZE); 之前, 都先 memp->next = 赋一个固定的值;

然后 free的时候, 检查下 ->next 指针 如果不是这个固定值的话, 就说明已经被free过了, 如果还是固定值的话, 就free一下..

理论上想了下, 应该可以这么做, 只是每个区间都再加4个字节的话.. 这片内存区域耗用还是挺庞大的..
作者: kayatsl    时间: 2013-10-11 19:28
网上都说 多线程访问导致的, 而这次的情况, 确实也是因为两条线程都对一个对象访问导致的..

但由于我调用的是 rawapi , 所以在connect的时候, 必须vtaskdelay 将cpu控制权让出来给 lwip内核来跑, 才能connect上服务器

这个等待时间很难断定 , 网速无法保证, 而就在等待过程中, 刚连上服务器,又被服务器踢掉, 踢掉的时候, 是用lwip的内核线程来处理的.

所以...不从内存分配的方向去解决的话, 这问题基本是无解了...

希望给遇到同样问题的人, 看到之后, 能快速定位到自己程序哪里引起这问题吧..

先写到这了...
作者: zf8848    时间: 2013-10-11 21:25
感谢楼主共享自己的经验, lwip 确实有一些莫名奇妙的 bug.
作者: fengyunyu    时间: 2013-10-11 21:57
这个分析很到位啊。前面有个帖子似乎也提到了同样的问题。
作者: yiyu    时间: 2013-10-11 21:57
本帖最后由 yiyu 于 2013-10-11 22:09 编辑 

lwip是单线程的,所有api都是通过队列通讯的,使用row是在裸奔情况下,应用和lwip在一个线程内,
作者: fengyunyu    时间: 2013-10-11 22:04
yiyu 发表于 2013-10-11 21:57 
lwip是单线程的,所有api都是通过队列通讯的,

lwip是单线程,如何理解?是不能用在使用操作系统的多任务的环境中?还是指?
作者: yiyu    时间: 2013-10-11 22:17
本帖最后由 yiyu 于 2013-10-11 23:19 编辑 

lwip在os中作为一个独立线程实现,外部应用只能使用api调用,可以看一下api的实现,很多都是把row函数通过消息队列发送到lwip线程调用
api投递消息
err_t
tcpip_apimsg(struct api_msg *apimsg)
{
  struct tcpip_msg msg;
  
  if (mbox != SYS_MBOX_NULL) {
    msg.type = TCPIP_MSG_API;
    msg.msg.apimsg = apimsg;
    sys_mbox_post(mbox, &msg);
    sys_arch_sem_wait(apimsg->msg.conn->op_completed, 0);
    return ERR_OK;
  }
  return ERR_VAL;
}

lwip线程
static void
tcpip_thread(void *arg)
{
  struct tcpip_msg *msg;
  LWIP_UNUSED_ARG(arg);

#if IP_REASSEMBLY
  sys_timeout(IP_TMR_INTERVAL, ip_reass_timer, NULL);
#endif /* IP_REASSEMBLY */
#if LWIP_ARP
  sys_timeout(ARP_TMR_INTERVAL, arp_timer, NULL);
#endif /* LWIP_ARP */
#if LWIP_DHCP
  sys_timeout(DHCP_COARSE_TIMER_MSECS, dhcp_timer_coarse, NULL);
  sys_timeout(DHCP_FINE_TIMER_MSECS, dhcp_timer_fine, NULL);
#endif /* LWIP_DHCP */
#if LWIP_AUTOIP
  sys_timeout(AUTOIP_TMR_INTERVAL, autoip_timer, NULL);
#endif /* LWIP_AUTOIP */
#if LWIP_IGMP
  sys_timeout(IGMP_TMR_INTERVAL, igmp_timer, NULL);
#endif /* LWIP_IGMP */
#if LWIP_DNS
  sys_timeout(DNS_TMR_INTERVAL, dns_timer, NULL);
#endif /* LWIP_DNS */

  if (tcpip_init_done != NULL) {
    tcpip_init_done(tcpip_init_done_arg);
  }

  LOCK_TCPIP_CORE();
  while (1) {                          /* MAIN Loop */
    sys_mbox_fetch(mbox, (void *)&msg);
    switch (msg->type) {
#if LWIP_NETCONN
    case TCPIP_MSG_API:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: API message %p ", (void *)msg));
      msg->msg.apimsg->function(&(msg->msg.apimsg->msg));
      break;
#endif /* LWIP_NETCONN */

    case TCPIP_MSG_INPKT:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: PACKET %p ", (void *)msg));
#if LWIP_ARP
      if (msg->msg.inp.netif->flags & NETIF_FLAG_ETHARP) {
        ethernet_input(msg->msg.inp.p, msg->msg.inp.netif);
      } else
#endif /* LWIP_ARP */
      { ip_input(msg->msg.inp.p, msg->msg.inp.netif);
      }
      memp_free(MEMP_TCPIP_MSG_INPKT, msg);
      break;

#if LWIP_NETIF_API
    case TCPIP_MSG_NETIFAPI:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: Netif API message %p ", (void *)msg));
      msg->msg.netifapimsg->function(&(msg->msg.netifapimsg->msg));
      break;
#endif /* LWIP_NETIF_API */

    case TCPIP_MSG_CALLBACK:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: CALLBACK %p ", (void *)msg));
      msg->msg.cb.f(msg->msg.cb.ctx);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;

    case TCPIP_MSG_TIMEOUT:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: TIMEOUT %p ", (void *)msg));
      sys_timeout(msg->msg.tmo.msecs, msg->msg.tmo.h, msg->msg.tmo.arg);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;
    case TCPIP_MSG_UNTIMEOUT:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: UNTIMEOUT %p ", (void *)msg));
      sys_untimeout(msg->msg.tmo.h, msg->msg.tmo.arg);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;

    default:
      break;
    }
  }
}


作者: kayatsl    时间: 2013-10-11 23:19
如果是用 netconn 来做的话, 确实是通过调用 tcpip_apimsg 来传递信息

但也可以直接调用raw_api 和设置自己的回调函数来做处理   lwip本身也是提供 raw_api 来给应用做更快速的处理的..  处理得到位就不会出现争抢的现象了.
作者: fengyunyu    时间: 2013-10-12 11:05
kayatsl 发表于 2013-10-11 23:19 
如果是用 netconn 来做的话, 确实是通过调用 tcpip_apimsg 来传递信息

但也可以直接调用raw_api 和设置自 ...

lwip到底能否商用还值得商榷。附网上看到一篇文章。


成功实现了LWIP的keepalive功能

原来的lwip没有启动keepalive功能,导致tcp客户端工作不可靠,主要就是无法处理服务器的断线、断网、down机等异常。表现是服务器故障后,tcp客户端总是等待无法返回,造成锁死。

处理方法:

1,使用TCPkeepalive功能,定时探测连接的状态,当发生掉线时,自动关闭连接,正常返回。

2,检测网线状态PHY寄存器中有标准位。(没有上一种方法好。)

下面详细介绍如何启动keepalive

1,打开keepalive的标志使能。

2,修改keepalive各个计数值,主要是改小,方便测试。

3,在pcb中需要置位keepalive的一个选项。pcb->so_options |= SOF_KEEPALIVE;

4,修改pcb的一处bug,该bug可以通过给我汇款获得。

启动了keepalive才是真正的tcp连接,用起来稳定可靠,异常爽快。


by 天远易 发表于:2012/7/13 10:04:43 

作者: kayatsl    时间: 2013-10-12 11:33
fengyunyu 发表于 2013-10-12 11:05 
lwip到底能否商用还值得商榷。附网上看到一篇文章。

keepalive 只是一个心跳包的作用..

本身应用层有设计心跳包机制, 就不需要底层再做了..


作者: fengyunyu    时间: 2013-10-12 11:35
kayatsl 发表于 2013-10-12 11:33 
keepalive 只是一个心跳包的作用..

本身应用层有设计心跳包机制, 就不需要底层再做了..

原文中的“4,修改pcb的一处bug,该bug可以通过给我汇款获得。"
作者: kayatsl    时间: 2013-10-12 13:58
fengyunyu 发表于 2013-10-12 11:35 
原文中的“4,修改pcb的一处bug,该bug可以通过给我汇款获得。"

哈哈哈... 肯定不是这个bug... 严格来说 这个bug并不属于pcb自身的..
作者: DOER    时间: 2013-10-12 20:50
密切关注中......
作者: SNOOKER    时间: 2013-10-12 22:10
高级技术帖,顶起
作者: farmerzhangdl    时间: 2013-10-14 11:11
我最近出现了一个lwip的问题,也是连接上的。。。还在查
作者: farmerzhangdl    时间: 2013-10-14 11:12
keepalive并不一定需要,网上那个说开启了keepalive才是真的tcp我认为是错误的,完全可以通过自定义心跳包解决
作者: kayatsl    时间: 2013-10-14 11:16
farmerzhangdl 发表于 2013-10-14 11:12 
keepalive并不一定需要,网上那个说开启了keepalive才是真的tcp我认为是错误的,完全可以通过自定义心跳包 ...

哎.. 现在还是会遇到 指向自己, 只不过导致的原因不是原来的地方, 跑到新的地方了.. 而且跑大半天甚至几天才能触发一次.. 更不好跟了
作者: fengyunyu    时间: 2013-10-14 11:53
本帖最后由 fengyunyu 于 2013-10-14 17:38 编辑 
kayatsl 发表于 2013-10-14 11:16 
哎.. 现在还是会遇到 指向自己, 只不过导致的原因不是原来的地方, 跑到新的地方了.. 而且跑大半天甚至几 ...


LZ是做Server还是做Client?据说做Server比较稳定,做Client则问题较多。
作者: kayatsl    时间: 2013-10-14 12:03
同时做 TCP的Client   UDP的Server 和 Client
作者: kayatsl    时间: 2013-10-14 12:04
fengyunyu 发表于 2013-10-14 11:53