4、全量升级方案:制作全量升级包、升级包签名与校验、升级流程设计
全量升级,说白了就是把整个 rootfs 完整地换一遍。你想想看,不管设备当前跑的是哪个版本,升级包一刷,整个文件系统全部覆盖。这种方式最直接,也最可靠。我在项目中遇到过好几次,客户要求做差分升级来省流量,结果因为文件系统碎片太多,差分包打出来比全量包还大——最后还是老老实实切回全量方案。
所以别觉得全量升级“笨”,它反而是嵌入式设备里最稳的兜底方案。今天我们就把它拆开来看:包怎么做,怎么签名防篡改,流程怎么设计才不出事。
4.1 制作全量升级包
全量升级包的核心,就是把目标 rootfs 打包成一个可部署的镜像。我习惯用 tar 或 squashfs,具体选哪个看你的文件系统类型。
4.1.1 打包方式对比
| 方式 | 压缩率 | 挂载支持 | 适用场景 |
|---|---|---|---|
| tar.gz | 中等 | 需解压到分区 | 可读写文件系统(ext4/ubifs) |
| squashfs | 高 | 可直接 loop 挂载 | 只读 rootfs,节省空间 |
| cpio.gz | 低 | initramfs 专用 | 早期引导阶段 |
我个人更推荐 squashfs。为什么?因为它打包后可以直接挂载,升级时先挂载新镜像,再通过 switch_root 切过去,省掉解压这一步,速度能快不少。
4.1.2 制作步骤
假设你的目标 rootfs 目录在 /build/rootfs_v2,我们把它打成 squashfs 镜像:
# 安装工具
sudo apt install squashfs-tools
# 制作 squashfs 镜像
mksquashfs /build/rootfs_v2 rootfs_v2.squashfs \
-comp xz -b 131072 -noappend
# 查看镜像信息
unsquashfs -s rootfs_v2.squashfs
这里我用了 xz 压缩,块大小设成 128KB。嗯,这里要注意:块大小越大压缩率越高,但解压时占的内存也越多。如果你的设备只有 64MB RAM,建议把块大小降到 64KB 甚至 32KB。
rm -rf /build/rootfs_v2/var/log/* 就能避免。
4.2 升级包签名与校验
包做好了,但你不能直接丢给设备去刷。万一中间被人篡改了怎么办?我在一个 IoT 项目里就遇到过,有人伪造了升级包,往设备里植入了挖矿程序。从那以后,签名校验成了我方案里的必选项。
4.2.1 签名流程
我们采用“先哈希,后签名”的方式。用 RSA-2048 或 ECDSA,我个人倾向 ECDSA,密钥更短,嵌入式设备验签更快。
# 生成密钥对(服务端)
openssl ecparam -genkey -name prime256v1 -out private.pem
openssl ec -in private.pem -pubout -out public.pem
# 计算升级包哈希并签名
sha256sum rootfs_v2.squashfs > rootfs_v2.sha256
openssl dgst -sha256 -sign private.pem \
-out rootfs_v2.sig rootfs_v2.squashfs
签名后,升级包由三部分组成:镜像文件、哈希文件、签名文件。设备端拿到后,先验签,再比对哈希,最后才刷写。
4.2.2 设备端校验
// 伪代码:设备端验签流程
int verify_update_package(const char *image_path,
const char *sig_path) {
// 1. 读取公钥(烧录在设备中)
EVP_PKEY *pubkey = load_public_key("/etc/update_pub.pem");
// 2. 计算镜像哈希
unsigned char hash[32];
compute_sha256(image_path, hash);
// 3. 验签
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
EVP_DigestVerifyInit(ctx, NULL, EVP_sha256(),
NULL, pubkey);
EVP_DigestVerifyUpdate(ctx, hash, 32);
FILE *sig_file = fopen(sig_path, "rb");
// 读取签名数据...
int ret = EVP_DigestVerifyFinal(ctx, sig_buf, sig_len);
return (ret == 1) ? SUCCESS : FAIL;
}
4.3 升级流程设计
流程设计是整章的核心。一个好的升级流程,要保证“刷到一半断电也不变砖”。我见过太多设备因为升级流程没考虑异常恢复,最后只能返厂。
4.3.1 双分区方案(A/B 系统)
最稳妥的做法,就是双分区。系统有两个 rootfs 分区,一个 active,一个 standby。升级时刷 standby,刷完重启切过去。如果新系统起不来,自动回滚到 active。
4.3.2 升级步骤详解
- 下载与校验:设备从服务器拉取升级包,先验签,再比对哈希。不通过就直接丢弃,不写 flash。
- 刷写 standby 分区:把解压后的 rootfs 写入非活跃分区。这里我建议用
dd或ubiupdatevol,逐块写入并校验。 - 标记切换:写入完成后,更新 bootloader 的启动标记。比如在 uboot 的 env 里把
boot_part改成 B。 - 重启与回滚:设备重启,从新分区启动。如果内核 panic 或 init 进程起不来,看门狗复位后 bootloader 检测到启动失败,自动切回旧分区。
4.3.3 单分区升级(兜底方案)
有些低端设备 flash 太小,做不了双分区。这时候只能用单分区升级,但风险更高。我的做法是:
- 先把新 rootfs 写入临时区域(比如剩余的空闲块)。
- 写入完成后,用
memcmp逐块比对,确保数据完整。 - 比对通过后,再覆盖旧 rootfs。
- 如果覆盖过程中断电,设备只能进 recovery 模式,从网络或 SD 卡重新刷写。
4.4 避坑指南
最后,分享几个我踩过的坑:
- 签名算法别用 MD5:我曾经在一个项目里看到有人用 MD5 签名,理由是“设备性能太差”。结果呢?被爆出签名碰撞,全线设备需要召回。用 SHA256 或更高,性能不够就硬件加速。
- 升级包版本号要严格递增:设备端要检查新版本号必须大于当前版本。否则运维误操作,把旧包推上去,系统就“降级”了,可能引发兼容性问题。
- 升级过程中保留日志:把升级的每一步都写到
/var/log/update.log。如果升级失败,这些日志是排查问题的唯一线索。我见过太多设备变砖后,连日志都没有,只能盲猜。