Lazy loaded image
🔄双分区(A/B)升级最佳实践与选型对比
Words 5884Read Time 15 min
2026-9-2
2026-9-3
🚀
单分区升级的致命问题是「升级失败 = 变砖」。A/B 双分区把风险从「能不能升级」转移到「升级失败能不能自动回滚」。本文从分区表、bootloader 回滚、签名校验到选型对比,一步步讲清如何把 A/B 升级做到生产标准。

📝 主旨内容

1. 为什么需要 A/B 双分区

单分区升级流程:下载镜像 → 覆盖 rootfs → 重启。一旦镜像损坏、中途断电、签名错误,板子直接变砖,只能拆机重烧。
A/B 的思路:系统盘做两份(A 槽 / B 槽),永远只写「非活动槽」。新版本写进非活动槽后,bootloader 尝试启动它;启动失败就自动回滚到旧槽。核心指标从「升级成功」变成「升级失败不砖」。
维度
单分区升级
A/B 双分区
升级失败后果
变砖,需重烧
自动回滚旧槽
存储成本
1× rootfs
2× rootfs
升级窗口
停机
可后台写非活动槽
实现复杂度
中(bootloader + 状态管理)

2. 三个核心概念

先定义清楚,后面全靠它们:
  • 槽位(Slot):一份完整的系统分区(kernel + rootfs),A 和 B 互为镜像。
  • BCB(Bootloader Control Block):写在独立 misc 分区里的一小块数据,记录「下次启动哪个槽」「当前槽状态」。bootloader 读它决定启动 A 还是 B。
  • 槽位状态机:每个槽有 active(当前运行)/ inactive(备用)/ unbootable(已坏)三种状态。这是设计层面的概念模型,与具体工具无关——无论 RAUC、SWUpdate 还是手写 BCB,都要跟踪这三种状态。区别在谁来落地:RAUC 原生内置(mark-good / mark-bad 管理槽状态);SWUpdate 不自带,需自己用 bootloader 接口(U-Boot 的 bootcount/bootlimit + upgrade_available,或 BCB)实现。

3. 分区表设计(GPT)

用 GPT 而不是 MBR:支持分区名与类型 GUID,A/B 槽的识别更可靠。
要点:
  • A、B 槽大小必须一致,否则升级包无法通用。
  • misc 分区独立于 A/B,升级时永远不动它。
  • data 分区独立,升级不清用户数据。

4. Bootloader 侧:bootcount 回滚

U-Boot 用三个环境变量实现「启动失败自动回滚」:
机制:每次启动 bootcount +1;系统正常启动后由应用层清零;若 bootcount 超过 bootlimit,说明新槽起不来,U-Boot 执行 altbootcmd 切回旧槽。

5. 升级包与签名校验

生产标准的最低要求:升级包必须签名,写入必须校验
  • 完整性:sha256 校验,防传输/写入损坏。
  • 真实性:RSA/ECC 签名,防固件被替换。
  • 可选加固:rootfs 用 dm-verity 做运行时完整性校验。

5.1 SWUpdate 如何实现发布者证书验证

