28、OTA升级与metadata:动态分区metadata更新、损坏恢复

动态分区这套机制,说白了就是让系统分区“活”起来。但活起来的东西,就得有个“账本”来管。这个账本,就是 metadata。今天我们就聊聊,OTA 升级时 metadata 怎么更新,万一它坏了又怎么救。

metadata 到底是什么?

我个人习惯把 metadata 理解为一张“分区布局表”。它记录了当前系统里有哪些动态分区、每个分区的名字、大小、起始位置、还有哪些是逻辑分区、哪些是物理分区。

在 Android 里,metadata 存放在 super 分区的末尾。具体位置由 lp_metadata_header 里的 metadata_slot 决定。嗯,这里要注意:metadata 是有槽位(slot)的,跟 A/B 分区机制对应。

核心要点:metadata 损坏 = 系统不知道分区在哪 = 无法挂载 = 变砖。

OTA 升级时 metadata 怎么更新?

OTA 升级包里的 payload.bin 不光包含系统镜像,还包含新的 metadata 信息。升级流程大致是这样:

  1. 解析 payload:update_engine 读取 payload,提取出新的 metadata 内容。
  2. 写入备用槽位:当前槽位的 metadata 不动,先写到另一个槽位。比如当前是 slot_a,就写到 slot_b。
  3. 切换槽位:升级完成后,重启时 bootloader 检查新槽位的 metadata 是否完整。如果完整,就切过去。
  4. 更新当前槽位:系统启动后,再把新 metadata 同步到当前槽位。

我在项目中遇到过一个问题:升级包里的 metadata 版本跟 bootloader 不兼容。结果 bootloader 读不懂新 metadata,直接拒绝启动。后来我们加了个版本号校验,才解决。

metadata 损坏了怎么办?

为什么会损坏?最常见的原因就是升级过程中突然断电。或者,emmc 坏块刚好落在 metadata 区域。

我曾经在调试一款平板时,遇到 OTA 升级到一半电池没电了。重启后系统直接进 recovery,提示“metadata 校验失败”。当时心里一凉,但还好我们有预案。

恢复策略一:使用备用槽位

A/B 机制的好处就在这里。如果当前槽位的 metadata 坏了,bootloader 会自动尝试另一个槽位。只要另一个槽位的 metadata 是好的,系统就能正常启动。

我的建议:永远不要只依赖一个槽位。OTA 升级时,务必保证两个槽位的 metadata 至少有一个是完整的。

恢复策略二:从 recovery 重建 metadata

如果两个槽位都坏了,那就得进 recovery 模式。recovery 里有个工具叫 lpdumplpadd,可以手动重建 metadata。

# 查看当前 super 分区布局
lpdump --slot 0

# 如果 metadata 损坏,可以用 --repair 尝试修复
lpdump --repair --slot 0

# 手动添加一个逻辑分区
lpadd --slot 0 --partition system --size 2G

嗯,这里要注意:lpdump --repair 只能修复 metadata 的校验和错误。如果数据本身被覆盖了,那就只能重新刷机了。

恢复策略三:fastboot 强制写入

如果 recovery 也进不去,那就只能上 fastboot 了。用 fastboot flash super super.img 把整个 super 分区重刷一遍。这招比较暴力,但管用。

警告:fastboot 重刷 super 分区会清空所有动态分区里的数据。包括用户数据分区(如果 userdata 也在动态分区里的话)。操作前务必确认。

metadata 更新的关键代码路径

我们来看看 update_engine 里是怎么处理 metadata 的。核心逻辑在 dynamic_partition_control_android.cc 里:

// 伪代码,展示核心流程
bool DynamicPartitionControlAndroid::UpdateMetadata(
    const std::string& payload_metadata) {
  // 1. 解析 payload 中的 metadata
  auto new_metadata = ParseMetadata(payload_metadata);
  
  // 2. 校验版本兼容性
  if (!CheckMetadataVersion(new_metadata)) {
    LOG(ERROR) << "Metadata version mismatch";
    return false;
  }
  
  // 3. 写入备用槽位
  if (!WriteMetadataToSlot(new_metadata, kOtherSlot)) {
    LOG(ERROR) << "Failed to write metadata to other slot";
    return false;
  }
  
  // 4. 校验写入结果
  if (!VerifyMetadataIntegrity(kOtherSlot)) {
    LOG(ERROR) << "Metadata integrity check failed";
    return false;
  }
  
  return true;
}

这段代码看着简单,但实际坑不少。比如 CheckMetadataVersion 这个函数,不同厂商的 bootloader 支持的版本不一样。我见过某厂商的 bootloader 只支持 v1,但 OTA 包里的 metadata 是 v2,结果直接翻车。

metadata 的校验机制

metadata 本身是有校验的。它包含一个 CRC32 校验和,覆盖整个 metadata 数据块。每次读取 metadata 时,系统都会重新计算 CRC32,跟存储的值比对。如果不一致,就认为 metadata 损坏。

字段 大小 说明
magic 4 字节 固定为 "LPMA"
major_version 2 字节 主版本号
minor_version 2 字节 次版本号
metadata_size 4 字节 metadata 数据大小
metadata_crc32 4 字节 CRC32 校验和
metadata_slot 4 字节 当前槽位号

你想想看,如果 CRC32 校验失败,系统会怎么做?它会尝试从另一个槽位读取 metadata。如果另一个槽位也坏了,那就只能进 recovery 了。

SVG 流程图:metadata 更新与恢复流程

metadata 更新与恢复流程 OTA 升级开始 解析 payload 中的 metadata 写入 metadata 到备用槽位 CRC32 校验 metadata 完整性 校验成功 → 切换槽位 校验失败 → 尝试另一槽位 正常路径 异常路径

避坑指南

我踩过的坑,总结成几条经验:

  • metadata 大小要预留足够:动态分区数量会随着 OTA 增加。如果 metadata 区域太小,后面加分区就写不下了。我建议至少预留 4KB 的 metadata 空间。
  • 升级过程中不要断电:这个听起来像废话,但很多设备确实没有掉电保护。如果实在无法避免,至少保证写入 metadata 时是原子操作。
  • bootloader 要能读两个槽位的 metadata:有些 bootloader 只读当前槽位的 metadata,另一个槽位坏了它也不知道。这会导致系统反复重启。
  • metadata 版本号要向前兼容:新版本的 metadata 格式,bootloader 要能读懂。否则 OTA 升级后,bootloader 不认识新 metadata,直接变砖。

我的经验:我曾经在一个项目里,metadata 的 CRC32 校验算法写错了。结果每次升级后 metadata 都校验失败,系统自动回滚。查了两天才发现是 CRC32 的初始值设错了。嗯,这种低级错误,犯过一次就再也不会犯了。

总的来说,metadata 是动态分区的命根子。OTA 升级时,保证 metadata 的正确写入和校验,是系统稳定性的底线。万一坏了,A/B 槽位和 recovery 模式就是最后的救命稻草。


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