4、差分升级流程:补丁分发、补丁校验、补丁应用、版本切换

差分升级不是把整个固件塞过去,而是只传变化的部分。这个流程我拆成四个阶段:分发、校验、应用、切换。每一步都有坑,咱们一个一个说。

差分升级四阶段流程 补丁分发 网络/本地/广播 补丁校验 CRC/SHA/签名 补丁应用 bsdiff/bspatch 版本切换 A/B分区/标志位 ⚠ 关键检查点 • 补丁完整性校验 • 版本兼容性检查 • 回滚标志位备份

4.1 补丁分发

分发是第一步,也是最容易被忽视的一步。我见过不少项目,差分算法写得漂亮,结果分发环节掉链子。

分发方式主要有三种:

方式适用场景典型问题
网络OTA联网设备、批量升级断点续传、弱网环境
本地存储U盘/SD卡升级文件系统损坏、空间不足
广播/组播局域网批量部署丢包、乱序
我的习惯:补丁包一定要带元数据头。至少包含:目标版本号、源版本号、补丁大小、校验算法标识。这样接收端拿到包就能做第一道过滤,版本不对直接丢弃,省得后面白忙活。

分发时还要考虑原子性。说白了,要么完整收到,要么就当没收到。我建议的做法是:先下载到临时分区,下载完成后再做完整性检查,检查通过才移动到正式补丁区。

4.2 补丁校验

校验不是走过场。我曾经在一个智能家居项目上吃过亏——补丁包在传输过程中被截断了,但设备没发现,结果应用时把系统搞崩了。

校验分三个层次:

  1. 完整性校验:CRC32 或 MD5,检查数据有没有损坏。速度快,适合第一道过滤。
  2. 一致性校验:SHA256,防止数据被篡改。安全要求高的场景必须上。
  3. 签名校验:RSA/ECDSA 签名验证,确保补丁来自可信源。这个在商业产品里是标配。

校验流程示例(伪代码):

// 1. 读取补丁包头
patch_header = read_header(patch_file)
if patch_header.src_version != current_version:
    return ERROR_VERSION_MISMATCH

// 2. 计算哈希
computed_hash = sha256(patch_file.data)
if computed_hash != patch_header.hash:
    return ERROR_HASH_MISMATCH

// 3. 验证签名
if verify_signature(patch_file.signature, patch_file.data, public_key) == false:
    return ERROR_SIGNATURE_INVALID

// 4. 通过
return OK
注意:校验失败后不要直接丢弃补丁。我建议把失败信息写入日志分区,同时保留原始补丁文件,方便后续排查。直接删掉的话,出了问题你连现场都找不到。

4.3 补丁应用

补丁应用是核心环节。差分算法生成的是二进制差异数据,应用过程就是把差异合并到旧版本上,生成新版本。

常用的差分工具是 bsdiff 和 bspatch。应用流程大致如下:

  1. 读取旧版本固件到内存缓冲区 A
  2. 读取补丁文件到内存缓冲区 B
  3. 执行 bspatch 算法,生成新版本到缓冲区 C
  4. 对缓冲区 C 做完整性校验(可选,但强烈建议)
  5. 将缓冲区 C 写入目标分区
避坑指南:我曾经遇到一个情况——设备内存只有 256KB,但新固件有 512KB。差分补丁虽然小,但应用时需要同时加载旧版本和补丁,内存根本不够。解决方案是分块应用:把旧固件分成 32KB 一块,逐块读取、逐块打补丁、逐块写入。代价是速度慢一些,但能跑起来。

分块应用的伪代码:

#define BLOCK_SIZE 32768  // 32KB

int apply_patch_blockwise(FILE *old_fp, FILE *patch_fp, FILE *new_fp) {
    uint8_t old_buf[BLOCK_SIZE];
    uint8_t new_buf[BLOCK_SIZE];
    int block_count = 0;

    while (!feof(old_fp)) {
        // 读一块旧数据
        int read_len = fread(old_buf, 1, BLOCK_SIZE, old_fp);
        if (read_len == 0) break;

        // 读对应的补丁块
        uint8_t patch_block[PATCH_BLOCK_SIZE];
        int patch_len = read_patch_block(patch_fp, patch_block, block_count);

        // 应用差分
        bspatch_block(old_buf, read_len, patch_block, patch_len, new_buf);

        // 写新数据
        fwrite(new_buf, 1, read_len, new_fp);

        block_count++;
    }
    return 0;
}

4.4 版本切换

补丁应用完成后,新版本已经在目标分区了。但别急着重启——版本切换需要做几件事:

步骤操作说明
1设置启动标志标记下次启动使用新分区
2备份回滚信息保存旧版本分区位置和校验值
3同步文件系统确保所有数据落盘
4执行重启系统从新分区启动

版本切换的核心原则:永远保留一个可回滚的版本。我习惯用 A/B 分区方案——A 分区跑当前版本,B 分区写新版本。切换时只改启动标志位,不改分区内容。这样就算新版本起不来,还能切回 A 分区。

启动标志位怎么设计?我见过三种做法:

  • 环境变量:uboot 或 bootloader 的环境变量里写 boot_partition=A/B。简单,但容易被误改。
  • 专用分区:划一个 4KB 的 metadata 分区,专门存启动信息。安全,但多占一个分区。
  • eMMC 的 RPMB:用 Replay Protected Memory Block 存标志位。最安全,但硬件要支持。
血的教训:有一次我在产品上用了环境变量方式,结果用户误操作擦除了环境变量,设备变砖了。从那以后,我强制要求:启动标志位必须冗余存储——写两份,一份在主分区,一份在备份分区。切换时先写备份,再写主份,确保任何时候至少有一份有效。

版本切换完成后,系统进入新版本运行。但别忘了做启动自检——检查关键服务是否正常、文件系统是否挂载成功、网络是否连通。自检通过后,才算是真正的升级成功。

嗯,差分升级的四个阶段就是这样。每个阶段都有它的技术细节和工程陷阱,但核心思路不变:分发要可靠、校验要严格、应用要稳健、切换要可回滚。把这四点做到位,差分升级系统就稳了。