🔐
核心一句话:SWUpdate 不签镜像,签的是更新清单 sw-description;设备端用内置信任锚验证签名和证书链,验证通过才允许解析清单、安装镜像。
信任链:签名认证清单 → 清单认证哈希(sha256)→ 哈希认证镜像内容。签一个文件,整包被认证。
两种签名模式
模式
机制
设备端验证什么
适用
RSA/ECDSA 公钥签名
私钥签 sw-description(openssl dgst -sha256 -sign priv.pem)
内置公钥验签
单发布方、简单场景
CMS(PKCS#7 / X.509 证书链)
私钥 + X.509 证书签(openssl cms -sign)
内置 CA/信任锚:验签名 + 验证书链 + 验证书用途
发布者证书验证,支持 PKI、证书轮换、吊销
关键落地细节
  • 验证时机:先验签 → 再解析清单 → 再逐镜像校验 sha256 → 最后安装,验签失败直接拒绝。
  • 硬性规则:清单里每个镜像必须有 sha256 属性,缺一个整包视为未验证,SWUpdate 直接报错停止。
  • 归档顺序:SWU 是 cpio 包,sw-description.sig 必须紧跟 sw-description。
  • 密钥分布:私钥只在发布方/CI,永不进设备;公钥或 CA 证书固化在设备固件。
签名与验签交互图
SWUpdate 签名与验签流程
SWUpdate 签名与验签流程

5.2 公钥与 CA 认证锚如何烧入固件

🔑
一句话核心:公钥/CA 信任锚是「设备信任谁」的根,必须随固件一起、放在只读/受保护的位置烧进设备;私钥永远只留在 CI 侧。
方式
做法
防篡改能力
rootfs 固化(最常见)
构建固件时把公钥/CA 证书放进固定路径(如 /etc/swupdate/、/etc/ssl/certs/),swupdate.cfg 配 public_key
中——需配合只读分区
独立只读分区
放专门 keys/recovery 分区,与可更新 rootfs 分离,OTA 动不了信任锚
Bootloader 层
信任锚放 U-Boot 环境/启动镜像,SWUpdate 启动时读取
eFuse / 安全芯片 / TPM
公钥哈希或证书固化进 OTP(一次性可编程熔丝),烧写不可逆
最高
关键机制
  • 烧入时机:量产阶段随固件镜像一起烧写(量产工具链/烧录器),不是运行时下发。
  • 为什么必须防篡改:信任锚是验签起点,能改设备上的公钥就能伪造签名通过验证,所以它必须在 OTA 更新范围之外。
  • 信任锚更新:新信任锚只能靠「被旧锚签名的新固件」带入,或走安全通道/物理接触,形成信任根更新链;没有此机制,换 CA = 设备变砖。
  • SWUpdate 落地:RSA/ECDSA 模式设备端放公钥文件(swupdate.cfg 的 public_key);CMS 模式放 CA 证书(信任锚),OpenSSL 把发布者证书链追到根。
🗣️
面试一句话:SWUpdate 用「签名 sw-description + 镜像 sha256 + 设备端内置信任锚」实现发布者认证——发布方用私钥(CMS 场景是 X.509 证书)对更新清单签名,设备端用内置公钥/CA 验证签名和证书链,通过才解析清单;清单里每个镜像的 sha256 被认证,镜像即被认证。私钥永不上设备,信任锚固化在固件里。

5.3 CA 证书实操:格式 / 大小 / 生成 / 注入

四个问题逐个拆,最后给一条完整的落地链路。
格式
CA 证书就是标准的 X.509 证书,两种编码:
格式
说明
适用
PEM(文本)
Base64 + -----BEGIN CERTIFICATE----- 头尾,Linux/OpenSSL 默认
嵌入式 Linux 首选,SWUpdate 设备端用这个
DER(二进制)
X.509 原始二进制
少见,需转换
SWUpdate 官方文档明确:设备端要装的就是一个 .pem 证书文件(如 mycert.cert.pem),OpenSSL 直接处理。PEM 就对了,不用纠结。
字节大小
X.509 证书就 1~2 KB 量级,对嵌入式是零负担:
密钥类型
DER 大小
PEM 大小(≈DER×1.33)
RSA-2048
~700–1000 B
~1–1.3 KB
RSA-4096
~1.5–2 KB
~2–2.7 KB
ECC P-256
~350–500 B
~0.5–0.7 KB
大小主要取决于密钥长度、Subject 字段和扩展数量。设备端存一个 CA 证书,存储成本可以忽略——真正要防的不是「装不下」,而是「装错/被改」。
如何获取
嵌入式 OTA 的标准做法是自建内部 CA(自己当信任根),不是买公网证书。用 OpenSSL 三步:
signing.ext 里必须写(SWUpdate 用 OpenSSL 验 CMS 时默认强制检查):
也可以买公网代码签名证书,但自建 CA 才让你完全掌控信任链和轮换策略——量产 OTA 基本都是这条路。
SWUpdate 如何注入(三层)
① 编译期(Kconfig)——决定「验不验、怎么验」:
  • CONFIG_SIGNED_IMAGES=y:启用签名镜像
  • CONFIG_SIGNATURE_TYPE="CMS":选 CMS 模式(或 RSA)
  • CONFIG_VERIFY_CERTIFICATE=y开启证书链验证——设备端把发布方证书链追到内置 CA。不开的话只验签名、不验证书链,安全性打折。
② 运行期(swupdate.cfg)——告诉 SWUpdate 信任锚在哪:
③ 固件注入——证书怎么进设备:构建固件时把 ca.crt 放进 rootfs 固定路径(如 /etc/swupdate/),随固件镜像在量产烧录时固化。Yocto 侧对应 meta-swupdate 的变量:签名端 SWUPDATE_SIGNING="CMS" + SWUPDATE_CMS_CERT(签名证书)+ SWUPDATE_CMS_KEY(私钥,只在 CI),设备端把公钥证书烤进 rootfs。
🗣️
完整链路一句话:CI 用私钥 + 签名证书签 sw-description(CMS)→ 设备端 SWUpdate 读 swupdate.cfg 的 public_key 指向的内置 CA 证书 → OpenSSL 验签名 + 验证书链到信任锚 → 通过才解析清单、校验镜像 sha256、安装。

5.4 SWUpdate 验签步骤与中间证书处理

常用验签步骤(CMS 模式)
  1. 解析 SWU(cpio)→ 定位 sw-description + sw-description.sig(sig 必须紧跟 description)
  1. 提取 CMS SignedData → 取出签名者证书 → 用其公钥验 sw-description 签名
  1. 证书链验证(CONFIG_VERIFY_CERTIFICATE=y)→ 从签名者向上逐级验 issuer+签名,追到设备内置信任锚
  1. 验签通过 → 才解析 sw-description(拿到镜像列表 + 各自 sha256)
  1. 逐镜像校验 sha256 → 全部通过才安装;任一失败整包拒绝
关键在 ③:验签名只证明「这个签名者确实签了」,验链才证明「这个签名者可信」。只开签名不开链验证(CONFIG_VERIFY_CERTIFICATE),等于只验了完整性没验来源。
中间证书如何处理(重点)
中间证书有两个来源,处理策略完全不同:
来源
机制
风险
CMS 里携带(additional certificates)
发布方打包时塞进 SignedData,OpenSSL 用它搭链
⚠️ 攻击者也能塞自己的中间证书
设备信任库
中间 CA 证书提前烤进设备
安全,设备自己掌控
核心风险点:OpenSSL 对 CMS 里携带的额外证书的规则是「被用于搭链 ≠ 被信任」。它会把 CMS 里的中间证书拿来构造链条,但只要最终能追到设备内置的信任锚,验签就通过——这意味着:攻击者只要拿到一张能被你根 CA 信任的证书(哪怕用途不对、哪怕是内部泄露的),就能塞进恶意包里搭出一条合法链。
应对:CONFIG_CMS_IGNORE_ADDITIONAL_CERTS
SWUpdate 专门为这个加了选项:严格忽略 CMS 里携带的任何证书(除直接签名者本身),强制中间证书只能来自设备信任库。开启后,攻击者塞的中间证书直接被无视,链搭不起来,恶意包必拒。
但注意权衡——这个选项默认是关闭的,因为存在合法场景:设备信任库里的中间 CA 过期了,需要靠升级包顺带携带新的中间证书来续链。
场景
处理策略
生产标准(推荐)
中间 CA 烤进设备信任库 + 开启 CONFIG_CMS_IGNORE_ADDITIONAL_CERTS
需要随包带中间证书
关闭该选项,但必须叠加签名者身份白名单(只认指定签名证书的 CN/序列号),不能只靠「链到根」
签名侧(meta-swupdate)
SWUPDATE_CMS_EXTRA_CERTS 指定要塞进 CMS 的中间证书文件列表
🗣️
一句话总结:中间证书要么提前烤进设备信任库(安全),要么由 CMS 携带(方便但有被利用风险)。生产标准做法:中间证书进信任库 + 开启 CONFIG_CMS_IGNORE_ADDITIONAL_CERTS 严格忽略 CMS 携带证书;若必须随包携带,则必须叠加签名者白名单,绝不能只验证「链到根」。
实践选型:扁平两级 vs CMS 携带中间证书
核心判断:一般实践选扁平两级——只用根证书 + 直接签发的签名证书,不引入中间证书。中间 CA 过期续链这个麻烦,本身就是中间证书带来的——没有中间证书,这个问题就不存在
OTA 签名 PKI 是封闭内部系统(一家公司、一条产品线),不需要企业级 CA 的组织层级。扁平结构长这样:
  • 链只有 2 跳,设备只存根,CMS 只带签名者证书(本来就在 SignedData 里)
  • 可以一直开着严格模式(CONFIG_CMS_IGNORE_ADDITIONAL_CERTS),安全不打折
  • 没有中间 CA,就没有「中间 CA 过期要续链」这个场景
签名证书过期怎么办:根 CA 重新签发一张新的签名证书,设备端根没变,新证书链照样追到同一个根 → 自动被信任,设备零改动
维度
扁平两级(推荐)
CMS 携带中间证书
层级
根 → 签名证书(2 跳)
根 → 中间 → 签名(3 跳+)
设备端存储
只存根
只存根(中间靠 CMS 带)
中间 CA 过期
不存在此问题
要续链,麻烦
签名证书过期
根重签,设备零改动
同左
安全模式
长期开严格模式
必须关严格模式 + 白名单
适用
单产品/单公司 OTA(绝大多数)
多产品线/合规分层
🗣️
一句话总结:实践上选扁平两级——根 CA 直接签签名证书,设备只存根,CMS 只带签名者证书,严格模式常开。签名证书过期靠根重签解决,设备零改动;中间证书是为组织层级付的复杂度,没有层级需求就不要引入。

6. 选型对比:SWUpdate vs RAUC vs 手写 BCB

维度
SWUpdate
RAUC
手写 BCB
定位
通用更新框架
轻量 A/B 更新框架
自己实现
A/B 支持
通过 bootloader 接口
原生 slot 概念
自己设计
签名
RSA/CMS 内置
CMS/X.509 内置
自己实现
差分升级
生态成熟
较弱
自己实现
系统集成
Yocto/Buildroot 好
D-Bus/systemd 好
无依赖
学习成本
中高
低(但坑多)
维护成本
低(社区维护)
选型建议:
  • 生产项目:RAUC(A/B 原生、设计简洁)或 SWUpdate(生态大、handler 灵活)。
  • 学习/面试/简历项目:先手写 BCB 最小闭环(bootcount 回滚),理解机制后再上 SWUpdate 做签名与升级包。
  • 关键判断变量:需要差分升级 → SWUpdate 生态更好;需要 D-Bus/系统集成 → RAUC;资源极度受限或纯学习 → 手写 BCB。

7. 完整升级流程(循序渐进)

每一步的注意点:
  1. 下载:断点续传 + 校验和。
  1. 校验签名:验签失败直接丢弃,不落盘。
  1. 写非活动槽:写完后 fsync,确保落盘。
  1. 校验写入完整性:读回比对 sha256。
  1. 更新 BCB:这是「切换」的唯一动作,必须原子(写一半断电要能恢复)。
  1. 重启:bootloader 读 BCB 启动新槽。
  1. commit:应用层确认启动成功后,新槽标记 active、bootcount 清零。
  1. 回滚:bootcount 超限自动切旧槽,标记新槽 unbootable。

⚠️ 最容易翻车的点:确认成功(commit)的时机

先交代前提:mark-good 是 RAUC 的术语。RAUC 的槽状态机里,新槽启动后若未执行 mark-good,会持续消耗尝试次数,耗尽即被标记 bad 并回滚;执行 mark-good 才把槽确认为 active。SWUpdate 不自带这套状态机的落地实现,A/B 回滚靠 bootloader 接口(典型是 U-Boot 的 bootcount/bootlimit + altbootcmd),对应的「确认成功」动作是:健康检查通过后清零 bootcount、清除 upgrade_available 标记,让 bootloader 不再回滚。两者机制不同,但时机的权衡是通用的
  • 内核启动完成 ≠ 产品已恢复:关键服务可能还没就绪、外设初始化可能失败、数据迁移可能只做了一半。
  • 确认太早:坏版本被确认为可用,回滚机制形同虚设。
  • 确认太晚:短暂掉电 + 看门狗复位会不断消耗尝试次数,把原本可用的新版本退回旧槽。
正确做法:定义项目级健康检查作为唯一判定标准。新系统进入用户态后,依次通过「核心服务就绪 → 外设自检 → 数据迁移完成」三层检查,全部通过才执行确认动作——RAUC 用 mark-good,SWUpdate 清零 bootcount / 清除 upgrade_available。判定标准写死在代码里,不靠人工肉眼确认。

8. 生产级加固清单

  • 断电安全:BCB 更新必须原子(双份备份 + 校验和),写一半断电可恢复。
  • 版本管理:升级包带版本号 + 硬件兼容性声明,拒绝降级或跨硬件升级。
  • 日志:每次升级记录版本、结果、耗时,可追溯。
  • 回滚策略:连续 N 次失败自动回滚并告警。
  • 灰度发布:先小批量设备验证,再全量推送。
  • 可观测:升级状态上报,失败率可监控。

9. 数据与配置的兼容性边界(最容易被忽略)

A/B 冗余的是系统槽,不会自动回滚共享数据库和配置。新版本改了数据格式再回退旧槽,旧程序可能已经读不了新数据——这不是回滚,是新的故障。
  • 向后兼容:新版本写入的数据,旧版本必须能读(字段只增不删、缺省值兜底)。
  • 迁移可逆:升级时做的数据迁移,回滚时要有对应的逆操作。
  • 不可逆时限制升级跨度:数据格式一旦不可逆,就限制「只允许跨 N 个版本升级」,禁止跨大版本直接升级。
为什么企业看重这个:一次失败状态处理错误,可能把软件缺陷变成人工上门、返厂,甚至整批设备召回。数据兼容性是「升级失败不砖」之外的第二个底线。

10. 测试与验收

10.1 正常路径验收

用例
预期
正常升级
新槽启动,commit 成功
版本回退
按策略允许或拒绝
灰度发布
小批量验证后全量,失败率可监控

10.2 故障注入测试(重点)

故障注入测的不是「能不能升级」,而是「升级失败会不会砖」。每条用例都要回答三个问题:故障发生在哪个环节、系统如何检测、系统如何恢复。
故障注入矩阵
故障类型
注入方法
注入点
预期行为
断电
继电器控制电源,指定时刻断电
镜像写入中
恢复后旧槽可启动,新槽未提交
断电
同上
BCB/环境保存中
BCB 原子性生效,旧槽可启动
断电
同上
首次启动后、健康检查前
尝试次数被消耗,未确认成功,可回滚
断电
同上
健康检查通过后
新槽已 commit,保持 active
坏镜像
位翻转 / 截断升级包
下载/写入
校验失败,拒绝写入
坏签名
错误密钥重签 / 篡改签名区
验签
拒绝升级,丢弃并告警
存储故障
坏块模拟 / 写满分区
写入
写入失败,旧槽仍可启动
BCB 损坏
向 misc 分区写垃圾数据
启动前
检测非法 BCB,回退默认槽
环境变量损坏
篡改 bootcount / bootlimit / altbootcmd
启动前
按默认策略处理,不无限重启
健康检查失败
注入关键服务启动失败
首次启动
不 commit,尝试次数消耗,超限回滚
跨硬件/降级
强制安装不兼容包或旧版本
验签/安装
按策略拒绝
注入手段(怎么把故障造出来)
  • 断电:串口继电器控制电源,脚本指定「写第 N 块时断电」这类精确时刻。
  • 坏镜像/坏签名:对升级包做位翻转、截断、换错误密钥重签,比手动删文件更接近真实故障。
  • 存储故障:用坏块模拟(如 device-mapper 映射错误扇区)或写满分区制造写入失败。
  • BCB/环境变量损坏:直接向 misc 分区写垃圾、篡改 U-Boot env,验证「脏数据也能安全启动」。
断言状态,而不是断言命令
以 RAUC 为例,它自己的测试就补过这样一类缺口:早期只检查 mark-good / mark-bad / mark-active 的退出码和输出文字,后续提交增加了操作前后的槽位状态断言。命令说「成功」,不等于系统状态真的改变——这个区别是衡量项目质量的好标尺,对 SWUpdate 同样适用。
  • 只截一张升级成功的终端图,证明的是界面输出
  • 记录槽位、尝试次数、启动原因、回退原因,才能证明你理解更新是一场跨 Bootloader、存储、用户态的事务
  • 每次注入前记录状态快照,注入后对比,用「状态是否达到预期」而非「命令是否报错」判定通过。
自动化:故障注入回归
  • 串口 + 继电器电源,脚本驱动「注入 → 断电 → 重启 → 读状态」循环,全矩阵跑一遍即一次回归。
  • 板端成本低,可纳入 CI;每次升级保留日志,失败率可监控。

🤗 总结归纳

A/B 双分区的本质是把「升级失败」从「变砖事故」变成「可自动恢复的异常」。生产标准 = 分区设计 + bootloader 回滚 + 签名校验 + 原子 BCB + 健康检查定时的确认成功 + 数据兼容性边界 + 完整测试。选型上:生产用 RAUC/SWUpdate,学习用最小闭环手写 BCB。

📎 参考文章

 
上一篇
Linux 设备权限实战:从串口到 GPIO 的免-sudo 之道(i.MX8)
下一篇
全局变量问题的根本解决之道

Comments
Loading...