一、背景该模块为嵌入式 Linux 设备提供固件升级能力支持两种模式模式说明单模块升级通过 SPI / 串口将单个固件烧写到指定硬件模块一键批量升级解压tar.gz压缩包按固定顺序烧写全部 8 个硬件模块升级过程中需实时向客户端上报百分比进度。二、进度条来回闪烁2.1 现象批量升级时客户端进度条在0% 和正常进度之间反复跳动。2.2 排查加日志后发现同一秒内服务端发出的进度值确实在两条差距悬殊的数据之间来回切换。顺藤摸瓜定位到两个线程在同时往同一个消息 ID 发送进度线程周期职责进度算法A主循环1s单模块时上报子模块烧写进度模块索引 × 12.5 内部进度 × 100/8B独立线程1s批量时上报整体进度模块索引 × 12.5 时序增量批量模式下线程 B 在推送整体进度如25.0 → 25.1 → 25.2但线程 A 仍在运行。当某个子模块烧写产生进度信号时线程 A 算出一个小范围值如5.25同样写入该消息 ID。客户端收到的数据混合交错进度条从此精神分裂。2.3 根因两个生产者向同一消息通道写入数据缺乏互斥机制和上下文感知。线程 A 不知道自己在批量模式下依然忠实地发送单模块进度。2.4 修复在线程 A 的发送条件中增加模式判断// 改前 if ((progress 0.0) (count 0) (count 8)) { send_progress(); } ​ // 改后 if (!batch_upgrading (progress 0.0) (count 0) (count 8)) { send_progress(); }batch_upgrading在批量升级入口置true出口恢复false。全路径验证路径batch_upgrading行为结论单模块升级false守卫恒通过行为不变✅批量升级中true守卫阻塞线程 B 独占通道✅批量刚结束刚恢复false各模块出口已重置变量为-1条件不满足✅空闲态false变量本身就是-1永不触发✅2.5 深层反思升级逻辑与进度上报应当解耦这个问题本质上是架构欠债——把升级执行和进度上报塞进了同一个线程。线程 A 最早可能只是个通用监控线程后来反复往里加进度上报代码最终变成既管设备状态、又管升级通知。当批量模式引入线程 B 后两台发动机在同一条轨道上对撞。更合理的分层升级模块只负责烧写 │ └─→ 进度事件 ─→ 进度上报模块统一对外输出不同升级模式只影响升级模块内部行为对外进度接口保持一致。这样永远只有一个出口。三、解压后文件权限丢失3.1 现象批量升级中某步需cp复制固件文件复制后权限不正确后续操作失败。3.2 根因tar -xzf解压保留压缩包内的原始权限位。制作包的机器上若是0744嵌入式目标机上解压后也是0744。而cp默认继承源文件权限。3.3 修复解压后、使用前统一赋权snprintf(cmd, sizeof(cmd), chmod 0777 %s*, dest_path); system(cmd);一行代码四两拨千斤。四、旧固件残留匹配4.1 场景压缩包解压到目标目录后代码扫描目录下所有文件按文件名中的关键词子串匹配区分各模块固件for each file in dir: name to_lower(file.name) for each keyword in keyword_map: if strstr(name, keyword): assign file to module[keyword] break危险场景若目录中残留了上一轮解压的旧文件strstr照样匹配成功。旧文件可能代替新文件被烧写入硬件。旧文件为何残留原因说明操作顺序时序清空目录发生在上次接收包阶段若上次包中途取消旧文件已解压但未被下次清空覆盖通配符盲区rm -rf /path/*不会删除.开头的隐藏文件4.2 核心操作顺序就是正确性清空 → 解压 → 赋权 → 扫描 → 烧写一步都不能乱错误顺序后果原因先解压 → 后清空白解压rm -rf删掉刚解压出来的文件先赋权 → 后解压无效tar覆盖文件权限变回包内原始值先扫描 → 后赋权烧写失败扫描到的文件权限错误cp带着错误权限走全流程扫描那一刻目录里必须是新文件 正确权限的最终态。4.3 更稳妥的做法临时目录 原子替换# 1. 解压到临时目录 tar -xzf pkg.tar.gz -C /tmp/upgrade_tmp/ ​ # 2. 赋权 chmod 0777 /tmp/upgrade_tmp/* ​ # 3. 扫描验证 scan_fw_in_dir(/tmp/upgrade_tmp/) ​ # 4. 验证通过后原子替换正式目录 rm -rf /upgrade/* mv /tmp/upgrade_tmp/* /upgrade/ ​ # 5. 烧写中途失败 → 正式目录毫发无损 → 下次重来零污染。五、硬件缺失的容错5.1 现状批量升级循环的当前处理方式for (int i 0; i total; i) { ModuleType mod order[i]; ​ if (fw_files[mod][0] \0) { continue; // 压缩包里没这个固件 → 跳过 } ​ result module_upgrade(fw_files[mod]); if (result ! OK) { print(Module %d failed, mod); // 打一行日志继续跑下一个。没有 break。 } } return OK; // 无论几个模块失败最终都返回成功两层跳过含义天差地别条件含义性质fw_files[mod]为空包里就没有这个固件正常跳过 ✅module_upgrade失败硬件通信异常异常 ⚠️5.2 三个隐患① 失败类型不可区分固件校验错、通信超时、硬件物理缺失 —— 全返回同一个错误码。上层不知道该重试、该跳过、还是该报停。② 盲目继续浪费时间若总线物理断开每个模块都会等满超时如 30s × 8 4 分钟。第一个就该停了。③ 最终仍返回成功return OK一句话把所有的失败都吞了上游信以为真。5.3 建议的分层容错方案第一步烧写前做轻量探活for each module: if !has_firmware(module) → mark(no_firmware) elif probe_hardware(module) → mark(to_upgrade) else → mark(hardware_missing) → report(模块 X 硬件不存在已跳过)一次探活指令 100ms比盲目等超时 30s 划算得多。第二步根据错误类型分级决策switch (result) { case ERR_HW_ABSENT: // 硬件不存在 → 跳过继续 case ERR_FW_CHECKSUM: // 校验失败 → 跳过但提示 case ERR_TIMEOUT: // 超时 → 重试 1 次再失败则跳过 case ERR_BUS_DEAD: // 总线挂了 → 立即终止后续无意义 }第三步如实上报最终结果情况返回值全部成功OK部分跳过硬件缺失OK_WITH_SKIP部分失败PARTIAL_FAIL全部失败FAILED六、总结多生产者共享通道必须有协调机制。两个线程写同一个消息 ID哪怕只加一个!batch_upgrading标志位也能解燃眉之急。根本解决则要解耦升级和通知。跨环境文件操作不假设权限。解压后显式chmod比事后排查省十倍时间。目录操作顺序就是正确性。清空 → 解压 → 赋权 → 扫描一步乱步步乱。临时目录 原子替换是更彻底的办法。硬件操作要有容错层级。不是所有失败都能用continue解决。探测 → 重试 → 跳过 → 终止根据错误类型分级对上游诚实。2026 年 7 月 · 个人学习总结