4. 双分区(A/B)升级策略:A/B分区原理、启动切换逻辑、升级失败自动回滚
双分区升级,圈里人常叫它 A/B 系统更新。说白了,就是给固件准备两个“窝”。
我刚开始接触这个方案时,觉得有点浪费——多占一倍的存储空间啊!但后来在量产项目上吃过亏,才明白这钱花得值。你想想看,设备正在升级,突然断电了,或者网络断了,传统单分区方案很可能变砖。A/B 方案就是来解决这个问题的。
4.1 A/B 分区原理
先看一张图,我画了个简单的结构示意:
每个 Slot 都是完整的系统镜像,包含 bootloader、kernel、rootfs 和应用程序。两个 Slot 完全独立,互不干扰。
元数据区是关键中的关键。它记录着当前哪个 Slot 是 active 的,启动尝试了几次,上次启动是否成功。Bootloader 启动时第一件事就是读这个区,然后决定往哪边跳。
核心思想:任何时候,至少有一个 Slot 是可启动的。升级只写 inactive 的那个 Slot,不影响当前运行的系统。
4.2 启动切换逻辑
启动流程其实不复杂,我拆成几步讲:
- 上电 → Bootloader 启动:读取元数据,找到 active_slot 标记。
- 检查尝试次数:如果 retry_count 还没用完,就尝试启动 active_slot。
- 启动系统:内核加载,文件系统挂载,应用启动。
- 应用层上报成功:系统正常运行后,应用层调用接口,把 boot_successful 标记写入元数据。
- 下次启动:Bootloader 看到 boot_successful 为 true,正常启动。
这里有个细节——尝试次数。我习惯设 3 次。为什么?因为有些设备启动慢,或者外设初始化需要时间。3 次是个比较稳妥的值。
我的经验:尝试次数别设太大,也别太小。1 次太激进,万一启动过程中有偶发问题就回滚了。5 次以上又拖长了故障发现时间。3 次刚刚好。
切换逻辑的伪代码大概长这样:
// Bootloader 伪代码
slot = read_metadata("active_slot");
retry = read_metadata("retry_count");
if (retry > 0) {
// 还有尝试机会
retry--;
write_metadata("retry_count", retry);
boot_slot(slot);
} else {
// 尝试次数用完了,切换 Slot
slot = (slot == SLOT_A) ? SLOT_B : SLOT_A;
write_metadata("active_slot", slot);
write_metadata("retry_count", MAX_RETRY);
boot_slot(slot);
}
// 应用层上报成功
void mark_boot_successful() {
write_metadata("boot_successful", 1);
write_metadata("retry_count", 0); // 清零,下次不再重试
}
嗯,这里要注意:应用层上报成功这个动作必须在系统完全就绪后执行。我见过有人把上报放在 init 脚本里,结果网络还没起来就标记成功了,后来网络模块挂了,设备反复重启——因为已经标记成功了,Bootloader 不再尝试另一个 Slot。
4.3 升级失败自动回滚
回滚机制是 A/B 方案最吸引人的地方。它不需要用户干预,系统自己就能恢复。
升级流程是这样的:
- 设备在 Slot A 正常运行。
- 下载升级包,写入 Slot B。
- 写入完成后,设置 active_slot = Slot B,retry_count = 3。
- 重启设备。
- Bootloader 尝试启动 Slot B。
如果 Slot B 启动失败(比如内核崩溃、文件系统损坏),会发生什么?
Bootloader 发现 retry_count 减到 0 了,boot_successful 还是 0。于是它自动把 active_slot 切回 Slot A,然后启动 Slot A。整个过程用户可能都没感觉到——除了重启时间稍微长了一点。
警告:千万不要在升级过程中断电或复位!虽然 A/B 方案能容忍升级中断,但元数据可能处于不一致状态。我遇到过最头疼的情况:元数据写了一半断电,两个 Slot 都标记为 inactive,Bootloader 直接傻眼。
我曾经在一个智能网关项目上吃过这个亏。当时测试同事反复拔电,终于把元数据区搞坏了。后来我加了个方案:元数据区写两份,互为备份。写的时候先写备份区,再写主区。读取时校验主区,如果校验失败就用备份区。从那以后,再没出过类似问题。
4.4 实际项目中的注意事项
讲几个我踩过的坑:
- 存储空间规划:两个 Slot 意味着存储需求翻倍。如果 Flash 只有 16MB,每个 Slot 只能分 8MB。你得算清楚:内核多大?文件系统多大?应用多大?留够余量。
- 升级包校验:写入 Slot B 之前,一定要校验升级包的完整性。我习惯用 SHA256 校验,确保包没被篡改或损坏。
- 回滚后的处理:回滚到 Slot A 后,Slot B 里的数据怎么处理?我建议保留,不要立即擦除。这样用户下次升级时可以断点续传,或者分析升级失败的原因。
- 测试覆盖:一定要模拟各种异常场景——升级中断、写入错误、启动崩溃、元数据损坏。我每次做 A/B 方案,都会写一个自动化测试脚本,跑几百次随机异常注入。
| 场景 | 预期行为 | 我的建议 |
|---|---|---|
| 升级过程中断电 | 回滚到原 Slot | 元数据加备份,防止写坏 |
| 新系统启动崩溃 | 尝试 3 次后回滚 | 应用层尽早上报 boot_successful |
| 元数据损坏 | 使用备份元数据 | 双备份 + CRC 校验 |
| 两个 Slot 都不可用 | 进入恢复模式 | 预留最小恢复系统 |
避坑指南:我曾经在某个项目上,把 boot_successful 的上报放在了应用层初始化完成之前。结果应用层有个模块初始化失败,系统反复重启,但 Bootloader 认为已经成功了,不触发回滚。后来我改成:应用层所有关键模块初始化完毕,再上报成功。这个顺序很重要。
最后说一句:A/B 方案不是银弹。它增加了存储成本,也增加了系统复杂度。但如果你的设备不允许变砖,或者升级失败会造成严重后果,那这个方案值得投入。我个人在工业网关、医疗设备、汽车电子项目上都在用,效果很好。
嗯,双分区的内容就这些。核心就三点:两个 Slot 互相备份、Bootloader 根据元数据做切换、失败自动回滚。把这三点吃透了,A/B 方案就能用好。
公众号:蓝海资料掘金营,微信deep3321