6、A/B系统升级原理:A/B槽位切换机制、boot_control HAL接口、update_engine服务架构

各位好,今天我们来聊一个在Android系统升级里非常核心的话题——A/B系统升级。说白了,这就是Google为了解决“升级失败变砖”这个老大难问题而设计的方案。我最早接触这个机制是在做一款车机项目的时候,当时客户要求系统升级必须做到“零停机”,用户不能感知到升级过程。嗯,那会儿我就在想,这玩意儿到底是怎么实现的?

6.1 为什么需要A/B系统?

传统的Android升级方式,大家应该都熟悉:系统重启进入Recovery模式,然后开始刷写system、boot、vendor这些分区。这个过程少则几分钟,多则十几分钟。最要命的是,如果中途断电或者刷写出错,设备就真的变砖了。

你想想看,一个智能音箱或者车载中控,升级到一半突然断电,用户回来发现设备开不了机了——这体验得多糟糕。A/B系统就是为了解决这个问题而生的。

核心思想:系统维护两套完整的系统分区(Slot A 和 Slot B),一套在运行,另一套作为备份或升级目标。升级时在后台静默完成,下次重启时切换槽位。

6.2 A/B槽位切换机制

A/B系统的核心就是“双槽位”设计。每个槽位包含完整的系统分区:boot、system、vendor、product等等。设备当前运行的槽位叫“当前槽位”(Current Slot),另一个叫“非当前槽位”(Other Slot)。

升级流程大致是这样的:

  1. 系统在后台下载OTA包,写入非当前槽位
  2. 写入完成后,标记非当前槽位为“可启动”状态
  3. 用户重启设备,Bootloader读取槽位标记,切换到新槽位启动
  4. 新系统启动成功后,标记旧槽位为“可回滚”状态
  5. 如果新系统启动失败,自动回滚到旧槽位

这里有个关键点——槽位切换不是简单的“翻牌子”。Bootloader在启动时会检查槽位的健康状态。我记得有一次调试,发现设备反复重启,最后定位到是槽位标记被错误地写成了“不可启动”。从那以后,我每次做槽位切换测试都会格外小心。

个人经验:槽位切换的时机非常关键。我建议在系统完全启动并稳定运行一段时间后,再执行“槽位确认”操作。过早确认会导致无法回滚,过晚确认又会影响下次升级。

6.3 boot_control HAL接口

boot_control HAL是连接系统服务和底层Bootloader的桥梁。说白了,它就是一套标准化的接口,让上层应用可以控制槽位的状态。

这个HAL定义在hardware/interfaces/boot/1.0/目录下,核心接口包括:

// IBootControl.hal
interface IBootControl {
    // 获取当前启动槽位
    getCurrentSlot() generates (uint32_t slot);
    
    // 获取可启动的槽位数量
    getNumberSlots() generates (uint32_t numSlots);
    
    // 标记槽位为可启动
    setActiveBootSlot(uint32_t slot) generates (bool success);
    
    // 设置槽位启动成功
    setSlotAsBooted(uint32_t slot) generates (bool success);
    
    // 判断槽位是否可启动
    isSlotBootable(uint32_t slot) generates (bool bootable);
    
    // 获取槽位的后缀名(_a 或 _b)
    getSuffix(uint32_t slot) generates (string suffix);
};

这些接口看起来简单,但实际实现时坑不少。比如setSlotAsBooted这个接口,它告诉系统“这个槽位已经成功启动了”。如果实现不当,可能会导致系统误判槽位状态。

避坑指南:我曾经遇到过一个案例,某厂商的boot_control实现里,setSlotAsBooted只是简单地写了一个标记位,没有做持久化存储。结果设备断电重启后,标记丢失,系统认为新槽位从未启动成功,于是回滚到旧槽位。嗯,这个问题排查了整整两天。

6.4 update_engine服务架构

update_engine是整个A/B升级的“大脑”。它负责下载OTA包、校验签名、写入分区、管理槽位状态。这个服务运行在后台,用户几乎感知不到它的存在。

它的架构大致分为三层:

层级 组件 职责
应用层 SystemUpdaterSample 提供用户交互界面,触发升级检查
服务层 update_engine 核心逻辑:下载、校验、写入、槽位管理
硬件抽象层 boot_control HAL 与Bootloader交互,控制槽位状态

update_engine的工作流程是这样的:

  1. 接收升级请求,解析OTA包的元数据
  2. 检查非当前槽位的空间是否足够
  3. 下载OTA包,同时进行签名校验
  4. 将payload写入非当前槽位的各个分区
  5. 调用boot_control HAL标记新槽位为可启动
  6. 触发系统重启

这里有个细节——update_engine支持“流式写入”,也就是边下载边写入。这样做的好处是节省存储空间,不需要先把整个OTA包下载到本地再解压。但坏处是,如果网络中断,写入到一半的分区就废了,需要重新下载。

核心要点:update_engine的写入操作是“幂等”的。也就是说,即使写入过程中断,重新执行也不会破坏现有系统。这是A/B系统安全性的重要保障。

6.5 整体架构图

下面这张图展示了A/B系统升级的整体流程和组件关系。我画这张图的时候,特意把关键的数据流和控制流都标出来了。

A/B系统升级架构图 应用层 SystemUpdaterSample / 设置应用 触发升级检查、显示进度、用户确认 服务层 update_engine 服务 下载OTA包 → 校验签名 → 写入分区 → 管理槽位 硬件抽象层 boot_control HAL getCurrentSlot / setActiveBootSlot / setSlotAsBooted 升级请求 槽位控制指令 Bootloader / 底层存储 读写槽位标记 应用层 服务层 HAL层

6.6 槽位状态管理

槽位状态管理是A/B系统最精妙的部分。每个槽位都有几个关键状态:

  • 可启动(Bootable):槽位包含完整的系统,可以正常启动
  • 已激活(Active):槽位被标记为下次启动的目标
  • 已确认(Confirmed):槽位已经成功启动并运行稳定
  • 可回滚(Rollback):旧槽位保留,可以回滚

这些状态存储在misc分区或者Bootloader的私有区域。我建议你在调试时,用adb shell getprop ro.boot.slot_suffix查看当前槽位,用adb shell bootctl命令查看槽位状态。

调试技巧:在开发阶段,可以用adb shell bootctl set-active-boot-slot 0手动切换槽位。但注意,生产环境千万别这么干,否则可能把设备搞成砖。

6.7 总结

A/B系统升级的核心就是“双槽位 + 后台升级 + 原子切换”。它让升级变得安全、可靠、无感。我个人觉得,这是Android系统设计里最优雅的机制之一。

当然,实现起来并不简单。boot_control HAL的稳定性、update_engine的容错能力、槽位状态的一致性——每一个环节都可能出问题。我在项目中踩过的坑,十个手指头都数不过来。但正是这些经验,让我对这套机制有了更深的理解。

好了,关于A/B系统升级的原理就聊到这里。如果你在实际开发中遇到什么问题,欢迎随时交流。