尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

LwIP netconn_connect卡死排查与超时机制解决方案

LwIP netconn_connect卡死排查与超时机制解决方案 1. 卡死现场还原程序停在netconn_connect()时发生了什么1.1 典型复现场景断网重连时最常遇到如果你调试过带以太网、Wi-Fi 或蜂窝模块的嵌入式设备大概率碰到过这种场景设备上电后第一次连接服务器一切正常但一旦网络中途断开比如路由器重启、网线被拔掉、4G 信号短暂丢失程序再走到netconn_connect()就再也不动了。任务管理器里看这个线程状态一直是阻塞等待。调试串口最后一条日志停在connect start这行后面什么都没打出来。程序没有崩溃没有进 HardFault其他任务还在运行但网络线程就像被按了暂停键。我最初遇到这个问题是在一个工业数据采集器项目上MCU 跑的是 FreeRTOS LwIP连接远端 MQTT Broker。现场反馈说设备运行几小时后就会掉线而且掉线之后永远重连不上必须断电重启。刚开始怀疑是看门狗没喂导致复位但查看日志发现系统没有复位就是任务卡死。后来用调试器挂上去查看任务栈发现线程停在了netconn_connect()里函数调用栈最后几帧是netconn_connect netconn_apimsg sys_arch_sem_wait那一刻我就明白了问题不是业务逻辑而是 LwIP netconn API 的阻塞机制惹的祸。1.2 第一反应式排查先确认卡在哪一层遇到程序停在某个函数里我建议先做三步定位不要急着改代码。这三次定位能帮你分清到底是协议栈问题、操作系统调度问题还是业务死循环。第一步确认代码确实还在跑。用调试器暂停 CPU查看当前活跃线程列表。FreeRTOS 下可以用uxTaskGetSystemState()或者直接看调试器的线程视图确认网络任务的状态是Blocked还是Ready。如果整个系统都死了那是另一类问题。第二步查看函数调用栈。确认是停在netconn_connect()的哪个阶段。如果栈顶是sys_arch_sem_wait说明在等待 tcpip_thread 返回消息如果栈顶在网卡驱动层说明在等待网卡互斥锁问题可能出在驱动。第三步加日志确认 tcpip_thread 的状态。在 tcpip_thread 的循环里加周期性的心跳变量主循环里每秒打一次。如果心跳还在增长说明协议栈主线程活着那问题就出在 netconn API 和 tcpip_thread 之间的消息通信上。如果心跳也停了说明 tcpip_thread 挂了那是另一个灾难通常跟内存踩踏有关。我见过很多人一看到卡死就怀疑 LwIP 配置、怀疑内存池折腾半天。其实先把这几步走完能省下大量时间。1.3 先分清真死还是假死内核还活着只是这个线程出不来这里要强调一个关键认知程序停在netconn_connect()不代表整个系统死机。更准确地说是这个线程被无限期阻塞了。其他优先级更高的任务可能还在跑看门狗可能还在喂外设中断也可能正常但网络相关的所有后续流程都卡住了。这种假死状态比真死更难排查因为从现象看像死机但底层系统还活着。你如果只盯着系统层面找问题永远找不到答案。正确的思路是把视角缩小到这个线程和 LwIP 协议栈的通信层。用生活化的类比理解netconn 线程就像一个在银行柜台等叫号的人。他递了申请单发消息给 tcpip_thread然后坐在椅子上等。如果柜台窗口一直不叫号他就在椅子上坐一辈子哪怕银行营业厅里其他业务都在正常办理。sys_arch_sem_wait就是那把椅子。所以核心问题变成为什么 tcpip_thread 这个柜台窗口一直不回应这才是我们需要解开的下一个扣子。2. netconn阻塞API为什么能卡这么久从调用链看超时机制2.1 netconn_connect()从调用到返回的完整执行路径要彻底搞懂卡死原因得先看清netconn_connect()的整套调用链。LwIP 的 netconn API 并不直接操作 TCP 控制块而是通过消息机制把请求转发给 tcpip_thread 处理。完整的调用路径是这样netconn_connect() - netconn_apimsg(do_connect) - sys_mbox_trypost(tcpip_thread.mbox, msg) // 把请求发给 tcpip_thread - sys_arch_sem_wait(msg-sem, timeout) // 等待 tcpip_thread 处理完并回发信号量tcpip_thread 收到消息后执行do_connect()do_connect() - tcp_connect(pcb, ipaddr, port, connected_callback) - tcp_enqueue_flags(pcb, TCP_SYN)这个设计的核心思路是netconn API 层不直接访问 PCB所有网络栈操作都放在单线程里串行执行。好处是协议栈内部不用加一堆锁坏处是 API 层和协议栈层之间多了一次线程间通信这次通信如果没有超时保护就成了卡死的温床。看清这条链路之后你会明白一个关键事实netconn_connect()能不能正常返回取决于两个条件。第一tcpip_thread 有没有及时处理消息第二协议栈底层连接尝试有没有结束。2.2 mbox等待没有超时recv_timeout的真正作用被大多数人忽略现在说到最核心的坑。很多开发者以为netconn_set_recvtimeout()只是设置接收数据的超时时间影响的是netconn_recv()的行为。但真实情况是这个超时设置会作用到所有经过 netconn_apimsg 的阻塞 API 调用上包括netconn_connect()、netconn_write()、netconn_close()。看一下 LwIP 源码中netconn_apimsg()的逻辑就能明白err_t netconn_apimsg(struct netconn *conn, api_msg_fn function) { struct api_msg msg; ... if (conn-recv_timeout ! 0) { /* 带超时等待信号量 */ if (sys_arch_sem_wait(msg.sem, conn-recv_timeout) SYS_ARCH_TIMEOUT) { conn-err ERR_TIMEOUT; return ERR_TIMEOUT; } } else { /* 永久等待直到 tcpip_thread 回应 */ sys_arch_sem_wait(msg.sem, 0); } ... }注意那个else分支sys_arch_sem_wait(msg.sem, 0)第二个参数 0 表示无限期等待。刚netconn_new()创建的连接recv_timeout默认为 0。如果你从头到尾没调过netconn_set_recvtimeout()你的netconn_connect()就会走这个永久等待分支。所以当 tcpip_thread 因为某种原因没有及时回应时调用线程就会无限期阻塞在sys_arch_sem_wait里。我说没有及时回应而不是不回应是因为在断网场景下tcpip_thread 不是不处理消息而是处理完之后底层 TCP 连接还卡在 SYN 重传阶段要等重传次数耗尽tcpip_thread 才会把错误返回给 API 层。这意味着即便 tcpip_thread 正常工作只要对端网络不可达connect 请求也要等到协议栈彻底放弃建连才能返回。默认配置下这个时间可能长得让你怀疑人生。2.3 协议栈底层SYN重传默认情况下connect最多能等多久很多人没仔细算过这个问题。LwIP 中控制 TCP SYN 重传次数的宏是TCP_SYNMAXRTX默认值是 6。SYN 发送之后如果对端没有回 SYN-ACK协议栈会每隔一段时间重传一次 SYN重传间隔是叠加的。从实际经验看LwIP 默认参数下对不可达 IP 发起netconn_connect()最坏情况等待时间能达到一两分钟甚至更久。对应用层来说这一两分钟就是卡死。更麻烦的是LwIP 还有一种情况目标 IP 在局域网内但主机不存在ARP 请求也可能先卡一阵子。ARP 表项超时时间通常要数十秒等 ARP 失败后 TCP 层才开始重传。两层等待叠加总时长更加夸张。所以你现在应该明白了程序停在netconn_connect()不一定是 bug而是阻塞 API 的设计特性 默认参数太长共同造成的表现。要解决它要么让 API 层有超时保护要么让协议栈底层尽快失败要么干脆换成非阻塞方式。3. 根因排查配置、线程、内存、死锁一个都不能漏3.1 lwipopts.h配置检查清单缺一个开关就卡一个版本既然前面提到消息机制第一个要排查的是lwipopts.h里的功能开关。哪怕你只要 TCP 客户端以下宏也必须是启用状态缺一个就可能导致异常宏作用建议值LWIP_NETCONN启用 netconn API 层1LWIP_TCP启用 TCP 协议栈1LWIP_SOCKET若要用 socket API需要开启1LWIP_NETIF_API网卡状态查询等扩展 API1LWIP_TCPIP_CORE_LOCKING决定 tcpip_thread 是否加锁访问根据架构选如果LWIP_NETCONN没开netconn_new()会直接失败不会走到卡死这步。但如果LWIP_TCP没开netconn_new(NETCONN_TCP)会拿到 NULL。这类问题通常早期就会暴露不太会等到运行时才卡死。真正值得警惕的是LWIP_TCPIP_CORE_LOCKING。如果开启了这个宏netconn API 层和 tcpip_thread 之间会走另一套锁机制。在某些移植层没有正确实现sys_mutex相关函数时会出现拿不到锁导致的隐性卡死表面上看起来和netconn_connect()卡住一模一样。另一个必须检查的是MEMP_NUM_NETCONN和MEMP_NUM_TCP_PCB它们决定了最多能同时创建多少个 netconn 和 TCP 控制块。如果之前的连接没有正常关闭而耗尽块新连接会创建失败。这类失败通常会返回错误码但在异常错误的处理分支里如果你直接while(1)或者重试等待外部表现也接近卡死。3.2 tcpip_thread真的在跑吗初始化顺序和栈大小的隐性影响接下来排查 tcpip_thread 本身。很多人会假设tcpip_init()调用之后 tcpip_thread 就一直在跑但现实中有两个细节容易出问题。第一个是初始化顺序。tcpip_init(NULL, NULL)必须在创建任何 netconn 之前完成。如果先netconn_new()后tcpip_init()netconn 层尝试向还不存在的 mbox 发消息可能导致未定义行为。更常见的坑是在tcpip_init之前就调用 netconn API但 LwIP 不会报编译错误运行时行为完全取决于内存状态表现就是偶尔卡死。第二个是 tcpip_thread 的栈大小。LwIP 默认创建 tcpip_thread 时用TCPIP_THREAD_STACKSIZE定义。在 lwipopts.h 里如果没显式配置不同移植层的默认值可能偏小特别是在调用do_connect()时如果触发 ARP 请求、TCP 重传定时器等路径栈压力会明显增加。一旦栈溢出tcpip_thread 会进入 HardFault 或者抛出堆栈溢出钩子结果同样表现为 tcpip_thread 不再响应消息。我当时排查的方式是在 tcpip_thread 主循环里放一个计数器每循环一次加一。然后写一个测试任务每 100ms 打印这个计数器的值。正常情况数字增长很快。如果数字停了就确认 tcpip_thread 已经死了。再配合vApplicationStackOverflowHook钩子基本能定位到是不是栈溢出。实测下来给 tcpip_thread 分配 1024 字节栈在某些场景确实会溢出调整到 2048 之后稳定很多。如果你的 LwIP 任务里还涉及 DNS 解析、DHCP、自动 IP 等功能2048 只是起步建议按需分配 4096。3.3 内存池不足的隐性失败分配失败不等于返回NULL第三个排查方向是内存池。LwIP 广泛使用内存池管理固定大小的对象协议栈内部出现内存不足时通常不会直接崩溃而是返回ERR_MEM。这个错误通过消息机制返回给 netconn 层后如果你代码里没做针对性的处理就可能出现看似卡住其实是错误处理路径重试的情况。常见与连接相关的内存池包括MEMP_NUM_TCP_PCBTCP 连接控制块数量MEMP_NUM_NETCONNnetconn 控制块数量MEMP_NUM_TCP_SEGTCP 报文段缓冲数量PBUF_POOL_SIZEPBUF 池大小一个很典型的隐患系统中有其他模块大量使用 TCP 连接但MEMP_NUM_TCP_PCB只有默认值 10 左右。当 TCP 控制块耗尽时tcp_alloc()返回 NULLdo_connect()最终返回ERR_MEM。netconn_connect()返回ERR_MEM后如果业务代码只处理ERR_OK和ERR_TIMEOUT其他错误码直接重试重试过程中所有连接块仍然被旧连接占用就会出现永久重试失败的假死现象。排查内存池的方法也很直接。把 lwipopts.h 里的MEMP_STATS和MEM_STATS打开运行一段时间后从串口打印内存池统计重点看maxused是否接近size。如果接近甚至相等说明内存池存在枯竭风险。我见过一个设备在长时间运行后 TCP_PCB 池耗尽每次重连都失败重启设备后恢复正常就是典型的池枯竭。3.4 在锁里等锁另一种被误判为卡死的死锁现场最后一种情况比较隐蔽但也很常见在 LwIP 的回调函数里直接调用阻塞的 netconn API。LwIP 有一个特性当使用LWIP_TCPIP_CORE_LOCKING时某些 API 允许在回调上下文中直接调用比如tcp_connect的回调里可以再调用 netconn API。但很多移植版本实际上不保证这种嵌套调用的安全性。如果连接的connected回调里又调用了netconn_connect()、netconn_write()或netconn_close()而这些调用内部需要访问 tcpip_thread 的消息队列就可能形成tcpip_thread 在等回调返回回调又等 tcpip_thread 处理消息的死锁局面。这类死锁的表现就是程序永久停住但断点看线程每个线程停在的位置可能各不相同。定位方式要看线程状态和锁持有关系tcpip_thread 可能在tcp_process的某个回调里业务线程可能在sys_arch_sem_wait。最稳妥的做法是在 LwIP 的协议栈回调里只做标记具体的 netconn 操作交给独立的任务处理。回调里用信号量或者消息队列通知业务任务业务任务再去执行连接、发送等操作。这能避开大量与锁和上下文相关的问题。4. 可落地的修复方案从简单到彻底4.1 方案一先用netconn_set_recvtimeout给API层上个保险丝如果你不想大改业务逻辑最省事的办法是用netconn_set_recvtimeout()给连接设置超时。前面说过它会影响netconn_connect()的等待行为这里直接给出完整的实现片段static err_t connect_with_timeout(struct netconn *conn, const ip_addr_t *addr, u16_t port, u32_t timeout_ms) { /* 设置 API 调用超时时间 */ netconn_set_recvtimeout(conn, timeout_ms); err_t err netconn_connect(conn, addr, port); /* 恢复为默认无限等待避免影响后续 recv 流程 */ netconn_set_recvtimeout(conn, 0); return err; }这个方案的优点很明显改动小只需给连接设置超时netconn_connect()在超时后返回ERR_TIMEOUT业务代码根据返回值走失败分支。几个细节需要注意第一recv_timeout是连接级别的属性会影响所有 netconn API 调用。如果连接建立成功后你把它设为 0后续netconn_recv又变回无限期阻塞。所以务必要在 connect 返回后恢复超时设置或者明确你的业务需要什么样的超时语义。第二超时时间的取值要看实际场景。连接一个公网服务器通常 3~5 秒比较合理连接局域网设备1~2 秒就够了。太短会导致弱网环境下连接失败率上升太长又起不到保护作用。第三返回ERR_TIMEOUT后这个连接不能直接复用。你需要调用netconn_delete()释放连接重新创建一个新的 netconn 再重试。实测中发现超时后用同一个连接再次 connect行为不确定可能与 netconn API 内部状态机有关。这个方案虽然简单但它本质上还是有限期阻塞不是真正的非阻塞。如果应用需要在等待连接期间做其他事情还要另想办法。4.2 方案二非阻塞connect 连接状态机的完整实现思路如果你希望 connect 调用立即返回、不阻塞任务需要把连接流程改成状态机。LwIP netconn API 里没有像 BSD socket 那种标准的EINPROGRESSselect()配合的方式但可以借助netconn_set_nonblocking()实现快速返回。基本流程是设置非阻塞模式调用netconn_connect()返回ERR_INPROGRESS或ERR_WOULDBLOCK说明连接在后台进行任务先做其他事情周期性检查连接是否建立成功问题在于第四步怎么检查。netconn API 没有直接暴露 TCP 状态的方法实战中有两种常见做法我分别说下优缺点。做法一周期性重新调用netconn_connect()如果返回ERR_ISCONN说明连接已经建立。这个方法被不少嵌入式工程师用过但依赖 LwIP 对错误码的具体实现不算标准的公开接口而且某些版本上行为可能有差异我只建议在测试可控、版本固定时使用。做法二改用 socket API 来实现非阻塞 connect select。这是更可控的路线放到下一小节单独讲。如果项目里必须使用 netconn API建议在netconn_connect()返回ERR_INPROGRESS之后用一个独立线程周期性检查连接netconn-pcb.tcp-state不过这一步需要访问内部结构体依赖 LwIP 版本且不够优雅。整体来说netconn API 的非阻塞支持比较鸡肋如果对非阻塞连接有强烈需求我的建议是直接跳到 socket API。4.3 方案三换用socket API select这条路为什么更稳socket API 是 LwIP 上层 API 中更接近 POSIX 标准的一层生态更成熟非阻塞支持和超时控制的方式也更成熟。虽然内部实现同样是 netconn但对外暴露了更完善的错误码和选项。这是默认带 5 秒超时的 connect 实现#include lwip/sockets.h static int connect_with_timeout(const ip_addr_t *ip, u16_t port, uint32_t timeout_ms) { int sock socket(AF_INET, SOCK_STREAM, 0); if (sock 0) { return -1; } struct timeval tv; tv.tv_sec timeout_ms / 1000; tv.tv_usec (timeout_ms % 1000) * 1000; /* 发送超时影响 connect 的行为 */ setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, tv, sizeof(tv)); struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(port); server_addr.sin_addr.s_addr ip-addr; int ret connect(sock, (struct sockaddr *)server_addr, sizeof(server_addr)); if (ret 0) { closesocket(sock); return -1; } return sock; }如果你想实现非阻塞 connect配合 select 判断连接是否建立可以这样static int connect_nonblocking(const ip_addr_t *ip, u16_t port, uint32_t timeout_ms) { int sock socket(AF_INET, SOCK_STREAM, 0); if (sock 0) { return -1; } /* 设置为非阻塞 */ int flags fcntl(sock, F_GETFL, 0); fcntl(sock, F_SETFL, flags | O_NONBLOCK); struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(port); server_addr.sin_addr.s_addr ip-addr; int ret connect(sock, (struct sockaddr *)server_addr, sizeof(server_addr)); if (ret 0 errno ! EINPROGRESS) { closesocket(sock); return -1; } /* connect 正在后台进行等待可写事件 */ fd_set write_fds; FD_ZERO(write_fds); FD_SET(sock, write_fds); struct timeval tv; tv.tv_sec timeout_ms / 1000; tv.tv_usec (timeout_ms % 1000) * 1000; int sel_ret lwip_select(sock 1, NULL, write_fds, NULL, tv); if (sel_ret 0) { /* 超时或异常 */ closesocket(sock); return -1; } /* 检查连接错误 */ int so_error 0; socklen_t err_len sizeof(so_error); getsockopt(sock, SOL_SOCKET, SO_ERROR, so_error, err_len); if (so_error ! 0) { closesocket(sock); return -1; } /* 连接建立成功 */ return sock; }这个方案的优点是connect()在非阻塞模式下立即返回业务任务可以在等待 select 的期间跑其他逻辑select 自带超时无论底层怎样都不会无限期卡死SO_ERROR能拿到具体的失败原因对排查问题大有帮助。我实测下来从 netconn API 切换到 socket API 之后连接流程的稳定性明显提升。核心原因是把超时控制从需要理解 LwIP 内部机制的层面降到了大家熟悉的 socket 编程模型里减少了很多误解空间。4.4 调整协议栈超时参数作为兜底如果你暂时无法改代码至少可以把协议栈层面的重传参数调小让netconn_connect()即便卡住也不会卡太久。最直接的是修改lwipopts.h里的TCP_SYNMAXRTX把默认的 6 改小。改成 2 或 3 后SYN 重传次数减少connect 的最长等待时间明显缩短。#define TCP_SYNMAXRTX 3这个改动的好处是全代码生效不需要业务逻辑配合。代价是弱网环境下如果第一个 SYN 真的丢了重传次数不足会导致连接失败的几率上升。所以这个参数是拦截时间和成功率之间的权衡不要拍脑袋改到 1建议从 3 起步实测。同时可以检查ARP_QUEUEING、ARP_TMR_INTERVAL等 ARP 相关参数。局域网内连接不可达 IP 时ARP 等待时间也会叠加进 connect 的总耗时。但改动 ARP 参数的影响面较大我不建议随便调整除非你明确知道自己在做什么。你还可以考虑在 lwipopts.h 中开启LWIP_SO_RCVTIMEO和LWIP_SO_SNDTIMEO让 socket API 层面的超时选项可用。前面例子里的SO_SNDTIMEO就需要这个宏配合。有些移植版的 lwipopts.h 默认没有开这个宏设置 socket 选项会返回失败但这个失败通常被忽略导致超时设置没生效卡死问题依旧存在。5. 实测验证与避坑记录5.1 测试环境与复现方法为了验证不同方案的效果我搭了一个可控的测试环境。设备端用一块带以太网的 MCU 开发板运行 FreeRTOS LwIP 2.1.2协议栈跑在 10Mbps 半双工模式和路由器之间加了一个可以手动断开的网线开关。复现卡死的方法很简单设备上电连接服务器确认 TCP 连接建立成功。拔掉网线或者关掉路由器电源。设备端检测到 TCP 断开通过 keepalive 或者数据收发超时。触发重连流程调用netconn_connect()连接一个不可达的 IP。观察程序是否卡在 connect。在默认配置下几乎稳定复现卡死调用netconn_connect()后任务长时间停留在sys_arch_sem_wait日志不再打印。为了测出最坏情况耗时我用了两个测试目标一个是局域网内不存在的 IP另一个是公网不可达 IP。后者更能反映真实场景因为公网路由不可达时SYN 重传会完整走完所有次数。5.2 各方案的表现对比我在同一个设备上依次测试了几种方案记录 connect 的最长等待时间和代码侵入程度。方案修改量connect 最长等待时间卡死是否消失备注默认配置无超时无60~120 秒否表现为假死TCP_SYNMAXRTX3改一个宏15~30 秒否时间缩短但仍是阻塞netconn_set_recvtimeout(3000)业务代码 2 行3 秒是超时后返回 ERR_TIMEOUTsocket API select改写连接模块可控测试用 5 秒是最稳定推荐用netconn_set_recvtimeout()的时候遇到一个有意思的现象超时返回之后如果马上用同一个 conn 再次 connect偶尔会成功偶尔会返回ERR_VAL。后来查了源码发现超时后 netconn 内部状态没有完全复位继续复用同一连接有风险。所以在超时分支里我直接调netconn_delete()重建连接重试逻辑改成删除旧连接、创建新连接、设置超时、再 connect严格按这个顺序来做之后重连成功率稳定在 100%。socket API select 的方案我把超时设成 5 秒测试环境里对不可达 IP 的 connect 都能在 5 秒内返回没有出现一次卡死。这里有个需要注意的细节select 返回 0 不代表连接一定成功还要用getsockopt(SO_ERROR)确认。如果忽略这一步select 因异常事件返回也可能被误判为连接成功。5.3 实测中踩过的二线坑重连、复用、内存泄漏方案切换过程中踩了几个和主题强相关的坑这里记录下来希望能帮你少走弯路。第一个坑是重连时资源泄漏。早期测试时每次连接失败我都直接netconn_delete()或者closesocket()但创建新的连接对象之前忘了检查旧对象是否已释放。在压力测试跑了几百次重连之后内存池统计显示MEMP_NUM_NETCONN和MEMP_NUM_TCP_PCB的使用量缓慢上涨最终耗尽导致无法创建新连接。这不是卡死问题但后果比卡死更严重——系统可能完全无法联网。排查方式是定期打印内存池使用情况我会在每次重连循环里打印mem_pool_stats对比前后差值一旦发现只增不减立刻检查代码。第二个坑是 ARP 表项的影响。局域网内连接一个不存在的 IP第一次 connect 会等待 ARP 超时第二次重试因为 ARP 表里已有失败记录反而可能更快。但如果你频繁换不同 IP 测试ARP 表会积累大量垃圾表项某些流量会被错误地发往错误的 MAC。必要时可以调用etharp_cleanup()清理 ARP 表但这个操作要慎用高频调用会影响正常通信。第三个坑是SO_SNDTIMEO这个选项的适用范围。我最初以为设了它就能完全控制 connect 超时但实测发现它只是保证发送数据包不会无限阻塞对 TCP 连接建立阶段的 SYN 重传并不完全有效必须配合TCP_SYNMAXRTX或者其他机制才能达到预期。这也是我们最终选择 select SO_ERROR组合的原因这个组合对 connect 流程的控制粒度最清晰。第四个坑是 DNS 解析和 connect 的顺序。如果用域名连接服务器netconn_gethostbyname()或getaddrinfo()是另一个可能卡住的环节而且它默认超时时间和 connect 还不一样。曾遇到过 DNS 解析卡了十几秒才失败随后才进入 connect 流程的案例。排查时务必把 DNS 解析超时也纳入整个连接流程的总超时控制里别只管 connect 不管 DNS。6. 这类问题更通用的排查思路从卡在XX函数到系统性止损做过的网络调试多了之后我发现卡死在 netconn_connect()只是嵌入式网络阻塞问题的冰山一角。类似的场景还包括netconn_write()卡死、netconn_recv()卡死、dhcp_start()卡死、dns_gethostbyname()卡死。虽然函数不同但底层原因其实很相似。共通的经验可以归纳成一句话先确认阻塞有没有超时保护再检查消息通道有没有回应最后排查资源是否耗尽。顺序不要反因为超时保护是第一个也是最容易补上的防线。具体到排查流程我习惯按下面这个顺序走查看调用栈确认停在sys_arch_sem_wait还是sys_mbox_trypost。确认 tcpip_thread 是否还活着打印它的循环心跳。检查recv_timeout是否显式设置过没有就补上。检查协议栈关键宏是否配置正确重点看LWIP_NETCONN、LWIP_TCP、LWIP_SOCKET。打开内存统计确认内存池没有持续增长。排除在回调、中断、锁内调用阻塞 API 的非法场景。根据项目约束选择方案小改动用 recv_timeout彻底解决用 socket select。最后分享一个我个人的经验偏好。在需要长期稳定运行的设备上我倾向于直接用 socket API select 重构连接管理模块因为它的行为更可预期交付之后出问题也好排查。而在一些临时工具、验证程序、或者代码结构已经定型的老项目上用netconn_set_recvtimeout()救急就够了没必要大动干戈。我自己那款工业数据采集器后来就是用的 socket select 方案重连逻辑完全交给了两个状态CONNECTING和CONNECTED。断网后任务会定期发起一次非阻塞 connectselect 超时失败就进入退避等待整个过程不会阻塞任何系统任务设备运维方从此再也没有提过掉线后连不回来这个问题。嵌入式网络这块靠按经验改参数往往只能把问题往后推真正的解法是理解阻塞模型把超时和状态管理掌握在自己手里。
返回列表