记一次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
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