4. A/B无缝升级机制

A/B 无缝升级的核心目标,是让设备在后台把新系统写入“非当前运行”的槽位,用户继续使用旧系统;重启时由引导链决定进入哪个槽位,从而把停机时间压缩到一次重启。要真正理解这套机制,需要把 slot 元数据、boot control HAL、snapuserd、COW 快照和回滚判定串成一条完整链路来看。

一、slot 与元数据:升级状态的唯一事实来源

设备通常有 slot_a 和 slot_b 两个槽位,每个槽位包含 boot、system、vendor 等分区。当前运行槽位称为 active slot,另一个是 inactive slot。升级时,OTA 客户端把新镜像写入 inactive slot,并更新元数据,标记该槽位为“待尝试启动”。

元数据一般保存在 misc 分区或专门的 bootloader_control 结构中,关键字段包括:

  • slot_suffix:当前活动槽位,如 _a 或 _b;
  • bootable:该槽位是否可启动;
  • successful:该槽位是否已被标记为启动成功;
  • tries_remaining:剩余尝试启动次数,常见默认值为 3;
  • verity_mode:是否启用 dm-verity 校验。

升级完成后,inactive slot 被置为 bootable=true、successful=false、tries_remaining=3。重启后 bootloader 读取这些字段,决定是否切换到新槽位。

二、boot control HAL:上层与引导链的桥梁

Android 框架不直接写分区,而是通过 boot control HAL 暴露的接口操作元数据。典型接口包括:

getCurrentSlot()        // 返回当前活动槽位
markBootSuccessful()    // 将当前槽位标记为成功
setActiveBootSlot(slot) // 设置下次启动槽位
setSlotAsUnbootable(slot) // 将某槽位标记为不可启动

升级流程中,OTA 客户端调用 setActiveBootSlot(_b),把下次启动目标切到 B 槽。重启后,如果新系统正常启动,init 会在启动后期调用 markBootSuccessful(),把 B 槽的 successful 置为 true,并清零 tries_remaining。如果启动失败,bootloader 会递减 tries_remaining,归零后自动回退到 A 槽。

判断依据:tries_remaining 是回滚策略的核心计数器。它由 bootloader 在每次尝试启动时递减,而不是由 Android 框架递减。因此,调试回滚问题时应优先查看 bootloader 日志和 misc 分区内容。

三、snapuserd 与 COW 快照:让写入不阻塞运行

在 A/B 升级中,inactive slot 的镜像写入通常采用“先写 COW 快照,再合并”的方式。snapuserd 是 Android 用户空间守护进程,负责管理 dm-snapshot 的 COW 设备。它的作用不是直接写 system 分区,而是把对旧分区的修改记录到 COW 文件中,使旧系统在升级期间仍可正常读写。

以一次典型 OTA 为例:

  1. OTA 客户端把新镜像下载到 /data/ota_package;
  2. snapuserd 为每个需要更新的分区创建 COW 设备,例如 system_b-cow;
  3. 写入新数据时,dm-snapshot 把原始块读入 COW,再写入新块;
  4. 升级完成后,COW 数据在重启时由 snapuserd 合并到目标槽位。

这种设计的关键收益是:旧槽位在升级期间保持完整可读,用户不会感知到分区被覆盖;同时,COW 文件位于 /data,可以按需分配空间,避免一次性占用大量连续分区空间。

A/B 无缝升级:slot 切换、COW 快照与回滚判定链路 Slot A(当前运行) boot_a / system_a / vendor_a successful = true tries_remaining = 0 Slot B(待尝试启动) boot_b / system_b / vendor_b successful = false tries_remaining = 3 OTA 客户端 下载新镜像 → 写入 inactive slot boot control HAL setActiveBootSlot(_b) markBootSuccessful() snapuserd + dm-snapshot COW 设备:system_b-cow 写入新块 → 记录旧块 /data/ota_package COW 文件 按需分配,不覆盖 Slot A Bootloader 读取元数据 tries_remaining 递减 → 决定启动槽位 启动失败回滚 启动成功标记 实线:升级写入与元数据更新 虚线:重启后的成功/回滚判定 红色:回滚路径;绿色:成功路径

四、回滚策略:tries_remaining 与启动成功标记

回滚不是“重新安装旧系统”,而是让 bootloader 在多次启动失败后自动切回旧槽位。判定逻辑如下:

  • 新槽位启动时,bootloader 读取 tries_remaining,若大于 0 则递减并尝试启动;
  • 若系统成功启动到 init 后期,调用 markBootSuccessful(),将 successful 置为 true,tries_remaining 清零;
  • 若连续启动失败,tries_remaining 归零,bootloader 将 active slot 切回旧槽位,并保持旧槽位 successful=true;
  • 回滚后,旧槽位继续正常运行,新槽位可被标记为 unbootable,等待下一次 OTA 重新写入。
注意:markBootSuccessful() 的调用时机非常关键。如果系统在启动早期崩溃,尚未调用该接口,则 tries_remaining 会继续递减,最终触发回滚。因此,调试“升级后无法进入新系统”时,应检查 init 日志中 markBootSuccessful 是否被调用,以及 bootloader 是否已递减计数器。

五、实际调试与验证要点

在工程实践中,可以通过以下方式验证 A/B 升级链路是否正常:

# 查看当前 slot
adb shell getprop ro.boot.slot_suffix

# 查看 boot control HAL 状态
adb shell dumpsys boot_control

# 查看 snapuserd 状态
adb shell ps -A | grep snapuserd

# 查看 COW 设备
adb shell ls -l /dev/block/mapper/ | grep cow

如果 dumpsys boot_control 显示 current_slot 与预期不符,应检查 OTA 客户端是否调用了 setActiveBootSlot;如果 snapuserd 未运行,则 COW 合并可能失败,导致新槽位数据不完整。回滚后,可通过 getprop ro.boot.slot_suffix 确认是否已切回旧槽位,并检查旧槽位的 successful 字段是否仍为 true。

整体来看,A/B 无缝升级的可靠性依赖三个层面的协同:boot control HAL 负责元数据切换,snapuserd 负责 COW 快照与合并,bootloader 负责基于 tries_remaining 的回滚判定。理解这三者的分工与数据流向,才能在 OTA 失败时快速定位是写入问题、元数据问题还是启动判定问题。