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 信息。升级流程大致是这样:
- 解析 payload:update_engine 读取 payload,提取出新的 metadata 内容。
- 写入备用槽位:当前槽位的 metadata 不动,先写到另一个槽位。比如当前是 slot_a,就写到 slot_b。
- 切换槽位:升级完成后,重启时 bootloader 检查新槽位的 metadata 是否完整。如果完整,就切过去。
- 更新当前槽位:系统启动后,再把新 metadata 同步到当前槽位。
我在项目中遇到过一个问题:升级包里的 metadata 版本跟 bootloader 不兼容。结果 bootloader 读不懂新 metadata,直接拒绝启动。后来我们加了个版本号校验,才解决。
metadata 损坏了怎么办?
为什么会损坏?最常见的原因就是升级过程中突然断电。或者,emmc 坏块刚好落在 metadata 区域。
我曾经在调试一款平板时,遇到 OTA 升级到一半电池没电了。重启后系统直接进 recovery,提示“metadata 校验失败”。当时心里一凉,但还好我们有预案。
恢复策略一:使用备用槽位
A/B 机制的好处就在这里。如果当前槽位的 metadata 坏了,bootloader 会自动尝试另一个槽位。只要另一个槽位的 metadata 是好的,系统就能正常启动。
我的建议:永远不要只依赖一个槽位。OTA 升级时,务必保证两个槽位的 metadata 至少有一个是完整的。
恢复策略二:从 recovery 重建 metadata
如果两个槽位都坏了,那就得进 recovery 模式。recovery 里有个工具叫 lpdump 和 lpadd,可以手动重建 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 增加。如果 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