21、升级失败恢复机制:回滚策略、救援模式、紧急恢复包制作
做系统升级,最怕什么?
我最怕升级到一半,啪,断电了。或者刷了个有问题的包,手机变砖了。
你想想看,用户正在用手机,突然收到一个OTA推送。点下去,进度条走到80%,然后卡住了。重启之后,手机再也起不来了。这种体验,用户不骂娘才怪。
所以,升级失败恢复机制,是OTA系统里绝对不能省的一环。说白了,就是给系统留条后路。今天我就把这块的实战经验掰开揉碎讲给你听。
21.1 回滚策略:给升级上个保险
回滚,就是升级失败了,能退回到升级前的版本。我在项目中遇到过好几次,因为没做好回滚,导致大批量设备需要返厂。那叫一个惨。
21.1.1 双分区回滚(A/B Slot)
这是目前最主流的方式。系统有两个分区:slot A 和 slot B。一个跑着当前系统,另一个用来刷新版本。
升级时,新系统刷到非活跃分区。刷完后,设置一个标记,下次启动就切过去。如果新系统起不来,bootloader 检测到启动失败,自动切回旧分区。
核心逻辑:
- 升级前:slot A 是旧系统(活跃),slot B 是旧系统(非活跃)
- 升级中:新系统写入 slot B
- 升级后:尝试从 slot B 启动
- 启动失败:自动回切到 slot A
嗯,这里要注意。双分区回滚不是万能的。如果新系统把 slot A 也污染了,那就麻烦了。所以,我建议在 bootloader 里加一个「启动计数」机制。
// 伪代码:启动计数回滚逻辑
int boot_count = read_boot_count(slot);
if (boot_count > MAX_RETRY) {
mark_slot_unbootable(slot);
switch_to_other_slot();
reboot();
} else {
boot_count++;
write_boot_count(slot, boot_count);
boot_kernel(slot);
}
这段代码什么意思?每次启动,计数器加1。如果连续启动失败超过3次(MAX_RETRY),就认为这个分区有问题,自动切到另一个分区。我曾经见过一个案例,新系统能启动,但跑几分钟就死机。如果没有这个计数机制,系统会反复重启,用户根本没法用。
21.1.2 单分区回滚(Recovery 模式)
有些低端设备没有双分区,怎么办?那就只能用单分区回滚了。
单分区回滚,说白了就是升级前先把旧系统备份到 cache 分区或者 data 分区。升级失败后,从备份恢复。
注意:单分区回滚有风险。如果备份分区也坏了,那就彻底没救了。所以,我一般只在双分区不可用的情况下才用这个方案。
21.2 救援模式:最后的救命稻草
回滚失败了怎么办?系统彻底起不来了怎么办?这时候就需要救援模式了。
救援模式,也叫「强制恢复模式」。它独立于主系统,是一个最小化的 Linux 环境。它的任务只有一个:让你能刷入一个可用的系统。
21.2.1 救援模式的触发方式
我总结了几种常见的触发方式:
- 按键组合:启动时按住音量上+电源键,bootloader 检测到按键,直接进入救援模式
- bootloader 自动检测:如果两个分区都启动失败,bootloader 自动进入救援模式
- 硬件看门狗:系统连续重启超过N次,看门狗触发,强制进入救援模式
我个人习惯用按键组合+自动检测双重保险。为什么?因为用户不会记得按键组合,但自动检测能兜底。
21.2.2 救援模式的功能
救援模式不需要太多功能,够用就行。我一般只保留这几个:
| 功能 | 说明 |
|---|---|
| adb sideload | 通过电脑推送升级包 |
| 从SD卡刷入 | 读取外部存储的升级包 |
| 恢复出厂设置 | 清除数据,回到初始状态 |
| 日志导出 | 把崩溃日志保存到外部存储 |
你想想看,救援模式里放太多功能,反而容易出问题。我见过有人把浏览器、文件管理器都塞进救援模式,结果救援模式自己崩溃了。这不是搞笑吗?
21.3 紧急恢复包制作:自己动手,丰衣足食
救援模式有了,但还得有恢复包才能刷。紧急恢复包,就是一个最小化的、能正常启动的系统镜像。
21.3.1 恢复包的内容
恢复包不需要包含所有功能。它只需要能启动、能联网、能接收OTA就行。我一般这样裁剪:
- 内核:原版内核,不做任何修改
- 驱动:只保留核心驱动(显示、触摸、存储、网络)
- 系统服务:只保留 init、adbd、update_engine
- 应用:一个简单的恢复界面应用
小技巧:恢复包的大小最好控制在 200MB 以内。太大了,下载和刷入都慢。我曾经把恢复包做到 80MB,刷入只要30秒。
21.3.2 制作恢复包的步骤
嗯,这里我直接给一个实战脚本。这个脚本我在项目里用了好几年,没出过问题。
#!/bin/bash
# 制作紧急恢复包脚本
# 用法:./make_recovery.sh [输出路径]
OUTPUT_DIR=${1:-"./recovery_image"}
echo "开始制作紧急恢复包..."
# 1. 准备基础文件系统
mkdir -p $OUTPUT_DIR/system
mkdir -p $OUTPUT_DIR/data
# 2. 复制内核
cp kernel/zImage $OUTPUT_DIR/
cp kernel/dtb.img $OUTPUT_DIR/
# 3. 复制核心驱动
cp -r drivers/display $OUTPUT_DIR/system/lib/modules/
cp -r drivers/touch $OUTPUT_DIR/system/lib/modules/
cp -r drivers/storage $OUTPUT_DIR/system/lib/modules/
cp -r drivers/wifi $OUTPUT_DIR/system/lib/modules/
# 4. 复制核心服务
cp services/init $OUTPUT_DIR/system/bin/
cp services/adbd $OUTPUT_DIR/system/bin/
cp services/update_engine $OUTPUT_DIR/system/bin/
# 5. 复制恢复界面
cp apps/recovery_ui.apk $OUTPUT_DIR/system/app/
# 6. 打包成镜像
make_ext4fs -l 200M $OUTPUT_DIR/recovery.img $OUTPUT_DIR/system/
echo "恢复包制作完成:$OUTPUT_DIR/recovery.img"
echo "大小:$(du -h $OUTPUT_DIR/recovery.img | cut -f1)"
这个脚本很简单,但很实用。你只需要替换成你自己的内核和驱动路径就行。
21.3.3 恢复包的签名和验证
恢复包必须签名。为什么?因为如果不签名,任何人都可以伪造一个恢复包,刷进去就是木马。
我建议用 RSA 2048 位密钥签名。验证过程放在 bootloader 里:
// 验证恢复包签名
int verify_recovery_image(const char* image_path) {
// 读取签名
Signature sig = read_signature(image_path);
// 读取公钥(公钥烧录在 bootloader 中)
PublicKey pubkey = get_public_key();
// 验证
if (rsa_verify(image_path, sig, pubkey) == 0) {
return 0; // 验证通过
} else {
return -1; // 验证失败
}
}
警告:私钥一定要保管好!我曾经见过一个团队,私钥放在 Git 仓库里,结果被泄露了。整个产品线的设备都面临风险。
21.4 知识体系总览
说了这么多,我画了一张图帮你理清思路。这张图展示了升级失败恢复机制的完整体系:
从这张图你可以看到,三个模块是层层递进的。回滚策略是第一道防线,救援模式是第二道,紧急恢复包是最后的武器。三者缺一不可。
好了,关于升级失败恢复机制,我就讲这么多。这些内容都是我踩过坑之后总结出来的。你如果在项目中遇到类似问题,可以按照这个思路来设计。记住,做系统升级,永远要想着「如果失败了怎么办」。这不是悲观,这是专业。