linux 差分升级简介
好问题 👍
你已经知道了“整包升级”,现在想搞 差分升级。我来给你系统讲解下:
一、什么是差分升级?
👉 定义:
差分升级(Delta Update / Binary Patch)就是:
不传完整固件,而是只传“旧版本和新版本之间的差异”,目标机利用差分包 + 旧版本数据,重构出新版本。
举个形象的例子:
- 整包升级:直接把 整本书的新版本寄给你。
- 差分升级:只寄给你一份 “勘误表”,你在旧版书上改一改,就得到了新版。
二、为什么要差分升级?
- 节省带宽:
比如 rootfs 有 1GB,但新版本只改了几十 MB 文件。差分包可能只有 20~50 MB。 - 节省升级时间:
包小 → 下载快 → 升级过程短。 - 适合远程 OTA / 弱网环境。
缺点:
- 生成与应用差分需要额外计算,升级流程比整包复杂。
- 必须保证“基准版本”一致:设备必须正好是差分包要求的版本,否则 patch 失败(可能直接变砖)。
三、差分升级的实现方式
差分升级分为两大类:
1. 文件级差分
- 针对文件系统内容,比较 文件 A(old) vs A(new),生成差分(如
rsync
/bsdiff
/xdelta
)。 - 应用时逐文件 patch。
- 特点:更灵活,适合只更新应用层(例如 /usr/bin/xxx 程序)。
工具:
- rsync 协议(增量同步)
- bsdiff/bspatch(对单文件做二进制差分)
- xdelta(对任意文件做差分,效率比 bsdiff 高)
适用:应用升级 / data 分区升级。
2. 镜像级差分
-
针对整块分区镜像(如 rootfs.img、boot.img),计算二进制差分。
-
生成差分包 → 设备上应用 patch → 得到完整新镜像 → 写到 rootfs_b。
-
常用工具:
imgdiff
(Android OTA 用的工具,对 squashfs/ext4 镜像做差分)xdelta3
(通用二进制差分)
适用:系统级升级(rootfs/boot 分区)。
四、在 RK3588S + Buildroot 的场景怎么做?
方案 1:整包升级(最简单)
- 制作新
rootfs.img
,通过 USB/OTA 整包替换。 - 缺点:包大。
方案 2:镜像差分升级(推荐)
-
PC 上用
xdelta3
对旧 rootfs.img 和新 rootfs.img 生成差分包:xdelta3 -e -s rootfs_old.img rootfs_new.img rootfs.patch
-
OTA/USB 传输
rootfs.patch
(远小于整包)。 -
设备端执行:
xdelta3 -d -s rootfs_old.img rootfs.patch rootfs_new.img
-
把
rootfs_new.img
写入 rootfs_b 分区,更新 U-Boot 环境变量,重启。
方案 3:文件级差分(适合应用层)
- 如果只是应用更新,可以用
bsdiff
生成app_old → app_new
的 patch,OTA 下发 patch,设备端用bspatch
更新。 - 优点:更细粒度,速度快。
- 缺点:只能解决应用层,无法覆盖整个 rootfs 升级。
五、差分升级的注意事项
-
基准版本一致
- 必须知道设备当前的 rootfs 版本号(比如 hash/版本号),否则 patch 会失败。
- 通常要在升级包里写明“本 patch 适用于 v1.2 → v1.3”。
-
校验
- 升级前后都要算 hash(SHA256/MD5)校验,保证新 rootfs.img 正确无误。
-
回滚机制
- 差分升级一旦失败,设备要能从旧的 rootfs 分区启动,避免变砖。
-
性能问题
- 生成差分包 → 在 PC 上做,问题不大。
- 应用差分包 → 设备端 CPU/IO 开销要考虑。RK3588S 性能较强,完全可以。
六、落地建议(你的 Buildroot + USB 升级场景)
-
如果你们是研发/量产阶段:
→ 推荐整包 USB 线刷(省心,包大无所谓)。 -
如果你们是最终产品 OTA/远程升级:
→ 推荐 镜像差分升级(用 xdelta3 或 imgdiff),节省流量。 -
如果你们的需求只是应用程序频繁更新:
→ 推荐 应用层文件级差分,直接更新 /data 下的二进制或资源。
要不要我帮你出一份 基于 Buildroot + RK3588S 的差分升级 Demo(包含 PC 端生成 patch + 设备端应用 patch + USB 传输 + A/B 分区切换的完整流程脚本)?
太好了 — 我把 “Buildroot + RK3588S,走 USB 通道的差分升级(镜像级)” 的 可复现实战 Demo 做成一套你能直接跑的说明和脚本。下面包含:
- PC 端:如何 生成差分包(xdelta3) + 签名 + 打包成传输用的
.tar
(或.patch
)。(thewiki.moe, Cool Fish In Glacier) - 设备端:如何 接收(USB Mass Storage / FunctionFS)、验证、合成新镜像、写入非活动槽(A/B)、更新 U-Boot 环境变量并重启(含失败回滚保护)。(opensource.rock-chips.com)
- 必要的安全/校验步骤(hash 与签名)。
- 注意事项与调试小技巧。
如果你想要我把某部分改成“文件级差分(bsdiff)”或改用imgdiff
的 Android 风格差分,也可以马上给你替换版本。(Android Git Repositories, XDA论坛)
1)设计前提(约定)
-
设备分区(示例,实际请用你设备的分区表):
boot_a
/dev/mmcblk0p5
(当前)boot_b
/dev/mmcblk0p6
(备用)rootfs_a
/dev/mmcblk0p7
(当前)rootfs_b
/dev/mmcblk0p8
(备用)
-
设备当前运行 slot 由 U-Boot 环境变量
boot_slot
标识(a
或b
)。设备上有fw_setenv/fw_printenv
(mtd-utils)可设置 U-Boot env。 -
差分采用 xdelta3(镜像级差分),因通用且速度/体积较好。也可以换
imgdiff
。(thewiki.moe, Cool Fish In Glacier)
2)PC 端:生成差分包(示例脚本)
文件:gen_rootfs_patch.sh
(在 PC 上运行,假设已有 rootfs_old.img
、rootfs_new.img
)
#!/bin/bash
set -eOLD_IMG="$1"
NEW_IMG="$2"
OUT_DIR="$3" # e.g. ./out
PATCH_NAME="rootfs_${OLD_VER}_to_${NEW_VER}.xdelta"if [ -z "$OLD_IMG" ] || [ -z "$NEW_IMG" ] || [ -z "$OUT_DIR" ]; thenecho "Usage: $0 <rootfs_old.img> <rootfs_new.img> <out_dir>"exit 1
fimkdir -p "$OUT_DIR"
# compute version ids (sha256 short)
OLD_VER=$(sha256sum "$OLD_IMG" | awk '{print $1}' | cut -c1-12)
NEW_VER=$(sha256sum "$NEW_IMG" | awk '{print $1}' | cut -c1-12)PATCH_FILE="$OUT_DIR/rootfs_${OLD_VER}_to_${NEW_VER}.xdelta"
META_FILE="$OUT_DIR/metadata.txt"
BUNDLE="$OUT_DIR/rootfs_patch_${OLD_VER}_to_${NEW_VER}.tar"echo "Generating xdelta3 patch..."
xdelta3 -e -s "$OLD_IMG" "$NEW_IMG" "$PATCH_FILE"echo "Writing metadata..."
cat > "$META_FILE" <<EOF
old_hash=$(sha256sum "$OLD_IMG" | awk '{print $1}')
new_hash=$(sha256sum "$NEW_IMG" | awk '{print $1}')
old_ver=$OLD_VER
new_ver=$NEW_VER
tool=xdelta3
EOF# Sign the patch (optional but recommended)
echo "Signing patch with openssl (use your private key private.pem)"
openssl dgst -sha256 -sign private.pem -out "$PATCH_FILE.sig" "$PATCH_FILE"echo "Bundling..."
tar -cvf "$BUNDLE" -C "$OUT_DIR" "$(basename "$PATCH_FILE")" "$(basename "$PATCH_FILE").sig" "$(basename "$META_FILE")"echo "Done: $BUNDLE"
说明:
xdelta3 -e -s old new patch
生成 patch。应用用xdelta3 -d -s old patch new
。(thewiki.moe, Reddit)- 我用 SHA256 做版本识别(在 metadata.txt 内),设备端会比对
old_hash
与本地实际镜像一致才应用。
3)PC → 设备 的 USB 传输方式(两种选项)
A)USB Mass Storage(虚拟 U 盘):把 .tar
放到设备暴露的 U 盘里(用户拖拽),设备端守护进程检测到文件并启动升级。优点简单。
B)USB FunctionFS + 自定义协议:PC 端写一个小工具(libusb 或 pyusb)把文件上传,设备端通过 FunctionFS 收包(更自动/可加速)。
你之前有兴趣做 FunctionFS 文件传输,这一步可以直接沿用。两者设备端接收后的后续处理相同。
4)设备端:接收后自动化脚本(核心部分)
文件:apply_rootfs_patch.sh
(设备 /usr/local/bin
,需可执行)
#!/bin/bash
set -eWORK_DIR="/tmp/upgrade"
BUNDLE="$1" # /mnt/usb/rootfs_patch_abc.tar
OLD_IMG="/tmp/rootfs_old.img"
NEW_IMG="/tmp/rootfs_new.img"
PATCH="/tmp/rootfs.patch"
META="/tmp/metadata.txt"
SIG="/tmp/rootfs.patch.sig"mkdir -p "$WORK_DIR"
cd "$WORK_DIR"# 1. extract bundle
tar -xvf "$BUNDLE"# 2. read metadata
source metadata.txt # defines old_hash/new_hash# 3. locate current active slot and extract its image to compare
CURRENT_SLOT=$(fw_printenv -n boot_slot)
if [ "$CURRENT_SLOT" = "a" ]; thenCURRENT_ROOT="/dev/mmcblk0p7" # rootfs_aINACTIVE_ROOT="/dev/mmcblk0p8" # rootfs_b
elseCURRENT_ROOT="/dev/mmcblk0p8"INACTIVE_ROOT="/dev/mmcblk0p7"
fi# Optional: dump current partition to file for patch base (may need space)
echo "Dumping current rootfs for patch base (this may take time)..."
dd if="$CURRENT_ROOT" of="$OLD_IMG" bs=4M conv=sync status=progress# 4. verify old hash matches
CALC_OLD_HASH=$(sha256sum "$OLD_IMG" | awk '{print $1}')
if [ "$CALC_OLD_HASH" != "$old_hash" ]; thenecho "Base image mismatch: expected $old_hash got $CALC_OLD_HASH"exit 2
fi# 5. verify signature (optional)
openssl dgst -sha256 -verify public.pem -signature "$SIG" "$PATCH" || { echo "signature invalid"; exit 3; }# 6. apply xdelta3 patch to produce new image
xdelta3 -d -s "$OLD_IMG" "$PATCH" "$NEW_IMG"# 7. verify new hash
CALC_NEW_HASH=$(sha256sum "$NEW_IMG" | awk '{print $1}')
if [ "$CALC_NEW_HASH" != "$new_hash" ]; thenecho "New image hash mismatch"exit 4
fi# 8. write new image to inactive partition
echo "Writing new image to inactive partition $INACTIVE_ROOT"
# Ideally use block-level tools; dd with sync is used here
dd if="$NEW_IMG" of="$INACTIVE_ROOT" bs=4M conv=fsync status=progress# 9. set U-Boot env to boot inactive slot next
if [ "$CURRENT_SLOT" = "a" ]; thenfw_setenv boot_slot b
elsefw_setenv boot_slot a
fi
fw_setenv upgrade_available 1
fw_setenv bootcount 0# 10. reboot
echo "Rebooting to new slot..."
reboot
说明要点:
- 先
dd
把当前分区导出为rootfs_old.img
(作为 xdelta 的基线)。如果你的系统保存中了原始镜像(比如/opt/images/rootfs_v1.img
),就可以用它而不用再 dd。这样更快、也省空间。 - 一定要做
old_hash
检查,避免把错的基线去 patch 导致产出垃圾镜像。 - 写入 inactive partition 后用
fw_setenv
切槽并设置upgrade_available
标志,设备下一次启动从新槽引导并做健康检测。U-Boot 的bootcount/bootlimit
可配合实现失败回滚。(opensource.rock-chips.com)
5)U-Boot & 回滚策略(设备上)
U-Boot env 建议字段(示例):
boot_slot=a
upgrade_available=0
bootcount=0
bootlimit=3
引导流程(简述):
- U-Boot 读取
boot_slot
,尝试从对应 slot 启动内核/根。 - 内核 / init 程序启动后负责做“健康自检”(比如关键服务能启动、应用返回 OK)并在成功时运行
fw_setenv upgrade_available 0; fw_setenv bootcount 0
。 - 如果内核没确认成功,下一次 U-Boot 会增加
bootcount
;当bootcount>=bootlimit
时,U-Boot 或启动脚本会把boot_slot
切回另一槽,清bootcount
并启动旧系统(回滚)。
该机制在很多嵌入式 OTA 系统常用(RAUC 也实现类似行为)。(RAUC)
6)安全性(必须做)
- 签名:补丁包签名(示例用 openssl),设备端用公钥验证。
- 双重 hash 校验:校验 old_hash(防误用)、校验 new_hash(防传输损坏)。
- 写入前检查空间:确保设备有足够临时空间(xdelta 需要输出完整
new.img
,可能临时占用与镜像相近空间)。 - 保障救砖通道:始终保留 loader / maskrom 可刷写接口(用于无法回滚/救砖时)。(opensource.rock-chips.com, wiki.t-firefly.com)
7)性能与优化建议
- 避免全量 dd 导出:如果设备上保存了“当前镜像的原始副本”,用它当基线会快很多。
- 分块差分:若镜像很大,可把镜像分成固定大小 chunk(如 64MB),对每个 chunk 做 xdelta 差分并只更新变更的 chunk(复杂度高但流量更低)。Android 的
imgdiff
思路类似。(Cool Fish In Glacier, Android Git Repositories) - 并行写入:RK3588S 性能强,可在写入阶段尽量使用
bs=4M
并conv=fsync
确保写入完整性。
8)调试流程(常见问题与排查)
old_hash
不匹配 → 检查你对基线的定位:是否设备已被改动?是否你拿到的基线版本不是设备当前版本?xdelta
解包失败 → 确认xdelta3
版本一致(不同版本产生的 patch 兼容性问题极少但可能)并确保没有传输损坏(用 sha256sum)。(thewiki.moe)- 写入后无法启动 → 先不要擦除旧槽,使用 loader 模式(rkdeveloptool)线刷来救砖;或使用 UART 查看内核日志定位问题。(opensource.rock-chips.com, Android Git Repositories)
9)如果你要我把 Demo 做成可直接运行的“整套工程”
我可以直接给你(一次性交付):
gen_rootfs_patch.sh
(PC 端生成与签名脚本)usb_send.sh
(PC 端:把 bundle 复制到设备的 MassStorage 或通过 libusb 上传的示例)apply_rootfs_patch.sh
(设备端自动化脚本,上面已给出核心)u-boot env
推荐模板与fw_setenv
使用示例。- 一个小 README,包含测试步骤(如何在开发板上做 end-to-end 测试)和常见失败恢复方法(如何用 rkdeveloptool 线刷回出厂镜像)。
你要不要我现在就把这些脚本整理成一个压缩包(把脚本全部写好并显示在这里),还是先只要 apply_rootfs_patch.sh
的可改进版本(比如支持 chunked patch / 进度显示)?另外告诉我你偏好哪种 USB 传输方式:Mass Storage(拖拽) 还是 FunctionFS(自动化 + 自写 PC 工具)?