跑 eCos + lwIP 的设备作为 TCP 服务器,在网络不繁忙不丢包的情况下,一切正常,在网络繁忙会出现丢包的情况下,重试几次后 TCP 拒绝服务(对 SYN 包都不会有任何响应, ping 功能可能正常也可能无响应),其它任务正常。能够承受的重试次数和选项 lwIP networking stack >> Protocols >> TCP support >> Sender pbufs (CYGNUM_LWIP_TCP_SND_QUEUELEN)
有关(据我推测,没有实测)。
看出来了吗?
客户端没有收到服务器的响应,服务器也没有重发!这不是 TCP 啊!TCP 是会重发的,对方没有收到就会重发,但是这里就是没有重发。
原因是定时器资源不够,TCP 没有申请到定时器,没法处理任何和超时有关的事物。先给出解决办法,问题分析在后面。有两个办法可以解决这个问题,二选一,第二方法更彻底。
将上图所示的 Simultaneous active timeouts by user modules
选项值加1。这样 lwIP 就有足够的定时器可用了,TCP 所需要的定时器也能正确分配到。
这个办法更加地彻底,从根本上解决了缺少定时器的问题。
关于超时定时器的配置部分(lwip_net.cdl:1342):
cdl_option CYGNUM_LWIP_MEMP_NUM_CORE_SYS_TIMEOUT {
display "Simultaneous active timeouts by core modules"
flavor data
calculated { CYGPKG_LWIP_TCP + CYGFUN_LWIP_IP_REASSEMBLY +
CYGPKG_LWIP_ARP + (CYGPKG_LWIP_DHCP * 2) +
CYGPKG_LWIP_AUTOIP + CYGPKG_LWIP_IGMP +
CYGPKG_LWIP_DNS + CYGPKG_LWIP_PPP }
description "
The number of simulateously active timeouts used by the lwIP
core modules."
}
从这个选项的脚本可以看出,lwIP 所需要的定时器数量是根据所选择的功能自动算出来的,但是少了 lwip_select
所需要的那个超时定时器。
将该脚本改成如下:
cdl_option CYGNUM_LWIP_MEMP_NUM_CORE_SYS_TIMEOUT {
display "Simultaneous active timeouts by core modules"
flavor data
calculated { CYGPKG_LWIP_TCP + CYGFUN_LWIP_IP_REASSEMBLY +
CYGPKG_LWIP_ARP + (CYGPKG_LWIP_DHCP * 2) +
CYGPKG_LWIP_AUTOIP + CYGPKG_LWIP_IGMP +
CYGPKG_LWIP_DNS + CYGPKG_LWIP_PPP +
CYGPKG_LWIP_SOCKET_API }
description "
The number of simulateously active timeouts used by the lwIP
core modules."
}
即在选项的计算式中加入 + CYGPKG_LWIP_SOCKET_API
。lwip_select
是 CYGPKG_LWIP_SOCKET_API
组件提供的一个函数。
lwIP 有个很好的特性,那就是 Traffic statistics
,打开这个特性,lwIP 可以统计数据包的收发以及内存的使用情况。在资源足够的情况下,强烈建议打开该选项。在 eCos 中,这个选项是 lwIP networking stack >> Traffic statistics (CYGPKG_LWIP_STATS)
。
调试是查找问题的好帮手,一定要留调试接口,一定要掌握调试这么手艺。
将设备连接调试器,连续运行直到故障重现,暂停程序执行,这个时候就可以检查 statistics 了,查看 lwip_tcpip >> current >> src >> core >> stats.c 文件中的 lwip_stats
结构。
检查的结果就是 SYS_TIMEOUT 类型的 LWIP_MEMPOOL 发生了错误,只分配了 6 个,但是实际最多需要 7 个。
给 lwIP 多增加几个定时器资源,再执行就不会出现故障了。
因此可以肯定 TCP 处理超时重发的定时器分配失败了,也就没有 TCP 的超时处理了,也就不会重发了。lwIP 所需要的定时器,大部分都在 tcpip_thread
的开始处申请完了,TCP 的定时器是在有需要的时候调用 tcp_timer_needed
函数申请的。在 eCos 中,lwIP 所需要的定时器个数是自动计算的:
CYGNUM_LWIP_MEMP_NUM_CORE_SYS_TIMEOUT =
CYGPKG_LWIP_TCP + CYGFUN_LWIP_IP_REASSEMBLY +
CYGPKG_LWIP_ARP + (CYGPKG_LWIP_DHCP * 2) +
CYGPKG_LWIP_AUTOIP + CYGPKG_LWIP_IGMP +
CYGPKG_LWIP_DNS + CYGPKG_LWIP_PPP
经检查发现,除了上面引用到的特性会使用定时器外,socket 的 lwip_select
函数也会使用到定时器,因此这里算式少算了一个定时器,问题就出在这。
lwip_select
函数通过调用 sys_sem_wait_timeout
函数间接地使用到了定时器。
我们的程序使用了 lwip_select
函数,而且还带超时,所以触发了这个bug。
缺少 TCP 定时器引起TCP拒绝服务的原因:
还有更快捷地发现问题的途径:启用断言!
lwIP 的断言设计也是很完善的,完全可以使用断言来捕获资源不够的问题,查看定时器申请的代码(sys_timeout
函数):
timeout = memp_malloc(MEMP_SYS_TIMEOUT);
if (timeout == NULL) {
LWIP_ASSERT("sys_timeout: timeout != NULL", timeout != NULL);
return;
}
如果启用了断言,当资源分配失败,这里的 LWIP_ASSERT
就被触发了!
但是断言有一点不好,非调试状态下它会引起系统死机或复位,而不仅仅是网络功能失效,对产品的口碑而言,网络功能失效比死机或复位要好一些。
要打开 eCos 中 lwIP 的断言,那么就要打开整个 eCos 的断言。
嗯 …… 下次找问题,先打开断言!