一个让人后背发凉的现场
前两年做一款工业网关的固件升级功能,设备通过无线链路和后台服务器通信,升级包大概2MB左右,分片传输。测试环境跑了几十次都没问题,结果现场部署第一天就出了状况。
现场反馈:一台设备升级到大概60%的时候,无线信号突然断了十几秒。信号恢复后,设备倒是没变砖,但后台再也连不上它了。重启设备能恢复,但现场设备装在高处,重启一次要派人爬梯子,客户非常不满。
拿到日志后我发现一个关键现象:设备重启前,协议栈的心跳包还在往后台发,但后台收到的心跳包格式全是乱码。后台的安全模块直接把设备拉黑了。也就是说,设备看起来"活着",但协议栈已经处于一种半死不活的状态。
这个坑让我意识到一件事:固件升级中断后的恢复,大多数人只关注"固件本身有没有写坏",却忽略了一个更隐蔽的问题——协议栈的状态机在升级过程中被拆得七零八落,中断后如果没有正确的恢复策略,设备会进入一种"通信假死"状态。
为什么协议栈会被升级过程搞乱
要理解这个问题,得先看清楚升级流程和协议栈之间的耦合关系。
典型的无线固件升级流程是这样的:
后台 -> 设备:升级开始通知 设备 -> 后台:确认,进入升级模式 后台 -> 设备:数据分片1 设备 -> 后台:ACK 1 后台 -> 设备:数据分片2 设备 -> 后台:ACK 2 ... 后台 -> 设备:数据传输完毕 设备 -> 后台:校验确认 设备 -> 后台:重启进入新固件问题出在"进入升级模式"这一步。为了腾出内存和Flash空间,很多实现会做这几件事:
- 暂停或降低协议栈的任务优先级
- 释放部分协议栈的缓冲区
- 关闭或简化心跳机制
- 把协议栈的部分状态标记为"升级中"
这些操作在升级顺利完成时没问题,因为重启后一切重新初始化。但如果升级中途断了,设备没有重启,协议栈就停在那个被"半拆解"的状态里。
更麻烦的是,不同协议栈层面对"中断"的反应不一样。物理层可能只是丢了几帧,链路层可能在重传,网络层可能在等路由更新,应用层的心跳任务可能已经被挂起了。这些层各自为政,恢复时间不同步,就出现了我在现场看到的那种情况:心跳包还在发,但内容已经不对了。
中断后的几种典型异常表现
根据我后来在多个项目中踩过的坑,升级中断后协议栈的异常大概分这几类:
第一类:心跳包内容错乱
现象是后台收到心跳包,但解析失败。原因通常是心跳任务在升级时被暂停,恢复后它继续用旧的缓冲区发送数据,而那块缓冲区已经被升级数据覆盖了。
现场排查方法:抓包看心跳包的原始字节,如果长度对但内容全是0或者随机值,基本就是缓冲区被踩了。
第二类:ACK序号错位
现象是后台反复重传同一个分片,设备反复回复错误的ACK号。原因是升级中断时,设备侧的接收窗口状态没有正确回滚,恢复后它以为已经收到了某些分片,实际上没有。
现场排查方法:看后台的重传日志,如果同一个分片重传超过3次,且设备回复的ACK号跳跃,就是这个问题。
第三类:连接假活
现象是后台显示设备在线,但下发任何命令都超时。原因是链路层的保活机制还在跑,但应用层的命令处理任务已经挂了。
现场排查方法:从后台ping设备的应用层端口,如果链路层能通但应用层不通,就是这个问题。
第四类:反复重连风暴
现象是设备每隔几秒就重连一次,后台日志被刷爆。原因是升级中断后,协议栈的重连逻辑被触发,但每次重连到一半又因为状态不一致而失败,形成死循环。
现场排查方法:看后台的连接日志,如果同一台设备在短时间内反复上下线,就是这个问题。
恢复策略的核心思路
处理这类问题,核心思路只有一句话:升级中断后,不要试图让协议栈"续上",而是让它"重启"。
听起来简单,但做起来有几个关键决策点。
决策点一:什么时候触发协议栈重启
最直接的做法是:升级中断后立即重启整个设备。但这在现场往往不可接受,因为设备可能装在难以触及的位置,而且频繁重启会影响其他功能。
折中方案是:升级中断后,只重启协议栈,不重启整个系统。具体做法是给协议栈设计一个"软复位"接口,调用后协议栈回到初始状态,但应用层的其他任务不受影响。
这个方案的关键在于:软复位必须彻底。我见过一些实现,软复位只清了应用层的状态,链路层和网络层的状态没动,结果恢复后问题依旧。
决策点二:如何判断升级真的中断了
这里有个隐蔽的陷阱:升级中断和升级缓慢,在短时间内看起来是一样的。
我的做法是设计一个"升级看门狗":每次收到升级数据分片时,刷新一个计数器。如果超过预定时间没有收到下一个分片,就判定为中断。
但这里有个权衡:超时时间设得太短,网络抖动时会误判;设得太长,设备会长时间停在半拆解状态。
经过多次调试,我一般把超时时间设为正常分片间隔的3到5倍。比如正常分片间隔是200ms,超时时间就设600ms到1s。
决策点三:恢复后如何通知后台
协议栈软复位后,设备对后台来说就是一个"新"设备。但后台可能还保留着之前的会话状态,如果不清理,会出现状态不一致。
我的做法是:软复位后,设备主动发送一个"会话重置"消息,后台收到后清理旧会话,重新建立连接。
这个消息的格式要简单,不依赖任何之前的状态。我一般用一个固定的短帧,里面只包含设备ID和一个重置标志。
具体实现中的几个坑
坑一:缓冲区释放的顺序
协议栈软复位时,缓冲区的释放顺序很重要。如果先释放了底层缓冲区,上层还在往里面写数据,就会踩内存。
正确的顺序是:先停止所有上层任务,再释放上层缓冲区,然后停止链路层任务,释放链路层缓冲区,最后停止物理层。
这个顺序听起来理所当然,但在实际代码中,任务停止和缓冲区释放往往是异步的,需要加同步机制。
坑二:中断上下文中的恢复
如果升级中断是在中断上下文中检测到的,这时候不能直接调用协议栈软复位,因为软复位可能涉及内存分配和任务调度,这些在中断上下文中是不安全的。
我的做法是:在中断中只设置一个标志,然后由一个低优先级的任务来执行实际的恢复操作。
坑三:恢复后的首次通信
协议栈软复位后,第一次通信往往是最脆弱的。因为这时候各种状态都是初始值,如果后台还按照旧状态来解析,就会出错。
我一般会在恢复后的第一次通信中,强制使用最保守的参数:最小的分片长度、最长的超时时间、最简单的帧格式。等确认通信正常后,再逐步恢复到正常参数。
一个可用的恢复流程
综合以上,我总结出一个在实际项目中验证过的恢复流程:
// 升级看门狗任务voidupgrade_watchdog_task(void*param){while(1){if(upgrade_in_progress){if(get_国外半导体厂商me_since_la半导体厂商_fragment()>UPGRADE_国外半导体厂商MEOUT_MS){// 判定为升级中断set_recovery_flag();}}delay_ms(100);}}// 恢复任务voidrecovery_task(void*param){while(1){if(get_recovery_flag()){// 1. 停止所有协议栈相关任务半导体厂商op_all_protocol_tasks();// 2. 按顺序释放缓冲区release_app_buffers();release_link_buffers();release_phy_buffers();// 3. 重置协议栈状态reset_protocol_半导体厂商ate();// 4. 重新初始化协议栈init_protocol_半导体厂商ack();// 5. 发送会话重置消息send_session_reset();// 6. 清除标志clear_recovery_flag();}delay_ms(50);}}这个流程的关键在于:它不试图保留任何升级前的状态,而是彻底重置。虽然这样会丢失一些上下文,但能保证协议栈回到一个确定的状态。
现场调试时的观察要点
如果你在现场遇到类似问题,这几个观察点能帮你快速定位:
第一,看后台日志中设备的最后一条正常消息和第一条异常消息之间的时间差。如果这个时间差和升级中断的时间吻合,基本可以确定是升级中断导致的。
第二,抓包看设备恢复后发出的第一条消息。如果这条消息的格式和正常消息明显不同,说明协议栈状态没有正确恢复。
第三,如果设备支持,通过调试串口看协议栈的状态变量。重点关注:连接状态、重传计数、缓冲区使用量。如果这些值和正常运行时差异很大,就是状态没有恢复。
第四,做对比测试:在实验室模拟升级中断,看设备是否出现同样的异常。如果实验室能复现,就可以在实验室调试恢复逻辑,不用去现场折腾。
最后一点经验
这类问题的根源,往往是升级流程和协议栈的耦合太紧。如果条件允许,最好的做法是在架构设计阶段就把两者解耦:升级过程不直接操作协议栈,而是通过一个中间层来协调。
但现实项目中,很多设备是在已有协议栈上后加升级功能,解耦的成本很高。这时候,一个可靠的恢复流程就是最后的防线。
记住一点:协议栈的状态恢复,宁可粗暴一点,也不要试图"智能"地续上。因为协议栈的状态太复杂,试图精确恢复往往比彻底重置更容易出问题。