2、升级流程详解:OTA升级包下载,升级包校验,写入非活跃分区,切换槽位
AB分区的升级流程,说白了就是一套「先准备好,再切换」的原子操作。我最早接触这个机制时,总觉得步骤有点多,后来在项目里吃过一次亏才明白——每一步都有它存在的道理。
整个流程可以拆成四个阶段:下载 → 校验 → 写入 → 切换。下面我逐个拆开讲。
核心原则:整个升级过程都在非活跃分区进行,系统当前运行的分区不受任何影响。即使升级到一半断电,重启后系统依然能回退到旧分区正常启动。
2.1 阶段一:OTA升级包下载
这一步是起点。设备从服务器拉取升级包,通常是一个压缩的镜像文件(比如 update.zip 或 ota_package.bin)。
我个人习惯把下载过程分成三个子步骤:
- 建立连接:设备通过 HTTPS 或 HTTP 与 OTA 服务器握手,获取升级包的元信息(版本号、文件大小、校验哈希值)。
- 断点续传:如果下载中断,下次可以从断点处继续,不用重头再来。嗯,这里要注意——续传后一定要重新校验已下载部分的完整性。
- 存储到缓存分区:下载的包通常先放到
/cache或/data/ota目录,等校验通过后再解压写入。
我曾经遇到过一个坑:某款设备的缓存分区只有 200MB,但升级包有 500MB。结果下载到一半就报空间不足。后来我们在设计阶段就把缓存分区改成了动态分配,才彻底解决这个问题。
避坑指南:下载阶段一定要做网络超时和重试机制。我曾经见过设备在弱网环境下反复重试,导致电池耗尽、升级失败。建议设置最大重试次数(比如 3 次),超过后提示用户检查网络。
2.2 阶段二:升级包校验
下载完成后,必须校验升级包的完整性。这一步不能省,否则一个损坏的包写进去,系统就变砖了。
校验通常分两层:
| 校验层级 | 校验方式 | 作用 |
|---|---|---|
| 文件级校验 | SHA-256 / MD5 | 确保下载的包没有被篡改或损坏 |
| 签名级校验 | RSA / ECDSA 签名验证 | 确保升级包来自官方,防止恶意刷机 |
具体流程是这样的:
- 计算下载文件的哈希值,与服务器下发的哈希值对比。如果不一致,直接丢弃,重新下载。
- 用公钥验证升级包的数字签名。签名验证通过,才说明这个包是官方签发的。
你想想看,如果只做哈希校验不做签名验证,攻击者完全可以替换掉升级包,同时伪造一个哈希值。所以这两步缺一不可。
重要提醒:公钥必须固化在设备的 BootROM 或安全存储区,不能从外部传入。否则攻击者可以替换公钥,绕过签名校验。我在某款 IoT 设备上就见过这种设计缺陷,后来花了很大力气才修复。
2.3 阶段三:写入非活跃分区
校验通过后,开始解压升级包,把镜像写入非活跃分区。这一步是 AB 分区机制的核心。
写入的内容通常包括:
- boot 镜像:内核和 ramdisk
- system 镜像:系统文件
- vendor 镜像:厂商驱动和库文件
- dtbo 镜像:设备树覆盖
写入过程是流式的——边解压边写入,不需要把整个升级包解压到内存。这样可以节省内存占用,也支持大包升级。
我记得有一次调试,发现写入速度特别慢。排查了半天,原来是 flash 的擦除操作没有做对齐,导致每次写入都要先擦除整个块。后来改成按块对齐写入,速度提升了 3 倍。
经验之谈:写入过程中建议记录进度到 misc 分区。这样如果写入过程中断电重启,系统可以读取进度,决定是继续写入还是回滚。我习惯在写入每个镜像前都写一个 checkpoint。
2.4 阶段四:切换槽位
写入完成后,最后一步是切换槽位。这一步其实很简单——修改 bootloader 的启动参数,把下次启动的槽位指向新写入的分区。
切换槽位的核心操作:
// 伪代码示例:切换槽位
// 假设当前运行在 slot_a,新写入的是 slot_b
// 1. 标记 slot_b 为成功启动(先标记,防止启动失败后回滚)
set_active_slot("slot_b");
set_slot_unbootable("slot_a");
// 2. 设置启动计数器
set_retry_count("slot_b", 0);
// 3. 写入 misc 分区,保存槽位信息
write_misc("active_slot", "slot_b");
// 4. 重启系统
reboot();
这里有个关键点:切换槽位后,系统并不会立即把旧分区标记为「可回滚」。而是等新系统成功启动并运行一段时间后,才把旧分区标记为可覆盖。这个机制叫「启动回滚保护」。
为什么会这样?因为如果新系统启动失败(比如内核 panic),bootloader 检测到启动超时,会自动切回旧分区。如果提前把旧分区标记为可覆盖,那就没有退路了。
注意:切换槽位后,旧分区的数据并不会被立即删除。只有当下一次升级写入时,才会覆盖旧分区。所以理论上,你可以保留两个可启动的系统版本。我建议至少保留一个回滚版本,不要为了省空间而提前清理旧分区。
2.5 整体流程示意图
下面这张图把四个阶段串起来了,你可以对照着看:
2.6 各阶段耗时对比
不同阶段的耗时差异很大。我整理了一份典型数据,供你参考:
| 阶段 | 典型耗时 | 影响因素 | 优化方向 |
|---|---|---|---|
| 下载 | 30秒 ~ 5分钟 | 网络带宽、包大小 | 增量升级、压缩传输 |
| 校验 | 5 ~ 30秒 | CPU性能、包大小 | 硬件加速哈希计算 |
| 写入 | 1 ~ 10分钟 | Flash速度、镜像大小 | 并行写入、块对齐 |
| 切换 | < 1秒 | 几乎无影响 | 无需优化 |
从表格可以看出,写入阶段是耗时最长的环节。我建议在写入时显示进度条,避免用户以为设备卡死了。另外,如果设备支持双通道写入(比如 eMMC 的 RPMB 分区),可以进一步缩短写入时间。
个人建议:在写入阶段,可以顺便做一次「预校验」——边写入边计算哈希,写入完成后直接对比。这样就不用单独花时间做写入后的校验了。我在一个车机项目里用过这个方案,整体升级时间缩短了 20%。
2.7 异常处理策略
升级过程中可能遇到各种异常。我总结了几种常见场景和处理方式:
- 下载中断:支持断点续传,重新下载时从断点继续。
- 校验失败:丢弃升级包,提示用户重新下载。不要尝试修复损坏的包。
- 写入失败:标记非活跃分区为「脏」,下次升级时先擦除再写入。
- 切换后启动失败:bootloader 检测到启动超时,自动切回旧分区。
我曾经遇到过一个极端情况:写入过程中突然断电,导致非活跃分区的数据不完整。重启后 bootloader 检测到槽位状态异常,自动回滚到旧分区。用户完全无感知,只是升级没成功而已。这就是 AB 分区最大的优势——升级失败不影响当前系统。