3、双分区AB系统原理:A/B分区的切换逻辑、active/inactive标记、bootloader的角色

双分区AB系统,说白了就是给Rootfs准备了两套副本。一套叫A分区,一套叫B分区。系统启动时,要么跑A,要么跑B。为什么要这么折腾?我当年第一次接触这个设计时也觉得多此一举,直到有一次OTA升级到一半断电了,设备变砖,被客户骂得狗血淋头……嗯,从那以后,AB分区就成了我设计升级方案时的标配。

3.1 为什么需要两套分区?

你想想看,传统升级方案是直接在原分区上擦写。如果升级过程中突然断电、网络中断、或者镜像本身有问题,系统就再也起不来了。这就是所谓的“变砖”。

AB分区的核心思想很简单:永远保留一个已知能启动的系统。升级时,我们只动那个“不活跃”的分区。就算升级失败,系统还能从另一个分区正常启动。用户甚至感觉不到升级出过问题。

核心原则:任何时候,至少有一个分区是完整且可启动的。

3.2 A/B分区的切换逻辑

切换逻辑其实不复杂。我画了一张图,帮你理清整个流程:

AB分区切换逻辑流程图 系统上电 Bootloader 读取 misc 分区 获取 active/inactive 标记 A分区 active? 检查 slot_suffix 标记 是 启动A分区 slot_a 否 启动B分区 slot_b 启动成功? 系统健康检查 是 正常运行 标记成功 否 切换标记 尝试另一分区 回退到另一分区重新启动

这个流程里,最关键的就是那个菱形判断框。Bootloader根据misc分区里的标记,决定启动哪个分区。如果启动失败,Bootloader会自动切换标记,尝试另一个分区。这个机制我称之为“自救逻辑”。

3.3 active/inactive标记

标记存放在哪里?通常是一个叫misc分区的小区域。这个分区很小,几MB就够,专门存元数据。

标记的核心字段就几个:

字段 含义 典型值
slot_suffix 当前活跃分区后缀 _a 或 _b
boot_success 上次启动是否成功 0 或 1
retry_count 当前分区重试次数 0~7
update_status 升级是否完成 idle / updating / done

我习惯把misc分区设计成固定大小,比如2MB。结构体定义要小心对齐,因为Bootloader和内核都会读写它。

// misc分区结构体示例
struct ab_slot_metadata {
    uint8_t  slot_suffix[4];    // "_a\0" 或 "_b\0"
    uint8_t  boot_success;      // 1=成功, 0=失败
    uint8_t  retry_count;       // 重试计数
    uint8_t  update_status;     // 升级状态
    uint8_t  reserved[7];       // 保留字节
    uint32_t crc32;             // 校验和
} __attribute__((packed));

注意:CRC32校验不能省。我曾经遇到过misc分区被意外写坏的情况,如果没有校验,Bootloader会读到错误标记,直接启动到错误分区。那场面……嗯,你懂的。

3.4 Bootloader的角色

Bootloader是整个AB切换机制的“裁判员”。它不参与业务逻辑,只做三件事:

  1. 读标记:从misc分区读取active/inactive状态
  2. 选分区:根据标记决定启动A还是B
  3. 写结果:启动后更新标记(成功或失败)

说白了,Bootloader就是个“看门人”。它不关心系统里跑的是什么应用,只关心能不能把系统带起来。

我在一个项目里遇到过这样的坑:Bootloader读misc分区时,没有做超时处理。结果某次misc分区所在的eMMC块刚好坏了,Bootloader卡死在读操作上,设备彻底起不来。后来我加了个超时机制,读不到就默认启动A分区。

经验之谈:Bootloader代码要尽量精简,不要引入复杂逻辑。它越简单,越不容易出错。AB切换的核心逻辑,控制在100行C代码以内是比较理想的。

3.5 切换的完整流程

咱们把整个流程串起来,看看一次典型的AB切换是怎么发生的:

  • 正常状态:A分区active,B分区inactive。系统从A启动。
  • 触发升级:系统下载新镜像,写入B分区(inactive分区)。
  • 标记切换:写入完成后,将misc分区中的active标记改为B。
  • 重启:Bootloader读到B是active,启动B分区。
  • 验证:B分区启动后,运行健康检查。如果通过,标记boot_success=1。
  • 回退:如果B启动失败,Bootloader将active标记改回A,重新启动A分区。

这个流程里,最容易被忽视的是“验证”这一步。我见过不少团队,升级完直接标记成功,结果新系统有bug,跑几分钟就挂了。正确的做法是:启动后运行一段时间(比如5分钟),确认系统稳定,再标记成功。

关键点:标记成功的时间点,决定了系统的可靠性。太早标记,可能掩盖问题;太晚标记,用户等待时间过长。我个人建议:核心服务启动完毕 + 关键功能自检通过,就可以标记成功了。不需要等所有后台任务都跑起来。

3.6 一个常见的误区

有人会问:既然AB分区这么可靠,是不是可以随便升级了?

不是的。AB分区只能保证“能启动”,不能保证“功能正常”。举个例子:你升级了一个新版本,系统能启动,但WiFi驱动有问题,连不上网。这时候AB分区不会自动回退,因为Bootloader认为“启动成功了”。

所以,AB分区是启动可靠性的保障,不是功能可靠性的保障。功能层面的回退,需要上层应用自己实现。

嗯,这个区别很重要。我在做第一个AB方案时就没想清楚,结果被测试团队追着改了好几个版本。


AB分区的切换逻辑,说白了就是一套“双保险”机制。Bootloader做裁判,misc分区做记分牌,A和B两个分区轮流上场。只要这套机制设计得够严谨,系统升级就再也不用提心吊胆了。