编程 Android A/B 与 Virtual A/B 更新机制的工程笔记:槽位状态机、boot_control 流程与调试命令

2026-08-31 00:04:21

Android A/B 与 Virtual A/B 更新机制的工程笔记:槽位状态机、boot_control 流程与调试命令

先说结论:A/B 的核心不是“双系统”,而是“可回滚的启动状态机”

A/B(seamless)更新把系统分区拆成 A、B 两组。当前运行槽是 active slot,另一组在正常运行时完全不碰。这样更新写坏了一个槽,bootloader 还能引导另一个,系统不会变砖。

但别把“双槽”理解成“双系统”。它真正复杂的不是分区备份,而是每个槽的 successful / retry-count / unbootable 状态怎么流转。这套状态机才是 bootloader、update_engine、update_verifier 协作的核心。

槽位属性与 bootloader 选槽逻辑

每个槽有两个关键属性:

  • successful:由用户空间设置,标记这个槽能启动、能运行、能自我更新。
  • retry-count:bootloader 启动一个未标记 successful 的槽时递减。

一个槽如果可 boot,但多次尝试都没被标记 successful,bootloader 应当把它标记为 unbootable,并把 active 切到另一个可 boot 的槽。通常就是切回“这次尝试启动前正在运行的那个槽”。

bootloader 选择槽的逻辑,按顺序判断:

  1. 不加载被标记为 unbootable 的槽。
  2. 如果当前槽未标记 successfulretry-count 减到 0,则把它标记为 unbootable,然后选择另一个非 unbootablesuccessful 的槽。
  3. 如果找不到任何可用槽,进 recovery 或显示错误。
  4. 启动时如果当前槽没标记 successful,递减 retry-count

注意:set_active 是唯一能清除 unbootable 标志的方法。它会同时清掉 successful、重置 retry count。这意味着调试时如果把槽搞成 unbootable 且没有别的可启动槽,只有靠 bootloader 的 set_active(HAL 层是 setActiveBootSlot)才能救回来。

boot_control HAL 流程:update_engine 的三个关键节点

HAL 定义在 hardware/libhardware/include/hardware/boot_control.h。update_engine 的典型流程,踩过坑的人会记住这三步:

  1. 更新开始时:把目标槽标记为 unbootablesetSlotAsUnbootable)。同时当前槽总被标记为 successful。原因很直接:防止 bootloader 在更新过程中回退到“即将被写入无效数据的目标槽”。

  2. 写入完成后:把目标槽标记为 active(setActiveBootSlot)。但注意,标记 active 不保证它能完成启动。bootloader 或系统在读到 successful 状态之前,随时可以把 active 切回去。

  3. 新槽启动并完成后重启检查:调用 markBootSuccessful 把当前槽(原目标槽)标记为 successful。只有走到这一步,回滚窗口才正式关闭。

坑在于:很多人以为 setActiveBootSlot 就是“切换完成”,其实它只是“下次尝试启动”。真正承诺的是 markBootSuccessful。中间任何一步失败,bootloader 都会按状态机回退。

update_verifier:为什么必须在 zygote 之前做检查

重启进入新槽后,update_verifier 用 dm-verity 做完整性检查。它必须在 zygote 启动前执行,原因很现实:

Java 服务一旦跑起来,可能做不可逆的系统修改(写数据、改配置),导致即使 dm-verity 检测到损坏,也无法安全回滚。

检查期间,bootloader/kernel 如果检测到 verified boot 或 dm-verity 损坏,可以触发重启。检查完成后,update_verifier 才标记启动成功。

所以“启动成功”不是系统能开机就算,而是完整性验证通过之后才算。这是 A/B 回滚机制的最后一道闸门。

A/B 化对分区布局的影响

  • 不再需要 recovery 分区和 cache 分区。下载的 OTA 包放在 data 分区,recovery 镜像代码放进 boot 分区。
  • A/B 化分区命名:boot_aboot_bsystem_asystem_bvendor_avendor_b
  • 当前 slot 后缀通过 DT 节点 /firmware/android/slot_suffix 或内核 cmdline/bootconfig 的 androidboot.slot_suffix 传递。

调试时要特别注意,如果你在 fastboot 里刷了 boot_a,但内核读到的 slot 后缀不是 _a,那就是 DT/cmdline 没传对,别去查分区表。

测试与调试工具

bootctl(system/extras/bootctl)

常用命令:

# 查看当前运行槽(注意:运行槽和 active 槽可能不一致!)
bootctl get-number

# 查看下次启动要加载的槽
bootctl get-active-boot-slot

