14、OTA升级流程:update_engine、update_verifier、postinstall

各位好,今天我们来聊聊OTA升级流程中的三个关键角色:update_engineupdate_verifierpostinstall。这三个组件,说白了就是升级过程中的「总指挥」、「质检员」和「装修队」。我在早期做系统开发时,经常把这三者的职责搞混,结果升级流程跑不通,还以为是内核出了问题。后来才明白,它们各司其职,缺一不可。

一、整体流程概览

先看一张我画的流程图,帮你快速建立整体认知:

OTA升级核心流程 阶段1:下载与准备 update_engine 下载OTA包 写入非活跃slot 阶段2:校验 update_verifier 校验哈希 验证签名与完整性 阶段3:安装 postinstall 执行 更新系统分区 重启进入新slot 阶段4:启动后校验 update_verifier 再次校验 确认系统可用 升级成功 ✓

嗯,这张图把整个流程分成了四个阶段。你会发现,update_engine 负责前期的下载和写入,update_verifier 负责校验,postinstall 负责安装后的收尾工作。重启之后,update_verifier 还会再出来检查一次,确保系统真的没问题。

二、update_engine:升级的总指挥

update_engine 是 A/B 系统升级的核心守护进程。我个人习惯把它理解为「升级调度器」——它不直接操作分区,而是协调所有升级动作。

2.1 主要职责

  • 下载 OTA 包:从服务器拉取增量或完整包
  • 解析 payload:读取 payload 中的元数据,确定要更新的分区
  • 写入非活跃 slot:将新系统写入当前未使用的 slot(比如当前是 slot_a,就写 slot_b)
  • 管理状态机:维护升级状态,支持断点续传

核心要点:update_engine 运行在用户空间,通过 binder 接口与系统服务通信。它不直接操作块设备,而是通过 libupdate_engine 库调用内核的 dm-verity 和 loop 设备。

2.2 状态机示例

我在项目中遇到过一个问题:升级到一半突然断电,重启后 update_engine 怎么知道该从哪继续?答案就在它的状态机里:

// 简化的状态机定义
enum UpdateState {
  STATE_IDLE = 0,
  STATE_CHECKING_FOR_UPDATE,
  STATE_UPDATE_AVAILABLE,
  STATE_DOWNLOADING,
  STATE_DOWNLOADING_FINISHED,
  STATE_VERIFYING,
  STATE_UPDATING,
  STATE_UPDATED_NEED_REBOOT,
  STATE_ERROR,
};

每次状态变更,update_engine 都会把当前状态写入 /data/misc/update_engine/prefs 目录。重启后,它会读取这个文件,从断点处继续。说白了,这就是个「记笔记」的机制。

三、update_verifier:系统的质检员

update_verifier 这个名字容易让人误解——它不是在升级过程中做校验,而是在重启后做校验。为什么?

你想想看,升级过程中 update_engine 已经把数据写入了非活跃 slot,但那时候系统还在运行,没法验证新系统能不能正常启动。所以 Android 的设计思路是:先重启,让 bootloader 加载新系统,然后由 update_verifier 在 init 阶段执行校验。

3.1 校验什么

校验项 说明 失败后果
dm-verity 哈希树 验证系统分区的完整性 回滚到旧 slot
文件系统一致性 检查 ext4/f2fs 是否损坏 尝试修复,失败则回滚
关键服务可用性 确认核心服务能正常启动 触发 rescue 模式

注意:update_verifier 的校验是「软校验」——它不会阻塞系统启动,而是并行执行。如果校验失败,它会设置一个标志位,下次重启时 bootloader 会读取这个标志,自动回滚到旧 slot。

3.2 我曾经踩过的坑

我曾经遇到过一个问题:升级后系统能正常启动,但 update_verifier 报告 dm-verity 校验失败。查了半天,发现是 OTA 包生成时,哈希树计算用的块大小和实际分区不一致。嗯,这种问题特别隐蔽,因为系统能跑,但每次重启都会触发回滚。后来我养成了一个习惯:生成 OTA 包后,先用 avbtool verify_image 手动检查一遍哈希树。

四、postinstall:系统的装修队

postinstall 是升级流程的「收尾阶段」。它不是一个单独的进程,而是一组在 init.rc 中定义的脚本或二进制程序。update_engine 在写入新系统后,会调用 postinstall 来完成一些「脏活累活」。

4.1 典型任务

  • 更新 bootloader 参数:比如修改 slot-suffix,告诉 bootloader 下次从新 slot 启动
  • 清理缓存:删除旧的 dalvik-cache 或 odex 文件
  • 更新 vendor 分区:有些厂商会把驱动放在 vendor 分区,postinstall 负责更新
  • 执行厂商自定义脚本:比如修改 modem 固件、校准传感器等

个人建议:postinstall 脚本一定要设计成「幂等」的——也就是执行多次和一次效果相同。为什么?因为如果升级过程中断电,重启后 update_engine 可能会重新执行 postinstall。如果脚本不是幂等的,就可能出现重复写入、文件冲突等问题。

4.2 代码示例

一个典型的 postinstall 脚本长这样:

# /system/bin/postinstall.sh
#!/system/bin/sh

# 更新 slot 信息
echo "Updating slot suffix..."
echo -n "_b" > /misc/ota/slot-suffix

# 清理旧缓存
rm -rf /data/dalvik-cache/*
rm -rf /data/system/package_cache/*

# 执行厂商脚本
if [ -f /vendor/bin/postinstall_vendor.sh ]; then
    /vendor/bin/postinstall_vendor.sh
fi

# 标记升级完成
setprop sys.ota.completed 1

exit 0

注意看最后一行 setprop sys.ota.completed 1——这个属性会被 init 进程监听,一旦设置为 1,init 就会触发重启。所以 postinstall 执行完毕后,系统会自动重启进入新 slot。

五、三者的协作关系

讲到这里,你应该能看出这三者的分工了:

  1. update_engine:在旧系统中运行,负责下载和写入新系统
  2. postinstall:在旧系统中运行(但由 update_engine 触发),负责收尾工作
  3. update_verifier:在新系统中运行(重启后),负责校验新系统是否可用

说白了,这是一个「先写后验」的流程。update_engine 写完了,postinstall 收尾了,然后重启,update_verifier 再检查。如果检查不过,就回滚。这种设计保证了升级的安全性——你永远不会启动一个损坏的系统。

核心总结

  • update_engine 是「写」的组件
  • postinstall 是「收尾」的组件
  • update_verifier 是「验」的组件
  • 三者通过 slot 切换属性系统 协同工作

嗯,这就是 OTA 升级流程中三个核心组件的全貌。实际项目中,你可能会遇到各种边界情况——比如升级过程中存储空间不足、postinstall 脚本执行超时、update_verifier 误报校验失败等等。但只要你理解了它们各自的职责和协作方式,排查问题就会变得有条理。


公众号:蓝海资料掘金营,微信deep3321