27、OTA升级与recovery:recovery模式下的动态分区操作

说到OTA升级,很多人第一反应是“下载包、重启、完事”。但真正深入到recovery模式下,你会发现事情远没那么简单。尤其是动态分区引入之后,recovery里的操作逻辑发生了根本性的变化。

我个人习惯把recovery模式下的动态分区操作分成三个阶段:挂载阶段、写入阶段、切换阶段。每个阶段都有各自的坑,咱们一个一个聊。

27.1 为什么recovery要懂动态分区?

传统分区时代,recovery只需要知道“把数据写到/dev/block/bootdevice/by-name/system”就行了。但动态分区不一样——分区表是虚拟的,真正的物理分区只有super一个。

说白了,recovery得先学会“拆解”super分区,才能找到真正的目标分区。这就像你收到一个快递箱(super),里面装着好几个小盒子(system、vendor、product),你得先拆开大箱子才能拿到里面的东西。

核心变化:recovery不再直接操作物理分区,而是通过dm-linear设备映射来访问动态分区。

27.2 recovery下的分区挂载流程

嗯,这里要注意一个关键点:recovery环境下的分区挂载和正常系统下完全不同。正常系统有init进程帮你处理一切,但recovery里你得手动来。

我遇到过一个问题:某次OTA升级时,recovery死活挂不上vendor分区。查了半天,原来是super分区里的vendor metadata损坏了。这个坑让我意识到,理解挂载流程有多重要。

27.2.1 第一步:读取分区表

recovery启动后,首先会读取/metadata/gsi/lp_metadata或者/super分区末尾的LVM metadata。这个metadata里记录了所有动态分区的布局信息。

// 伪代码:读取动态分区metadata
int read_lp_metadata(struct lp_metadata *meta) {
    // 从super分区末尾读取metadata
    int fd = open("/dev/block/by-name/super", O_RDONLY);
    lseek(fd, -LP_METADATA_SIZE, SEEK_END);
    read(fd, meta, LP_METADATA_SIZE);
    
    // 验证magic number
    if (meta->magic != LP_METADATA_MAGIC) {
        // 尝试从备份位置读取
        return read_backup_metadata(meta);
    }
    return 0;
}

27.2.2 第二步:创建设备映射

拿到metadata之后,recovery会为每个动态分区创建dm-linear设备。这个过程说白了就是告诉内核:“从super分区的第X个扇区开始,取Y个扇区,映射成一个新的块设备”。

动态分区名 在super中的偏移 大小 映射后的设备节点
system_a 0x00000000 2GB /dev/block/dm-0
vendor_a 0x80000000 512MB /dev/block/dm-1
product_a 0xA0000000 1GB /dev/block/dm-2

小技巧:调试时可以用dmsetup table命令查看当前所有的dm-linear映射,确认分区是否正确挂载。

27.3 OTA升级时的分区写入操作

升级包下载完成后,recovery会解析包里的payload.bin,然后开始往目标分区写数据。动态分区下的写入操作有几个特殊之处。

27.3.1 写入前的空间检查

你想想看,动态分区的空间是弹性分配的。如果升级包里的system镜像比原来大,但super分区里剩余空间不够怎么办?

我曾经遇到一个案例:某次OTA升级时,product分区需要扩容200MB,但super分区只剩150MB的空闲空间。结果升级到一半就失败了,手机变砖。后来我们加了一个前置检查逻辑。

// 空间检查逻辑
int check_space_for_update(struct update_plan *plan) {
    uint64_t total_needed = 0;
    uint64_t available = get_super_free_space();
    
    for (int i = 0; i < plan->num_partitions; i++) {
        if (plan->partitions[i].new_size > 
            plan->partitions[i].current_size) {
            total_needed += plan->partitions[i].new_size - 
                           plan->partitions[i].current_size;
        }
    }
    
    if (total_needed > available) {
        // 空间不足,需要先做分区调整
        return resize_partitions(plan);
    }
    return 0;
}

27.3.2 写入时的校验机制

动态分区写入时,recovery会使用verityavb进行校验。每次写入一个block,都会计算hash并和升级包里的签名对比。

我记得有一次,某个厂商的OTA包里的vendor分区hash值算错了,导致recovery在校验阶段反复失败。最后发现是打包脚本里少算了一个padding block。这种问题排查起来特别痛苦。

27.4 升级完成后的槽位切换

写入完成后,recovery需要更新slot信息,让系统下次从新分区启动。动态分区下的槽位切换比传统分区多了一个步骤:更新metadata。

警告:千万不要在写入过程中断电!metadata更新是原子操作,但如果写入一半断电,super分区里的metadata可能处于不一致状态,导致两个槽位都无法启动。

27.4.1 更新metadata中的槽位信息

// 槽位切换的核心逻辑
int switch_active_slot(int new_slot) {
    struct lp_metadata meta;
    
    // 读取当前metadata
    read_lp_metadata(&meta);
    
    // 更新槽位信息
    meta.active_slot = new_slot;
    meta.slot_suffix = (new_slot == 0) ? "_a" : "_b";
    
    // 写入metadata(带校验)
    write_lp_metadata(&meta);
    
    // 同步到备份区域
    sync_metadata_to_backup(&meta);
    
    return 0;
}

27.5 动态分区操作的整体流程

为了让你更直观地理解整个过程,我画了一张流程图。这张图涵盖了从recovery启动到升级完成的完整链路。

Recovery模式下动态分区OTA升级流程 1. 读取super分区metadata 2. 创建dm-linear映射 3. 挂载动态分区 4. 解析OTA payload 5. 校验空间 & 写入数据 6. verity/avb校验 7. 更新slot metadata 8. 同步备份metadata 9. 重启进入新系统 ⚠️ 关键注意事项: • 步骤5-6之间不要断电,否则super分区metadata可能损坏 • 步骤8的备份同步至关重要,主metadata损坏时靠它恢复

27.6 常见问题与避坑指南

做动态分区OTA升级这几年,我踩过的坑真不少。挑几个典型的说说。

  • metadata损坏:我曾经遇到一次,recovery写metadata时突然断电,结果两个槽位的metadata都乱了。后来我们加了一个“双备份”机制,主metadata和备份metadata互相校验,只要有一个完好就能恢复。
  • 空间碎片化:动态分区用久了,super分区里会产生碎片。比如你删了一个1GB的分区,又新建一个500MB的,中间就会有空隙。我建议每次大版本升级前做一次defrag操作。
  • dm-linear映射冲突:如果recovery异常重启,旧的dm设备可能没清理干净。我习惯在挂载前先执行dmsetup remove_all清理所有映射。

调试小工具:在recovery模式下,可以用lpdump命令查看当前动态分区的布局信息。这个命令会打印出所有分区的名称、大小、偏移量,排查问题时特别有用。

动态分区下的recovery操作,说白了就是“挂载-写入-切换”三步走。但每一步都有它的复杂性。理解了这些底层机制,遇到OTA升级失败时你就能快速定位问题,而不是盲目地重刷系统。


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