# 切换下次启动槽
bootctl set-active-boot-slot SLOT

# 把某槽标记为 unbootable
bootctl set-slot-as-unbootable SLOT

# 查询某槽是否可 boot
bootctl is-slot-bootable SLOT

get-numberget-active-boot-slot 不一样,前者是当前跑的,后者是下次启动的。切换时要确认自己改的是不是“下次启动”的目标。

fastboot 对槽的支持

fastboot --slot _a flash boot boot.img   # 指定槽刷写
fastboot --set-active _b                # 设置 active 槽
fastboot getvar current-slot
fastboot getvar slot-count
fastboot getvar slot-successful:_a
fastboot getvar slot-unbootable:_a
fastboot getvar slot-retry-count:_a

默认情况下,fastboot 刷当前槽。如果你刚切了 active,忘了带 --slot,可能会刷到意料之外的槽。

Virtual A/B(Android 11+):不再保留完整第二套分区

Virtual A/B 的主机制变了:动态分区不再为另一个槽保留完整副本,而是把增量写入 snapshot(COW,写时复制),确认启动成功后再合并回基础分区。合并由内核 dm-user 和用户空间 snapuserd daemon 完成。

压缩快照实验数据:全量 OTA 体积缩小约 45%,增量约 55%。这是官方给的数字,实际取决于分区内容和压缩算法。

合并过程可被中断,重启后恢复。这意味着你不需要在 OTA 后立刻 “等合并完”,重启也不怕,系统会接着合并。

坑点在于:

  • boot 时一阶段 init 必须从 ramdisk 启动 snapuserd 才能挂载分区。这牵扯到 sepolicy 上下文问题:如果 snapuserd 的 SELinux 规则没配对,合并流程起不来,分区挂载失败,系统直接无法启动。
  • fastboot 新增命令和状态:
fastboot getvar snapshot-update-status
# 返回:merging / snapshotted / none
fastboot snapshot-update merge
fastboot snapshot-update cancel
  • MERGING 状态下,erasewipeset_active 会被中止,防止变砖。这是有意为之的防护,不是 bug。调试时别硬来。
  • flashall 前必须先查 snapshot-update-status,如果是 mergingsnapshotted,先执行 snapshot-update cancel 再刷。否则 fastboot 会拒绝 erase/wipe,导致刷机流程卡住。

什么情况下别这么做 / 坑在哪

  1. 别在 OTA 后立刻手动 erase 或 wipe。如果 snapshot 还在 MERGING 状态,fastboot 会拒绝,甚至可能破坏合并状态导致重启后无法挂载分区。
  2. 别把 setActiveBootSlot 当成启动成功的确认。它只决定“下次尝试启动哪个槽”。真正的成功标志是 markBootSuccessful。如果新槽起不来,bootloader 会回退到前一个槽,active 也会被切回去。
  3. 调试时不要随意把当前槽标记为 unbootable。一旦当前槽和另一个槽都变成 unbootable,且没有第三个槽,就只剩 bootloader 的 set_active / HAL 的 setActiveBootSlot 能清掉 unbootable。如果连这个入口都进不去,只能进 recovery 或返厂。
  4. Virtual A/B 下不要绕过 snapuserd 手动挂载分区。尤其是一阶段 init 阶段,没有 snapuserd 的 COW 映射,挂载的就是合并前的数据,或者直接挂载失败。sepolicy 上下文务必在调试早期就验证。
  5. fastboot flashall 前一定先 getvar snapshot-update-status。如果返回 mergingsnapshotted,先 snapshot-update cancel。这条是刷机流程里的标准前置检查,省略的话会遇到莫名的 FAILED (remote: cannot erase in merging state)

状态机速查(写给记性不好的人)

事件槽状态变化下一次启动结果
更新开始,目标槽设为 unbootable目标槽 unbootable=1, successful=0不会启动目标槽
写入完成,目标槽设为 active目标槽 unbootable 被清除(set_active 唯一清除途径),successful=0, retry-count 重置尝试启动目标槽
启动时发现目标槽未 successfulretry-count 递减如果 retry-count 到 0,标记 unbootable
新槽完成 verifier 检查,markBootSuccessfulsuccessful=1正常驻留

最后一条建议:调试 A/B 和 Virtual A/B 时,把 bootctl get-numberget-active-boot-slotgetvar snapshot-update-status 写进你的脚本开头。这三条输出能告诉你:我现在在哪、下次去哪、合并做到哪一步。比看 log 快得多。

复制全文 生成海报 a b recovery Android A B Virtual OTA bootctl

推荐文章

程序员茄子在线接单