📺 B站:博主个人介绍
📘 博主书籍-京东购买链接*:Yocto项目实战教程
📘 加博主微信,进技术交流群: jerrydev
基于 Yocto 与 SWUpdate 的双端离线升级实战:设备端 Web 服务与控制端程序完整落地
在很多嵌入式产品中,升级并不是“把一个镜像拷过去再执行命令”这么简单。真正进入量产阶段之后,工程上要同时面对四个问题:第一,升级包必须可校验、可追踪;第二,升级过程必须能抗断电、抗中断;第三,升级失败之后要能回退,不能把设备刷成“半死不活”的状态;第四,控制升级的那一端和真正执行安装的那一端,往往并不是同一台设备。SWUpdate 的定位恰恰不是一个“只会解包写盘”的命令行工具,而是一个可从本地介质或网络执行更新、并且能够和不同安装处理器、不同启动链路配合的升级框架;它既支持单拷贝,也支持双拷贝,还支持内置 WebServer 进行远程上传。![([Sbabić][1])](https://i-blog.csdnimg.cn/direct/9b7ed9290e5f4d459d4b5c752f5415aa.png)
如果把视角再拉高一点,会发现这类方案在市场上的成熟做法其实已经非常收敛。SWUpdate 的最佳实践强调,升级成功不是“文件写完了”,而是“更新代理写入成功、设备完成重启、Bootloader 能切到新版本、新版本完成自检并确认事务结束”;Mender 在设备支持文档中也明确要求,想做健壮的系统升级,Bootloader 必须参与分区选择和回滚逻辑;RAUC 则把“写入 inactive slot、由系统在启动后确认状态”作为一套明确的 slot 机制来实现。换句话说,行业里真正成熟的方案,不管表面上叫 SWUpdate、Mender 还是 RAUC,底层都在遵守同一套工程原则:写备用槽、让 Bootloader 参与事务、把控制面和安装面分离。 ([Sbabić][2])
本文就按这个成熟路径,给出一套完整的双端实战方案。为了避免把讨论绑死在某一个具体产品形态上,本文统一采用两个角色来描述:一端叫设备端,也就是被升级端;另一端叫控制端,也就是用户操作端、升级发起端。设备端运行 SWUpdate,并打开内置 WebServer;控制端不直接“安装镜像”,而是负责上传 .swu、监听进度、展示结果、必要时触发重启。这个方案适合局域网离线升级、主从架构升级、便携式主控终端控制从属节点升级,也适合未来继续扩展成一对多的批量升级架构。SWUpdate 官方文档里甚至已经给出了 master / slaves 的 forwarder 模式:主设备作为唯一对外入口,把 SWU 转发给隐藏在局域网内的从设备。([Sbabić][3])
一、为什么这里不直接把 swupdate-client 装到控制端
很多人第一次做双端方案时,都会自然地想到一句话:设备端有 SWUpdate,那控制端是不是装一个 swupdate-client 就行?这在单机方案里有时成立,但在双机网络方案里通常不是最佳答案。原因很简单:SWUpdate 官方定义的外部程序接口,本质上是通过 Unix Domain Socket 和本机运行中的 SWUpdate 通信。文档明确写到,控制 socket 的默认路径在 /tmp/sockinstctrl 一类的位置,客户端先发 REQ_INSTALL,收到 ACK 之后再把整个镜像流送给 SWUpdate。也就是说,swupdate-client 的天然定位是“本机前端程序和本机 SWUpdate 守护进程之间的 IPC 工具”,而不是“跨网络控制另一台设备的远程升级客户端”。([Sbabić][4])
真正适合双端网络方案的,是 SWUpdate 内置 WebServer 这条路径。官方文档给出的接口很明确:设备端可以开放 POST /upload 来接收 .swu,开放 POST /restart 在配置允许时执行重启,并通过 WebSocket 向外发送 JSON 事件,事件类型包括 status、source、info、message、step,状态值则包括 IDLE、START、RUN、SUCCESS、FAILURE、DONE。这意味着控制端完全可以自己实现一个轻量级控制程序,专门负责上传包、观察升级过程和做用户交互,而不必强行把 swupdate-client 远程化。([Sbabić][4])
因此,本文的核心判断只有一句话:设备端负责安装,控制端负责控制。 这不是文字游戏,而是把“数据平面”和“控制平面”从一开始就拆开。设备端要保证包能校验、能落盘、能写 Bootloader 状态;控制端要保证用户看到的是完整的升级流程、错误原因和最终结果。这样做,系统结构会干净很多,后面无论你把控制端做成 CLI、Qt 前端、Web 前端还是后台服务,都不会改动设备端的核心安装链路。([Sbabić][4])
二、整套方案的总体结构
本文采用如下架构:
控制端
├─ 自定义升级控制程序
├─ 选择 .swu
├─ 上传到设备端
├─ 监听 WebSocket 进度
└─ 展示成功 / 失败 / 日志
│ 局域网 HTTP / WebSocket
▼
设备端
├─ SWUpdate 守护进程
├─ 内置 WebServer
├─ /dev/standby 指向当前备用槽
├─ sw-description 描述安装行为
├─ Bootloader 环境变量切换启动目标
└─ 重启后由新系统确认事务完成
这套方案的关键,不只是“能升级”,而是“升级语义正确”。SWUpdate 的最佳实践明确建议,在双拷贝模式下要么通过 selection/mode 指定备用槽,要么在启动时建立类似 /dev/standby 这样的链接,让 SWUpdate 永远对“当前备用副本”写入,而不是误写正在运行的活动槽。文档还明确指出,双拷贝 A/B 的 stand-by copy 要在运行前就识别好;如果不想依赖启动参数,也可以在运行时通过脚本或 Lua 去识别当前 rootfs。本文为了让工程尽快落地,采用的是更稳定、也更容易在远程 Web 模式下使用的办法:在设备端启动时建立 /dev/standby 符号链接,让 SWUpdate 的更新目标始终固定成 /dev/standby。 ([Sbabić][2])
把这个设计放到真实项目里,它的好处非常明显。控制端不需要知道设备此刻到底跑在 A 还是 B,也不需要在上传前给 SWUpdate 额外传 mode;它只管把同一个 .swu 上传过去。真正的槽位判断发生在设备端启动阶段,一次判断、全程复用。这比“控制端先查询活动槽,再决定发哪一个专用包”更优雅,也比“每次远程启动 SWUpdate 时临时拼 -e 参数”更适合守护进程常驻的产品。这个思路和 RAUC 的 inactive slot 选择机制在工程上是高度一致的:安装面应该有一套稳定的“写备用槽”机制,而不是把槽位逻辑散落在控制端 UI 里。 ([RAUC][5])
三、设备端:在 Yocto 中集成 SWUpdate WebServer
1. 加入 meta-swupdate
SWUpdate 官方推荐在 Yocto 体系里通过 meta-swupdate 集成。官方文档写得很直接:meta-swupdate 负责交叉编译 SWUpdate 应用,也负责生成复合 SWU 镜像;把它加进 bblayers.conf 时,还需要同时加入 meta-oe。文档还提到,它提供了一个 swupdate-image 用来生成 initrd/rescue 系统,但那是“救援系统”方向的用法;如果你做的是常规双端在线升级,更常见的做法是把 SWUpdate 运行时直接放进业务镜像。([Sbabić][6])
一个典型的 bblayers.conf 片段如下:
BBLAYERS += " \
${TOPDIR}/../poky/meta \
${TOPDIR}/../poky/meta-poky \
${TOPDIR}/../poky/meta-yocto-bsp \
${TOPDIR}/../meta-openembedded/meta-oe \
${TOPDIR}/../meta-openembedded/meta-python \
${TOPDIR}/../meta-swupdate \
${TOPDIR}/../meta-your-product \
"
这里额外把 meta-python 一并放进来,是因为后文会在控制端镜像中放一个 Python 写的升级控制程序;如果你的控制端不是 Python,也可以换成自己的技术栈。这个取舍属于项目实现,不影响 SWUpdate 的基础接入。
2. 把 SWUpdate 运行时直接装进设备端镜像
meta-swupdate 的 recipe 会把不同资源拆成多个包。GitHub 中的 swupdate.inc 可以看到,它会安装 swupdate.service、swupdate.socket、swupdate.sh,还会在启用 Mongoose 时安装默认的 10-mongoose-args 配置;与此同时,/www 目录中的 Web 资源也会被打包进去。默认的 Mongoose 参数文件内容是:
SWUPDATE_WEBSERVER_ARGS="-r /www ${SWUPDATE_MONGOOSE_EXTRA_ARGS:--p 8080}"
也就是说,只要编译时启用了 Mongoose 并把对应包装进镜像,默认文档根目录就是 /www,默认监听端口就是 8080。([GitHub][7])
因此,设备端镜像最直接的做法是在 image recipe 或 local.conf 中加入:
IMAGE_INSTALL:append = " swupdate swupdate-www"
这样,设备端启动后就具备三个核心能力:能运行 swupdate,能由 systemd 拉起守护进程,能提供 Web 界面和 Web API。swupdate-www 是否必须,取决于你是否需要默认 Web 资源;如果你只打算用 REST 和 WebSocket 而不打开 HTML 页面,可以按你的发行版做更细的裁剪,但在前期原型验证阶段,直接一起装进去通常更高效。([GitHub][7])
3. 在自己的 layer 中写 bbappend,不要直接改官方层
SWUpdate 最佳实践明确建议,在 OE/Yocto 中通过 swupdate_%.bbappend 加自己的 defconfig,而不是直接去修改官方层里的 recipe。官方还专门建议用 bitbake -c menuconfig swupdate 先检查配置,确认安全选项、Bootloader 接口和需要的 handlers。([Sbabić][2])
你的自定义层结构可以这样组织:
meta-your-product/
└── recipes-support/
└── swupdate/
├── swupdate_%.bbappend
└── files/
└── defconfig
swupdate_%.bbappend 先写成最小版本:
FILESEXTRAPATHS:prepend := "${THISDIR}/files:"
SRC_URI += "file://defconfig"
然后执行:
bitbake -c menuconfig swupdate
把你需要的配置导出成自己的 defconfig。官方最佳实践特别指出,默认配置对大多数项目并不合适,应该根据项目关闭不需要的接口和 handler,只保留真正需要的部分,同时强烈建议启用 Lua 以扩展运行时行为。([Sbabić][2])
4. defconfig 最少要关注哪些能力
由于不同版本的 SWUpdate 配置符号可能略有差异,最稳妥的方式永远是以你当前源码树中的 menuconfig 为准。不过从官方文档和集成行为出发,设备端至少要确认五类能力:WebServer/Mongoose、raw 镜像写入、Bootloader 环境处理、哈希/签名验证,以及你实际要用到的压缩或脚本能力。SWUpdate 最佳实践还特别强调:接口和 handler 不要贪多,但也不要忘了未来可能用到的能力。([Sbabić][2])
下面给一个“思路型”的配置片段,表达的是方向而不是某个版本下逐字不差的最终答案:
# 说明:以下仅示意需要打开的能力类别,具体符号名称以 menuconfig 为准
CONFIG_WEBSERVER=y
CONFIG_MONGOOSE=y
CONFIG_RAW=y
CONFIG_BOOTLOADERHANDLER=y
CONFIG_SIGNED_IMAGES=y
CONFIG_HASH_VERIFY=y
CONFIG_LUA=y
为什么要把 Lua 放进去?因为官方明确建议在复杂运行逻辑里优先使用 Lua,而不是 Shell。原因一是安全性更好,二是运行时不需要 fork/exec 外部 shell,资源占用和可控性都更优。([Sbabić][2])
四、设备端:让 /dev/standby 永远指向备用槽
前面已经说过,双端 Web 上传模式下,最怕的是控制端还没搞清楚当前槽位,设备端就把镜像写错地方。因此,本文采用 /dev/standby 方案。SWUpdate 最佳实践明确把它列为 A/B 方案的一种典型实现方式。([Sbabić][2])
1. 启动脚本
下面这个脚本的作用是在系统启动时识别当前根文件系统所在分区,并把 /dev/standby 链接到另一个分区。本文假设双槽是 /dev/mmcblk0p2 和 /dev/mmcblk0p3,你实际项目里替换成自己的分区即可。
#!/bin/sh
set -eu
rootdev="$(findmnt -n -o SOURCE /)"
mkdir -p /run
case "$rootdev" in
/dev/mmcblk0p2)
ln -sfn /dev/mmcblk0p3 /dev/standby
echo "A" > /run/active_slot
echo "/dev/mmcblk0p3" > /run/standby_slot
;;
/dev/mmcblk0p3)
ln -sfn /dev/mmcblk0p2 /dev/standby
echo "B" > /run/active_slot
echo "/dev/mmcblk0p2" > /run/standby_slot
;;
*)
echo "unsupported active root device: $rootdev" >&2
exit 1
;;
esac
这个脚本本身不是 SWUpdate 官方自带的文件,但它符合官方最佳实践里“在启动时建立 stand-by link”的思路。它最大的价值是把“谁是备用槽”这个问题前移到系统启动阶段,从而让 sw-description 永远只面对一个稳定的设备路径:/dev/standby。([Sbabić][2])
2. systemd 服务
为了保证 SWUpdate 启动前这个链接已经准备好,可以做一个 one-shot 服务:
[Unit]
Description=Create standby slot symlink for SWUpdate
DefaultDependencies=no
After=local-fs.target
Before=swupdate.service
Requires=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/bin/standby-link.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
然后把脚本和 service 一起打进设备端镜像。这样每次开机,系统都会自动决定备用槽,并且在 swupdate.service 之前准备好环境。
五、设备端:编写 sw-description 与 SWU 打包 recipe
1. sw-description 的设计思路
sw-description 是 SWUpdate 的中心描述文件。官方文档强调,它应该是你的“升级概念”落地后的直接产物,而不是临时拼凑出来的一堆标签。它还特别建议:始终显式设置 type,始终为 artifact 设置 sha256,不要依赖默认安装器,也不要只相信 CPIO 自带的弱校验。([Sbabić][2])
在本文的双槽设计里,sw-description 的核心只有两件事:
第一,把根文件系统写入 /dev/standby;
第二,写入 Bootloader 变量,让下次启动切换到新槽,并进入“待确认”的事务状态。
SWUpdate 文档说明了两种修改 Bootloader 环境的方法:可以把一个文件作为 type = "bootloader" 的镜像交给 bootloader handler,也可以直接在 bootenv 段里列出要修改的变量。对于工程整洁性而言,bootenv 往往更直观。([Sbabić][8])
下面给出一个适合本方案的 sw-description 示例:
software =
{
version = "@@DISTRO_VERSION@@";
images: (
{
filename = "core-image-minimal-@@MACHINE@@.ext4.gz";
device = "/dev/standby";
type = "raw";
compressed = "zlib";
installed-directly = true;
sha256 = "$swupdate_get_sha256(core-image-minimal-@@MACHINE@@.ext4.gz)";
}
);
bootenv: (
{
name = "ustate";
value = "1";
},
{
name = "upgrade_available";
value = "1";
}
);
}
这里有几个要点。installed-directly = true 表示启用流式安装,官方最佳实践把它称为 zero-copy 或 streaming mode;对于普通 rootfs 分区,这可以减少临时 RAM 占用。sha256 使用的是 meta-swupdate 提供的自动替换能力。ustate 则对应官方推荐的升级状态机变量,Bootloader 可以根据它判断“当前新系统是否处于待确认状态”。([Sbabić][2])
2. SWU 打包 recipe
官方文档给出了使用 inherit swupdate 打包 SWU 的标准方式:在 recipe 中声明 SRC_URI 包含 sw-description,声明 IMAGE_DEPENDS 和 SWUPDATE_IMAGES,然后给出每个镜像对应的文件后缀。([Sbabić][6])
一个最小可用的 recipe 可以这样写:
DESCRIPTION = "SWU package for product rootfs"
LICENSE = "MIT"
SRC_URI = "file://sw-description"
IMAGE_DEPENDS = "core-image-minimal"
SWUPDATE_IMAGES = "core-image-minimal"
SWUPDATE_IMAGES_FSTYPES[core-image-minimal] = ".ext4.gz"
inherit swupdate
如果你的产品镜像不是 core-image-minimal,只要把名字替换掉即可。构建时执行:
bitbake your-product-swu
生成出来的 .swu 就是控制端要上传的包。
3. 关于签名:不要只做“能升级”,要做“可信升级”
SWUpdate 的签名文档把问题说得非常透:如果你只签整个复合镜像,往往要等整包都加载完才能确认是否合法;如果你只签子镜像,攻击者又有可能把不同 release 的已签名子镜像重新拼起来。官方给出的更稳妥方案,是签名 sw-description,再对每个子镜像做哈希校验。这样,只有可信来源生成的 sw-description 会被接受,而 sw-description 里面又包含每个子镜像的哈希,从而保证 release 描述与实际交付内容是一一对应的。文档还明确说明,公钥应该安装在目标端,私钥则属于构建系统。([Sbabić][9])
因此,正式版本建议至少做到三件事:
构建机持有私钥;
设备端镜像内置公钥;
所有产线包都带 sw-description.sig 和每个 artifact 的 sha256。
如果你当前还处在验证阶段,可以先把签名留到第二阶段;但从产品演进角度看,这一步几乎是迟早要补上的。SWUpdate 甚至要求,一旦启用了 CONFIG_SIGNED_IMAGES,运行时不能随便关掉检查。([Sbabić][9])
六、控制端:编写真正可用的升级控制程序
1. 控制端程序应当做什么
既然设备端已经暴露出 POST /upload、POST /restart 和 WebSocket 事件,那么控制端程序的职责就很清晰了:
先建立 WebSocket 监听;
再通过 multipart/form-data 上传 .swu;
根据 WebSocket 返回的 status、step、message 事件刷新进度;
升级完成后,视策略调用 POST /restart;
重启后,再通过你的业务链路去确认新版本是否真正生效。
这套交互逻辑完全符合 SWUpdate 官方 Web 接口的设计:WebServer 用 REST 负责“触发动作”,用 WebSocket 负责“持续反馈过程”。([Sbabić][4])
2. 一个可直接运行的 Python 控制端示例
下面给出一个简化但可实际工作的控制端程序。它使用 requests 负责上传,使用 websocket-client 监听事件。你后续无论把它包成 CLI、Qt 调用层,还是服务进程,都可以在这个基础上继续扩展。
#!/usr/bin/env python3
import argparse
import json
import os
import sys
import threading
import time
import requests
import websocket
class SwuMonitor:
def __init__(self, host: str, port: int):
self.host = host
self.port = port
self.ws_url = f"ws://{host}:{port}/ws"
self.last_status = "IDLE"
self.failed = False
self.finished = False
def on_message(self, ws, message):
try:
data = json.loads(message)
except Exception:
print(f"[WS] invalid message: {message}")
return
msg_type = data.get("type")
if msg_type == "status":
status = data.get("status", "UNKNOWN")
self.last_status = status
print(f"[STATUS] {status}")
if status in ("SUCCESS", "DONE"):
self.finished = True
elif status == "FAILURE":
self.failed = True
self.finished = True
elif msg_type == "step":
name = data.get("name", "")
step = data.get("step", "?")
number = data.get("number", "?")
percent = data.get("percent", "0")
print(f"[STEP] {step}/{number} {name} {percent}%")
elif msg_type == "message":
level = data.get("level", "")
text = data.get("text", "")
print(f"[MESSAGE][{level}] {text}")
if str(level) == "3":
self.failed = True
elif msg_type == "info":
print(f"[INFO] {data.get('source', '')}")
elif msg_type == "source":
print(f"[SOURCE] {data.get('source', '')}")
else:
print(f"[WS] {data}")
def on_error(self, ws, error):
print(f"[WS][ERROR] {error}")
def on_close(self, ws, close_status_code, close_msg):
print(f"[WS] closed: {close_status_code} {close_msg}")
def run(self):
self.ws = websocket.WebSocketApp(
self.ws_url,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close,
)
self.ws.run_forever()
def start(self):
t = threading.Thread(target=self.run, daemon=True)
t.start()
# 给 websocket 一点建立连接的时间
time.sleep(1.0)
def upload_swu(host: str, port: int, swu_path: str):
url = f"http://{host}:{port}/upload"
with open(swu_path, "rb") as f:
files = {"file": (os.path.basename(swu_path), f, "application/octet-stream")}
resp = requests.post(url, files=files, timeout=3600)
print(f"[HTTP] upload response: {resp.status_code}")
print(resp.text)
resp.raise_for_status()
def restart_device(host: str, port: int):
url = f"http://{host}:{port}/restart"
resp = requests.post(url, timeout=30)
print(f"[HTTP] restart response: {resp.status_code}")
print(resp.text)
def main():
parser = argparse.ArgumentParser(description="SWUpdate remote control client")
parser.add_argument("--host", required=True, help="device ip or hostname")
parser.add_argument("--port", type=int, default=8080, help="swupdate web port")
parser.add_argument("--file", required=True, help="path to .swu")
parser.add_argument("--restart", action="store_true", help="restart after success")
args = parser.parse_args()
if not os.path.exists(args.file):
print(f"SWU not found: {args.file}", file=sys.stderr)
sys.exit(2)
monitor = SwuMonitor(args.host, args.port)
monitor.start()
try:
upload_swu(args.host, args.port, args.file)
except Exception as e:
print(f"[ERROR] upload failed: {e}", file=sys.stderr)
sys.exit(3)
while not monitor.finished:
time.sleep(0.5)
if monitor.failed:
print("[RESULT] update failed", file=sys.stderr)
sys.exit(4)
print("[RESULT] update success")
if args.restart:
try:
restart_device(args.host, args.port)
except Exception as e:
print(f"[WARN] restart request failed: {e}", file=sys.stderr)
if __name__ == "__main__":
main()
这个程序对应的交互逻辑,和官方文档是对得上的:上传用 POST /upload,事件监听用 WebSocket,状态由 status 和 step 等事件表示。([Sbabić][4])
3. 控制端在 Yocto 中的打包方法
如果控制端系统本身也是 Yocto 发行版,可以把这个程序单独做成一个 recipe,安装到镜像里。下面给一个简化版本:
SUMMARY = "SWUpdate control client"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835a3157e3b7f8a5fef18df2d5f2d9b"
SRC_URI = "file://swu-control.py"
S = "${WORKDIR}"
RDEPENDS:${PN} += " \
python3-core \
python3-requests \
python3-websocket-client \
"
do_install() {
install -d ${D}${bindir}
install -m 0755 ${WORKDIR}/swu-control.py ${D}${bindir}/swu-control
}
然后在控制端镜像里加入:
IMAGE_INSTALL:append = " swu-control"
这样,控制端开机后就有一个现成的升级程序可用。若你后续想做图形界面,可以让 Qt 或 WebView 前端去调用这个 CLI,也可以把它改造成一个常驻 D-Bus 服务或本地 HTTP 服务,供前端间接调用。
4. 实际调用方式
上传一份升级包并在成功后重启设备端:
swu-control --host 192.168.8.2 --port 8080 --file product.swu --restart
如果你还在早期验证阶段,甚至可以先不用写 Python,直接用官方示例里的 curl 命令测通链路:
curl -F file=@product.swu http://192.168.8.2:8080/upload
这是官方文档明确给出的调用方式,非常适合做第一阶段连通性验证。([Sbabić][4])
七、Bootloader、回滚与事务确认:真正可靠升级的关键
真正把嵌入式升级做成熟,难点不在“上传成功”,而在“失败时仍能安全恢复”。SWUpdate 最佳实践把这件事解释得很清楚:升级不是一个瞬间动作,而是一个事务。这个事务至少跨越四个阶段:更新代理写入、设备重启、Bootloader 切换、用户空间确认。文档还定义了两个重要变量:recovery_status 用于表示升级过程是否被中断或失败,ustate 用于表示新版本是否处于待验证状态;Bootloader 应该根据这些变量和启动次数决定是否回退。([Sbabić][2])
这也是为什么我前面一直强调,sw-description 里不要只写“把 rootfs 刷进去”。如果 Bootloader 完全不知道这次升级发生了什么,那么一旦新系统启动失败,系统就失去了自动回退的抓手。反过来,只要 Bootloader 和 SWUpdate 对事务变量有共识,即使升级中途断电,Bootloader 也能识别“上一次事务没有完成”,然后选择继续走恢复路径或回退路径。SWUpdate 官方文档明确说明,在双拷贝场景下,如果断电发生在安装过程中,由于活动槽还没切换,系统仍然可以回到旧副本启动。([Sbabić][10])
因此,本文推荐的最小 Bootloader 协议是这样的:
- 设备端当前运行在槽 A,
/dev/standby指向槽 B。 - SWUpdate 把新 rootfs 写入槽 B,并把
ustate=1、upgrade_available=1一类变量写入环境。 - Bootloader 下次启动时检测到
ustate=1,于是切换到槽 B。 - 新系统启动成功后,在用户空间做完自检,再把
ustate=0或等效确认变量写回环境。 - 若新系统没有在规定次数内完成确认,Bootloader 则判定新软件有问题,自动切回槽 A。
这套流程和 Mender 文档中“Bootloader 负责选择恰当分区并提供回滚能力”的要求,以及 RAUC 对 inactive slot 的使用方式,本质上是同一类稳健更新思想。([docs.mender.io][11])
八、成熟方案为什么都在强调安全与最小依赖
SWUpdate 最佳实践里有两句非常值得反复体会。第一句是:升级要尽量减少对业务应用的依赖。原因是业务应用本身也会被更新,假如你把升级流程深度绑在业务应用上,一次应用 bug 就可能把升级能力本身也拖死。第二句是:总是打开 sha256 校验,总是显式设置 type,总是优先保证原子性。这几条看似琐碎,实际上都是为了让现场设备在“系统已经不太正常”的情况下,仍然保留恢复能力。([Sbabić][2])
从安全角度看,正式量产时建议最少做到四层防护。第一层是签名 sw-description,并让设备端只信任构建系统签出的包;第二层是所有 artifact 带 sha256,避免 release 描述和真实内容被拼接篡改;第三层是 Bootloader 环境以原子方式写入,不要在 postinstall 脚本里反复 fw_setenv 多个变量;第四层是对 Web 接口加控制手段,例如局域网白名单、会话令牌或上层认证,否则任何能访问设备端 8080 的主机理论上都能发起升级。前两层来自 SWUpdate 的签名与校验机制,第三层来自它对 Bootloader 环境原子写入的最佳实践,第四层则是任何生产级远程控制接口都绕不开的工程常识。([Sbabić][9])
如果你的产品后续还需要进一步加强保护,SWUpdate 的 Yocto 集成还支持对 SWU 内容进行对称加密,并允许在 recipe 里通过 SWUPDATE_AES_FILE、SWUPDATE_IMAGES_ENCRYPTED[...] 等变量控制 artifact 加密;文档也说明了,对 sw-description 本身做加密是可行的,但前提是 SWUpdate 编译时打开了相应选项。是否现在就启用加密,要看你的产品是否已经进入“必须防止离线包被直接分析”的阶段。([Sbabić][6])
九、测试路径、常见问题与演进方向
这类方案建议按“三层递进”来验证。第一层只验证链路:设备端 swupdate.service 是否启动,8080 是否监听,控制端 curl -F 是否能成功触发上传。第二层验证安装:上传后 WebSocket 是否能收到 START、RUN、SUCCESS 或 FAILURE,目标槽位内容是否真的被更新。第三层验证事务:设备端重启后,Bootloader 是否切到新槽,新系统是否能完成确认,如果人为制造失败,是否真的能自动回退。SWUpdate 的官方 Web API 事件模型已经把升级过程拆成了足够清晰的状态和步骤字段,因此这套测试并不难做。([Sbabić][4])
实战中最常见的错误,往往不是 SWUpdate 本身,而是“外围工程没闭环”。例如,很多人能成功生成 .swu,但 Bootloader 没实现事务变量,所以升级后切不过去;也有人把 A/B 槽都写了,结果看起来“升级成功”,本质上却把回滚能力一并覆盖掉;还有人把控制程序做成了直接 SSH 执行一串命令,早期调试没问题,一到产品化阶段就遇到权限、稳定性和状态同步问题。SWUpdate 最佳实践其实已经把这些坑提前点出来了:写升级概念、做风险分析、减少依赖、不要破坏原子性、规划救援系统。([Sbabić][2])
当你的系统以后从“一台控制端对一台设备端”升级到“一台控制端对多台从设备”时,可以进一步研究 SWUpdate 的 swuforward handler。官方明确说明,这个 handler 就是为 master / slaves 场景准备的:主设备作为唯一对外接口,把嵌入式 SWU 转发给运行着 SWUpdate WebServer 的从设备。换句话说,本文这套“控制端自定义程序 + 设备端 WebServer”的模型,并不是死路,而是恰恰可以自然过渡到更大规模的主从升级架构。([Sbabić][3])
十、结语:把升级做成系统能力,而不是一次性脚本
到这里,整套方案已经清楚了:
设备端在 Yocto 中集成 meta-swupdate,安装 swupdate 与 swupdate-www,打开 Mongoose/WebServer,借助 systemd 常驻运行;
设备端启动时建立 /dev/standby,确保升级始终写向备用槽;
sw-description 只描述两件事:如何把新镜像写到备用槽,如何让 Bootloader 进入待验证事务;
控制端不去“远程运行 swupdate-client”,而是自己实现一个轻量控制程序,调用 POST /upload 上传 .swu,通过 WebSocket 监听过程,必要时再发 POST /restart;
Bootloader 负责最后一环:选择新槽启动、统计失败次数、完成自动回退。
这套架构之所以值得长期使用,不是因为它“看起来合理”,而是因为它和 SWUpdate 官方推荐的启动方式、Web API、A/B 设计思路、签名机制,以及更广义上的 Mender/RAUC 稳健升级原则,是彼此对齐的。真正成熟的升级系统,从来不是一条命令,也不是一个按钮,而是一条从构建系统、包签名、设备端守护进程、Bootloader 状态机到控制端交互程序都闭合起来的链路。只要这条链路搭好了,后面你无论增加版本控制、升级审批、多设备管理,还是把离线局域网升级扩展成云端统一编排,整体思路都不需要推倒重来。
📺 B站:博主个人介绍
📘 博主书籍-京东购买链接*:Yocto项目实战教程
📘 加博主微信,进技术交流群: jerrydev
2628



被折叠的 条评论
为什么被折叠?



