4、差分升级流程:补丁分发、补丁校验、补丁应用、版本切换
差分升级不是把整个固件塞过去,而是只传变化的部分。这个流程我拆成四个阶段:分发、校验、应用、切换。每一步都有坑,咱们一个一个说。
4.1 补丁分发
分发是第一步,也是最容易被忽视的一步。我见过不少项目,差分算法写得漂亮,结果分发环节掉链子。
分发方式主要有三种:
| 方式 | 适用场景 | 典型问题 |
|---|---|---|
| 网络OTA | 联网设备、批量升级 | 断点续传、弱网环境 |
| 本地存储 | U盘/SD卡升级 | 文件系统损坏、空间不足 |
| 广播/组播 | 局域网批量部署 | 丢包、乱序 |
我的习惯:补丁包一定要带元数据头。至少包含:目标版本号、源版本号、补丁大小、校验算法标识。这样接收端拿到包就能做第一道过滤,版本不对直接丢弃,省得后面白忙活。
分发时还要考虑原子性。说白了,要么完整收到,要么就当没收到。我建议的做法是:先下载到临时分区,下载完成后再做完整性检查,检查通过才移动到正式补丁区。
4.2 补丁校验
校验不是走过场。我曾经在一个智能家居项目上吃过亏——补丁包在传输过程中被截断了,但设备没发现,结果应用时把系统搞崩了。
校验分三个层次:
- 完整性校验:CRC32 或 MD5,检查数据有没有损坏。速度快,适合第一道过滤。
- 一致性校验:SHA256,防止数据被篡改。安全要求高的场景必须上。
- 签名校验: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。应用流程大致如下:
- 读取旧版本固件到内存缓冲区 A
- 读取补丁文件到内存缓冲区 B
- 执行 bspatch 算法,生成新版本到缓冲区 C
- 对缓冲区 C 做完整性校验(可选,但强烈建议)
- 将缓冲区 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 存标志位。最安全,但硬件要支持。
血的教训:有一次我在产品上用了环境变量方式,结果用户误操作擦除了环境变量,设备变砖了。从那以后,我强制要求:启动标志位必须冗余存储——写两份,一份在主分区,一份在备份分区。切换时先写备份,再写主份,确保任何时候至少有一份有效。
版本切换完成后,系统进入新版本运行。但别忘了做启动自检——检查关键服务是否正常、文件系统是否挂载成功、网络是否连通。自检通过后,才算是真正的升级成功。
嗯,差分升级的四个阶段就是这样。每个阶段都有它的技术细节和工程陷阱,但核心思路不变:分发要可靠、校验要严格、应用要稳健、切换要可回滚。把这四点做到位,差分升级系统就稳了。