22、OTA 升级系统:A/B 分区升级、Seamless Update、升级回滚策略
OTA 升级,说白了就是让车机系统「在线打补丁」。但车机不是手机,升级失败导致黑屏,那可是要出大事的。所以 Android Automotive 搞了一套非常严谨的升级机制——A/B 分区 + Seamless Update + 回滚策略。今天我们就来拆开看看。
为什么需要 A/B 分区?
传统的升级方式是这样的:系统在运行中,下载升级包,然后重启进入 Recovery 模式,把新系统刷到当前分区。这期间如果断电、刷写失败,车机就变砖了。
我早期做车机项目时,就遇到过升级到一半,用户把车断电了。结果第二天回来,系统起不来,只能返厂刷机。嗯,这种体验谁受得了?
A/B 分区的思路很简单:准备两套系统分区,一套叫 slot A,一套叫 slot B。当前运行的是 A,升级包就下载到 B。下载完成后,标记 B 为「可启动」,然后重启。重启后 Bootloader 发现 B 标记有效,就启动 B。如果 B 启动成功,系统就正式切换到 B。如果启动失败,Bootloader 自动回退到 A。
核心思想:升级过程在后台完成,用户几乎无感知。重启只是「切换」到新系统,而不是「安装」新系统。
Seamless Update:无缝升级的流程
Seamless Update 是 A/B 分区机制的上层封装。它让整个升级过程对用户透明。我把它拆成几个阶段:
- 下载阶段:系统在后台下载 OTA 包,下载到 cache 或 data 分区。用户正常开车,完全无感。
- 更新阶段:系统调用
update_engine,把 OTA 包中的镜像写入非活跃 slot。比如当前运行 slot A,就把新系统写入 slot B。 - 标记阶段:写入完成后,更新 slot B 的 metadata,标记为「可启动」。同时设置一个「启动计数」和「回滚标志」。
- 重启阶段:用户下次启动车辆时,Bootloader 检查 slot 状态,发现 B 有效,就启动 B。
- 确认阶段:系统启动后,如果正常运行一段时间(比如 5 分钟),就发送一个「确认成功」的信号,把 slot B 标记为「永久有效」。
你看,整个过程用户只需要一次重启。而且如果 B 启动失败,Bootloader 会自动切回 A,用户甚至不知道升级过。
我个人习惯:在开发阶段,我会把确认时间调短一点,比如 30 秒。但量产车上,建议至少 5 分钟,给系统足够时间加载所有服务。
升级回滚策略:怎么保证不出事?
回滚策略是 A/B 分区的安全网。说白了,就是「新系统不行,我还能退回去」。Android Automotive 的回滚机制分三层:
1. Bootloader 层回滚
Bootloader 在启动时会检查 slot 的 bootable 标志。如果新 slot 启动失败(比如 kernel panic),Bootloader 会递减「启动计数」。当计数归零时,自动切换到另一个 slot。我曾经调试过一个 case,新系统因为驱动不兼容导致启动卡死,Bootloader 在 3 次尝试后自动回滚,用户完全没察觉。
2. 系统层回滚
系统启动后,update_verifier 服务会检查关键分区的完整性。如果发现 system、vendor 等分区校验失败,会主动触发回滚。这个机制我建议一定要保留,别为了省时间关掉它。
3. 用户层回滚
如果系统能启动,但某些功能异常(比如蓝牙连不上),用户可以通过诊断模式手动回滚。不过这个入口一般隐藏得比较深,需要维修人员操作。
注意:回滚不是万能的。如果 OTA 包同时更新了 Bootloader 和系统分区,而新 Bootloader 有 bug,可能导致两个 slot 都起不来。所以 Google 建议:Bootloader 更新要格外谨慎,最好单独走一个「不可回滚」的通道。
代码层面:update_engine 怎么工作?
核心组件是 update_engine,它运行在 system/update_engine/ 目录下。我挑几个关键点说说:
// 检查当前活跃 slot
static bool IsActiveSlot(const std::string& slot) {
// 读取 /proc/cmdline 中的 androidboot.slot_suffix
// 如果匹配则返回 true
}
// 标记新 slot 为可启动
bool MarkBootSuccessful(const std::string& slot) {
// 写入 /misc 分区中的 metadata
// 设置 bootable=1, successful=1
}
// 回滚逻辑
bool RollbackToPreviousSlot() {
// 将当前 slot 标记为 unbootable
// 切换到另一个 slot
// 重启
}
这段代码看起来简单,但实际工程中要考虑很多边界情况。比如:升级过程中断电怎么办?update_engine 有断点续传机制,下载了一半的包可以继续。但如果写入分区时断电,那就只能靠回滚了。
SVG 流程图:A/B 分区升级完整流程
实际项目中的避坑指南
我在几个量产项目中踩过不少坑,挑几个典型的说说:
- 分区大小不一致:有些厂商为了省空间,把 slot B 分得比 slot A 小。结果新系统镜像写不进去。我建议两个 slot 完全对称,别省那几百 MB。
- 回滚后数据丢失:用户升级后在新系统里存了导航收藏,回滚后这些数据就没了。解决方案是把用户数据分区独立出来,不参与 A/B 切换。
- 升级包校验不严:我曾经遇到过 OTA 包在传输过程中损坏,导致写入后系统起不来。后来强制要求对每个镜像做 SHA256 校验,写入前先验证。
总结一下:A/B 分区 + Seamless Update 这套机制,说白了就是「双保险」。一个分区跑,一个分区备着。升级失败了还能退回来。对于车机这种对稳定性要求极高的场景,这是最稳妥的方案。
嗯,关于 OTA 升级的核心机制就聊到这里。如果你在实际项目中遇到升级回滚的问题,可以回头看看这三个关键点:Bootloader 的启动计数、update_engine 的校验逻辑、以及分区 metadata 的正确性。把这三点盯住了,基本不会出大问题。