3. A/B分区架构:双备份系统的原理、切换逻辑与状态管理

各位好,我是老周。今天聊A/B分区架构。说白了,就是给系统准备两套完整的固件副本——一个在A分区跑着,另一个在B分区闲着。万一升级炸了,系统能自动切到另一个分区,保证设备不变成砖。

我在车载项目里见过太多升级失败的惨案。有一次,OTA下载到一半网络断了,系统重启后直接卡在logo界面。从那以后,我对A/B分区就特别上心。你想想看,一个双备份机制,成本只多一份存储空间,却能省掉80%以上的售后返修,这笔账怎么算都划算。

3.1 分区布局与启动原理

A/B分区,也叫双副本系统。它的核心思路很简单:

  • A分区:当前运行的系统,包含bootloader、kernel、rootfs、app
  • B分区:备份系统,结构与A完全一致
  • 元数据区:记录当前哪个分区是active,哪个是inactive

我习惯把元数据区放在独立的小分区里,比如一个4KB的misc分区。为什么?因为如果元数据和系统放一起,系统坏了,元数据也读不出来,那就真成死局了。

核心原则:任何时候,至少有一个分区是已知可启动的。升级只操作inactive分区,绝不碰active分区。

启动流程是这样的:

  1. Bootloader读取元数据,找到active分区
  2. 从active分区加载kernel和rootfs
  3. 系统正常启动,进入工作状态
  4. 如果有升级任务,下载到inactive分区
  5. 升级完成后,交换active/inactive标记
  6. 重启,从新active分区启动

嗯,这里要注意:交换标记不等于立即生效。我一般会在新分区启动成功、应用层上报"我活着"之后,才把旧分区标记为可回滚。这个细节,后面会细说。

3.2 切换逻辑:三种触发场景

切换逻辑,说白了就是决定"什么时候切、怎么切"。我把它分成三种场景:

场景 触发条件 切换方式 风险等级
正常升级切换 升级完成,用户确认或超时 标记交换 + 重启 低
启动失败回滚 新分区启动超时或崩溃 bootloader自动切回旧分区 中
运行时故障切换 应用层检测到严重异常 主动触发回滚 + 重启 高

我曾经在一个网关项目里踩过坑:正常升级切换后,新系统能启动,但Wi-Fi驱动死活连不上。bootloader觉得"能启动就是好分区",不会自动回滚。结果用户反馈设备变"单机版"了。后来我加了一层应用层健康检查——启动后30秒内必须上报"所有服务正常",否则触发运行时回滚。

注意:运行时故障切换要谨慎。如果误判,会导致设备反复重启。我建议设置回滚次数上限,比如连续回滚3次后,锁定当前分区并上报服务器。

3.3 状态管理:分区状态机

状态管理是A/B分区的灵魂。没有状态机,你根本不知道当前系统处于什么阶段。我画了一个精简的状态机,大家感受一下:

A/B分区状态机 A/B分区状态机 A_active / B_inactive 升级中(写B分区) 升级完成,待切换 B_active / A_inactive 回滚到A分区 故障锁定(上报) 开始升级 写入完成 重启 启动失败 连续回滚超限 回滚完成 说明: 正常流程:A_active → 升级B → 切换 → B_active。异常时:启动失败自动回滚,连续失败锁定分区。

状态机里每个状态都对应一个元数据值。我常用的元数据字段包括:

  • active_slot:当前active分区编号(0或1)
  • boot_attempt:当前分区启动尝试次数
  • max_attempt:最大允许尝试次数(一般设3次)
  • update_status:升级状态(idle/downloading/installed/committed)
  • rollback_flag:是否处于回滚模式

我的习惯:每次启动成功,应用层主动调用一个commit接口,把boot_attempt清零。如果应用层没调用commit,bootloader认为启动失败,尝试次数+1。超过max_attempt就触发回滚。

3.4 元数据读写与防撕裂

元数据读写有个大坑——写入过程中掉电怎么办?数据写了一半,元数据区损坏,bootloader读出来是乱码,两个分区都启动不了。这就是经典的"撕裂问题"。

我常用的方案是双副本元数据:

// 元数据结构体
struct ab_metadata {
    uint32_t magic;          // 魔数,用于校验
    uint32_t crc32;          // 整个结构体的CRC
    uint32_t active_slot;    // 当前active分区
    uint32_t boot_attempt;   // 启动尝试次数
    uint32_t update_status;  // 升级状态
    uint32_t reserved[10];   // 保留字段
};

// 存储两个副本
struct ab_metadata copy1;  // 偏移0x000
struct ab_metadata copy2;  // 偏移0x100

写入流程:

  1. 先写copy2,写完后刷缓存
  2. 再写copy1,写完后刷缓存
  3. 读取时,先读copy1,校验CRC和magic
  4. 如果copy1损坏,读copy2
  5. 如果两个都损坏,使用默认值(A分区active)

我曾经在一个项目里只用了单副本元数据,结果产线测试时频繁掉电,导致5%的设备变砖。后来改成双副本,问题彻底解决。嗯,这个教训花了我两周的加班时间。

3.5 实际项目中的避坑指南

最后分享几个我踩过的坑,希望能帮大家少走弯路:

坑1:分区大小不一致

A分区和B分区必须完全一样大。我见过有人把A分区设成128MB,B分区设成64MB,结果新固件超过64MB就写不进去。记住:分区表要预留足够的冗余空间。

坑2:升级过程中断电

即使有A/B分区,升级过程中断电也可能导致inactive分区数据不完整。我建议在升级前先擦除inactive分区,然后逐块写入并校验。如果校验失败,标记升级失败,下次启动时重新下载。

坑3:回滚后忘记清理

回滚到旧分区后,inactive分区里还留着新固件。下次升级时,如果直接覆盖,可能因为空间不足失败。我习惯在回滚后,把inactive分区标记为"待清理",下次升级前先擦除。

好了,A/B分区架构的核心内容就这些。双备份系统不复杂,但细节决定成败。状态机、元数据防撕裂、回滚策略,这三个点做好了,你的设备升级成功率能到99.9%以上。剩下的0.1%,交给远程诊断来处理——那是后面章节的内容了。