18、OTA升级与回滚:boot_control、slot切换、回滚保护
OTA升级,说白了就是让手机“空中换血”。但换血有风险,万一换到一半断电了、文件损坏了,手机变砖怎么办?这就是我们今天要聊的核心——boot_control、slot切换和回滚保护。
我个人习惯把这套机制叫做“双保险”。Android从7.0开始引入A/B分区(也叫无缝升级),就是为了解决升级失败的问题。你想想看,系统有两套分区,一套在跑,一套在升级,互不干扰。就算升级失败,大不了切回旧分区,手机照样能用。
18.1 boot_control HAL:谁来管slot切换?
boot_control是一个硬件抽象层(HAL),它负责管理slot的切换。说白了,它就是系统里那个“管钥匙的人”。
我在项目中遇到过一个问题:升级完成后,系统重启了,但死活不切到新分区。查了半天,发现是boot_control HAL的实现里,setActiveBootSlot() 没有正确写入misc分区。嗯,这里要注意,misc分区是存储slot信息的关键区域,写错了就全白搭。
来看一个典型的boot_control HAL接口定义:
// hardware/interfaces/boot/1.0/IBootControl.hal
interface IBootControl {
// 获取当前活跃的slot
getActiveBootSlot() generates (uint32_t slot);
// 设置当前活跃的slot
setActiveBootSlot(uint32_t slot) generates (bool success);
// 标记当前slot为成功启动
markBootSuccessful() generates (bool success);
// 获取slot数量(通常是2)
getNumSlots() generates (uint32_t numSlots);
// 判断当前slot是否可以启动
isSlotBootable(uint32_t slot) generates (bool bootable);
};
每个方法都很关键。比如markBootSuccessful(),它决定了系统是否“确认”当前slot是好的。如果升级后第一次启动没调用这个,下次重启可能又会切回旧slot。
18.2 Slot切换:升级的“换手”逻辑
OTA升级时,系统会把新固件写入非活跃slot(比如当前是slot A,就写slot B)。写完后,设置slot B为活跃,然后重启。
重启后,bootloader会检查misc分区里的slot信息,决定从哪个slot启动。如果一切正常,系统启动后,init进程会调用markBootSuccessful(),确认新slot可用。
我画了一张流程图,帮你理清这个逻辑:
你看,整个流程其实不复杂。但有一个关键点:回滚保护。如果升级后系统连续启动失败,bootloader会自动切回旧slot。这个机制是怎么实现的?
18.3 回滚保护:为什么能“救砖”?
回滚保护的核心是启动计数。每次从新slot启动,bootloader会递增一个计数器。如果系统正常启动并调用了markBootSuccessful(),计数器清零。如果启动失败,计数器一直累加,直到超过阈值(通常是7次),bootloader就认为这个slot“坏了”,自动切回旧slot。
我曾经踩过一个坑:某款设备升级后,每次启动都卡在开机动画,但系统并没有真正崩溃。结果计数器一直没清零,7次后自动回滚了。用户反馈说“手机自己好了”,其实就是回滚保护在起作用。
回滚保护的关键参数:
- 最大启动尝试次数:通常为7次,由bootloader定义
- 成功启动标记:由init进程在系统完全启动后调用
- misc分区:存储slot状态和计数信息
来看一下misc分区里存储的数据结构:
// bootloader_message.h
struct bootloader_message {
char command[32]; // 启动命令
char status[32]; // 状态信息
char recovery[768]; // recovery模式参数
// A/B slot 信息
struct slot_metadata {
uint8_t priority; // 优先级(0-15)
uint8_t tries_remaining; // 剩余尝试次数
uint8_t successful_boot; // 是否成功启动
uint8_t verity_corrupted; // 是否校验损坏
} slot_suffix[2]; // 两个slot的元数据
};
每次启动,bootloader会读取这个结构。如果tries_remaining大于0,就递减并尝试启动。如果启动成功,successful_boot被置为1。如果所有尝试都用完了,就切到另一个slot。
18.4 实战中的避坑指南
我在多个项目中调试过OTA回滚问题,总结了几条经验:
避坑1:markBootSuccessful() 的调用时机
我曾经遇到一个案例:系统在启动早期就调用了markBootSuccessful(),结果后续初始化失败,但slot已经被标记为“成功”。下次升级时,旧slot被覆盖,再也回不去了。
建议:在系统完全就绪后再调用,比如所有核心服务启动完毕、用户界面显示之后。
避坑2:misc分区的写入可靠性
misc分区通常很小(几MB),但写入频率不高。如果写入时掉电,可能导致数据损坏。我建议在写入时做CRC校验,或者使用双备份机制。
注意:有些厂商把misc分区放在eMMC的特定区域,写入前要确保分区没有被挂载。
还有一个容易被忽略的点:slot切换后的内核兼容性。如果新slot的内核模块和旧slot的硬件驱动不兼容,系统可能根本起不来。这时候回滚保护也救不了你——因为bootloader根本看不到内核启动。所以,升级前一定要做兼容性测试。
18.5 总结一下
OTA升级与回滚,核心就是三件事:
- boot_control HAL:管理slot的激活、标记、查询
- slot切换:写入非活跃slot,设置活跃位,重启
- 回滚保护:通过启动计数和misc分区,自动切回旧slot
这套机制在Android 10之后又加入了虚拟A/B分区(Virtual A/B),进一步减少了存储空间占用。但核心逻辑没变——还是那套“双保险”思路。
嗯,最后说一句:如果你在调试OTA回滚问题,先检查misc分区的内容。用hexdump看一眼,很多问题就一目了然了。
公众号:蓝海资料掘金营,微信deep3321