这里记录 Swupdate 的简易使用说明。
初识
流程
SWUpdate 是位于用户空间的应用程序,用于升级嵌入式系统(不包含 bootloader)。
它以事务的方式标识整个升级过程,事务的标识会写入到 bootloader 中,bootloader 会根据事务标记的值来确认当前升级是否成功。
比如 SWUpdate 通过设置环境变量recovery_status
,来表示升级过程:
开始升级时,其值为
progress
升级成功后,其值会被擦除
升级失败,其值为
failed
bootloader 通过查看其值为progress
或failed
则代表其升级未完成:
如果当前为
single-copy
模式,则会再次启动升级流程如果当前为
double-copy
模式,则会启动上一个版本的程序
文件格式
上图为其打包后的文件版本,主要是sw-description
来实现多个镜像文件的描述。
可以看到它是将多个文件打包为一个cpio
文件,那么这里再来复习一下cpio
工具的操作:
1 | ## 打包 |
需要注意的是: cpio 打包后的文件大小不能超过 4GB.
编译
在 buildroot
中只需要搜swupdate
就可以找到该包并使能,如果想要更细致的配置,可以通过以下命令配置:
1 | make swupdate-menuconfig |
在输出路径output/build/swupdate/tools
中有文件swupdate-progress.c
可以作为很好的参考,用于与swupdate
交互获取当前的状态。
为了与 U-Boot 交互,则需要进行以下配置:
在
Bootloader Interfaces
中使能 U-Boot,并设定文件/etc/fw_env.config
以及/etc/u-boot-initial-en
设定
U-Boot
中变量为ustate
使用
swupdate
的一般流程如下:
提取
sw-description
并校验,如果还使能了签名验证,还会提取sw-description.sig
文件进行签名验证。根据
sw-description
中提供的信息,读取当前设备的硬件版本,来验证是否有兼容该硬件版本的软件包。根据
sw-description
中的信息识别哪些软件包需要被安装,如果具有embedded-script
则会在解析这些软件包前执行这些脚本,如果具有hooks
则会在解析软件包时执行(即使这些软件包会被跳过)。最终生成一张执行列表和对应的哪些handler
需要被调用。如果有
pre update command
,则先执行这些命令如果有分区的必要,则执行
partition handlers
依次从
cpio
文件中提取需要安装的软件包,在读取软件包时还会进行内容校验,如果检验失败则报错。在安装软件包之前,如果具有
pre-install
脚本,则会先执行这些脚本执行对应软件包的
handler
来安装软件包安装完成后,如果具有
post-install
脚本,则会执行这些脚本更新 bootloader 的环境变量
向外部接口输出升级状态
如果具有
post update command
则执行这些命令
使用swupdate
执行升级命令为:
1 | swupdate -i <filename> |
也可以启动一个网络服务,通过网页来升级:
1 | 启动后就可以通过 http://<target_ip>:8080 来访问 |
改变 U-BOOT
U-BOOT
可以保存两份环境变量,便于保证在更新环境变量时的安全性,要使能这个特性,需要配置CONFIG_ENV_OFFSET_REDUND
或CONFIG_ENV_ADDR_REDUND
。
除此之外,还可以在U-BOOT
中增加一个启动计数器,如果计数器没有正确的被应用程序清零则意味着这个版本升级的应用没有正常运行,然后可以切换到之前备份的应用。
构建升级包
升级包需要sw-updescription
是第一个文件,其余的镜像可以依次往后放即可。使用类似下面的脚本就可以打包:
1 | CONTAINER_VER="1.0" |
也可以通过GitHub - sbabic/swugenerator: A host tool to generate SWU update package for SWUpdate来打包生成升级包。
升级包的查看可以通过下面的命令完成:
1 | swupdate -c -i my-software_1.0.swu |
升级策略
single copy
正常情况下,bootloader 直接启动用户的内核,进入文件系统运行应用程序。
当需要升级时:
通知 bootloader 需要启动
swupdate
,然后重启系统- 通知的方式多种多样,比如通过环境变量、GPIO等
bootloader 启动带 swupdate 的内核和 RAMFS
在 RAMFS 中启动
swupdate
分析升级包并升级
如果升级过程失败,应用程序无法正确清空 bootloader 的启动计数器,则 bootloader 会主动进入升级系统。
double copy
bootloader 交替的启动切换最新的软件,swupdate
则升级那个未被启动的软件分区。
当当前应用程序没有正确清空 bootloader 的启动计数器时,bootloader 会主动切换回上一个版本的应用程序。
bootloader 根据变量ustate
的值为1,来确认目前有新的软件被安装,那么就需要通过计数器来测试该软件是否安装正确。当测试失败,bootloader 也可以修改ustate
的值为 3,来通知应用程序升级是否成功。
当
ustate=0
,则表示当前无新升级,bootloader 正常引导当
ustate=1
,表示有新软件被安装,需要测试测试失败,由 bootloader 将值设置为 3
测试成功,由应用程序将值设置为 0
在 bootloader 中,需要准备的变量有:
在double-copy
的基础上,还可以增加一个救援系统,这样当两个版本都无法正确运行(或那个硬盘损坏)的情况下,仍然可以启动救援系统来重新格式化、更新系统。
这个救援系统也是可以被更新的
语法
示例
一个典型的sw-description
文件内容如下:
1 | software = |
以software
tag为顶层描述,下面的就是为各个镜像的单独说明。
上面中的hardware-compatib
是为了软件与硬件的兼容,而硬件的信息则存储于/etc/hwrevision
中,其格式为:
1 | boardname:设备名称,为了便于一个升级包还可以升级多种不同设备 |
对于 double copy 的升级策略,一个升级包可能会对应两个分区,那么配置文件应该这样描述:
1 | software = |
在实际升级时,到底应该是选择哪个分区,则是由应用程序来区分(比如查看当前程序是挂载在哪个分区),然后给swupdate
发送消息。
这个消息是要在
swupdate
启动前就确认要升级哪个分区,然后通过-e <selection,mode>
来告知swupdate
。也可以在启动前创建一个连接文件
/dev/standby
指向需要被升级的分区。如果要在运行时来区分,就要在
sw-description
中编写嵌入式脚本来区分,Lua 脚本使用getroot()
函数获取当前文件系统挂载的分区。
硬件兼容性
一个软件包可以兼容多个版本的硬件,则可以像前面一样,在文件内容开始一次性给出。但也有可能在不同的硬件版本下的文件会略有不同,那么还可以再进一步说明:
1 | software = |
如果其中有部分版本完全一致,那么还可以使用引用的方式:
1 | software = |
ref
是表示引用的关键字,后面的#
是必须的。可以用./
表示当前层级,用../
表示上一个层级。
swupdate
获取版本号是在/etc/hwrevision
文件中,但是这个文件的内容则可以在应用程序启动时通过各种方式进行更新,比如读取当前硬件上的 EEPROM 获取版本号等。
images
images
标识表示要更新到系统中的镜像文件,其语法为:
1 | images: ( |
对于 emmc 而言,其内容大体如下:
1 | { |
对于 flash 而言,其内容大体如下:
1 | { |
Files
files
标识用于拷贝文件到系统,其语法如下:
1 | files: ( |
主要就是将文件拷贝到对应路径。
scripts
scripts
标记用于执行一系列的脚本,默认情况下如果没有标注脚本的类型,swupdate
会认为是lua
脚本。
lua
1 | scripts: ( |
lua 脚本必须至少包含 3 个函数:
function preinst()
:安装镜像前会被执行function postinst()
:安装镜像后会被执行function postfailure()
:升级失败后会被执行
shellscript
1 | scripts: ( |
swupdate
会在镜像安装前后执行脚本,在执行时会传入参数”preinst”、”postinst”或”postfailure”,脚本可以依据这些参数进行不同的操作。
除此之外,也可以单独编写preinstall
和postinstall
脚本:
1 | scripts: ( |
1 | scripts: ( |
升级过程中的标记状态
默认情况下swupdate
通过设置 bootloader 的环境变量recovery_status
来表示升级的过程,其值有以下几种情况:
in_progress
:正在升级过程中值被清空:升级成功
failed
:升级失败
如果想关闭这些状态记录,可以在配置文件中设置:
1 | software = |
除了这种字符串标记外,还有将 bootloader 的环境变量ustate
来设数值方式:
1
:安装成功3
:安装失败
关闭ustate
也是在配置文件中设置:
1 | software = |
更新 bootloader 的环境变量
有两种方式可以更新 bootloader 的环境变量,一种方式是将变量写在文件中,然后将此文件在配置文件中标注一下:
1 | images: ( |
文件中变量赋值的格式为:<name of variable>=<value>
,如果值不设置,则该变量会被清除掉。
就像下面这样:
1 | Default variables |
另一种方式是直接在配置文件中就写变量:
1 | bootenv: ( |
版本管理
swupdate
可以进行版本号比较,默认格式为:<major>.<minor>.<revision>.<build>
每个小段都是由数值组成的,其值为 0~65535,4 个 16 位组合成 64 位进行大小比较。
在启动swupdate
之前,应用软件需要更新/etc/sw-versions
来保存各个软件包的版本,然后再来启动swupdate
进行版本管理,文件内容格式为:
1 | <name of component> <version> |
嵌入式脚本(Embedded Script)
嵌入式脚本指的是在sw-description
中嵌入脚本,该脚本是对配置文件全局可见的。
1 | embedded-script = "<Lua code>" |
在编写脚本时需要注意:双引号应该使用转义字符进行转义,以避免影响脚本的解析。
比如:
1 | print (\"Test\") |
在配置文件中的镜像文件或普通文件类型都可以调用一个嵌入式脚本的函数,通过hook
标记函数名:
1 | files: ( |
一个脚本的示例如下:
1 | function set_version(image) |
上述的image
则为传入的参数列表,该脚本设置了版本和安装属性然后返回。
swupdate
提供了一些函数便于调用,在脚本前需要写入:
1 | require ('swupdate') |
验证与签名
基本逻辑
swupdate
的逻辑为:
对
sw-description
进行签名验证,确保该文件未被篡改在
sw-description
中包含各个独立镜像文件的哈希校验,确认这些镜像文件的正确性
swupdate
可以通过menuconfig
来配置签名验证使用的算法
RSA 公钥/私钥验证:私钥存放在构建主机中,公钥存放在目标设备上
CMS 用于证书
GPG 用于签名
生成密钥和证书
对于RSA
和CMS
算法,OpenSSL 用来生成密钥,对于GPG
,gpg 用于生成密钥和对镜像文件签名。
使用 RSA
生成公钥和私钥:
1 | 先生成私钥 |
使用 RSA 生成签名:
1 | 对 sw-description 生成 rsa-pkcs#1.5 签名 |
证书生成
1 | openssl req -x509 -newkey rsa:4096 -nodes -keyout mycert.key.pem \ |
mycert.key.pem
用于保存在构建主机上mycert.cert.pe
用于保存在目标设备机上
使用 CMS 进行签名
1 | openssl cms -sign -in sw-description -out sw-description.sig -signer mycert.cert.pem \ |
使用签名和验证
对于签名而言,配置文件sw-description
和sw-description.sig
结合使用,签名文件跟在配置文件之后。
在配置文件中的镜像都需要包含属性sha256
,代表该镜像文件的校验和。
下面的脚本演示了如何打包:
1 | !/bin/bash |
下面的配置文件演示了带有 sha256 校验的内容:
1 | software = |
对称加密
swupdate
支持使用 AES 对镜像文件进行对称加密,在配置swupdate
时需要使能ENCRYPTED_IMAGES
.其使用步骤如下:
生成密钥
对于aes-256-cbc
,需要生成 32 字节密钥和 16 字节初始化向量:
1 | openssl rand -hex 32 |
加密镜像
1 | openssl enc -aes-256-cbc -in <INFILE> -out <OUTFILE> -K <KEY> -iv <IV> |
<INFILE>
:输入的原始镜像文件<OUTFILE>
:加密后的镜像文件<KEY>
:上面生成的 32 字节的密钥<IV>
:上面生成的 16 字节初始化向量
创建密钥文件
创建密钥文件,将密钥和初始化向量以空格分隔,写入文件。并在启动 swupdate
时,以-K
选项传入该文件:
1 | 390ad54490a4a5f53722291023c19e08ffb5c4677a59e958c96ffa6e641df040 d5d601bacfe13100b149177318ebc7a4 |
配置示例
最后是在配置文件中加入描述:
1 | software = |
handler
handler
指的就是对各种镜像文件的安装处理程序,根据镜像文件的类型来匹配不同的handler
。
swupdate
开放了接口,可以扩展增加各种handler
,以适配用户定义的类型。
创建新的 handler
只需要按照swupdate
的接口注册执行函数就可以了。
1 | int my_handler(struct img_type *img, |
img_type
指向要安装的镜像文件的起始,处理函数通过此结构体获取文件的信息,然后读取文件内容并执行安装。
data
是用于描述安装过程的数据指针,对于脚本的 handler,其为struct script_handler_data
swupdate
还提供了通用函数,将流数据拷贝到目的地址:
1 | int copyfile(int fdin, int fdout, int nbytes, unsigned long *offs, |
fdin
:输入流,在处理函数中通过img->fdin
获得hash
:镜像文件的签名
用户通过下面的函数注册新的handler
函数:
1 | __attribute__((constructor)) |
其函数原型为:
1 | register_handler(my_image_type, my_handler, my_mask, data); |
my_image_type
:以字符串表示的匹配镜像文件类型my_handler
:处理函数的指针my_mask
:HANDLER_MASK
枚举值,表示可以处理的类型data
:用于传递给处理函数的数据
API
swupdate
具有可被外部程序调用的 API,用于启动swupdate
,发送镜像文件,获取状态等。
swupdate
使用的是 Unix Domain Socket,socket 路径按照以下顺序来决定:
编译时配置的
CONFIG_SOCKET_CTRL_PATH
当环境变量
RUNTIME_DIRECTORY
设置了,则其位于$RUNTIME_DIRECTORY/sockinstctrl
当环境变量
TMPDIR
设置了,则其位于$TMPDIR/sockinstctrl
/tmp/sockinstctrl
swupdate 也编译了一个
swupdate-client
工具,用于演示客户端如何与服务端进行通信。
这也就意味着swupdate
在系统启动后就已经启动服务了,其脚本swupdate.sh
,用于启动该服务。该脚本还会遍历位于/etc/swupdate/conf.d/
处的配置文件。
客户端与服务端的通信逻辑
同时,swupdate
提供了客户端库,便于以直接调用函数的方式来完成通信。
在文件network_ipc.h
中定义了通信的帧结构:
1 | typedef struct { |
magic
:通信包的魔数type
:包类型,包含:REQ_INSTALL, ACK, NACK, GET_STATUS, POST_UPDATE, SWUPDATE_SUBPROCESS, SET_AES_KEY
msgdata
:通信的数据数据
客户端和swupdate
通信的流程如下图:
客户端先发
REQ_INSTALL
,等待获取到ACK
,然后就可以发送镜像文件了客户端持续发送镜像文件直到发送完成,发送标记给
swupdate
客户端从
swupdate
获取多次获取状态,来记录升级的进度
客户端的 API 库说明
启动升级
与升级相关的 API 有以下几个:
1 | int swupdate_async_start(writedata wr_func, getstatus status_func, |
swupdate_async_start
:创建了一个线程与swupdate
进行通信wr_func
:当需要获取升级包时,此函数会被调用status_func
:当完成数据流传输时,有状态改变时该函数会被调用end_func
:当swupdate
停止后,该函数会被调用req
:是swupdate_request
结构体,用于控制升级过程size
:即为结构体的大小
1 | struct swupdate_request { |
source
:指定数据源的类型,包含SOURCE_UNKNOWN, SOURCE_WEBSERVER, SOURCE_SURICATTA, SOURCE_DOWNLOADER, SOURCE_LOCAL
dry_run
:运行状态,包含RUN_DEFAULT (set from command line), RUN_DRYRUN, RUN_INSTALL
info,len
:用于转发处理进程的接口software_set,running_mode
:选择设置状态
swupdate_request
结构体需要首先通过swupdate_prepare_req()
来设置为默认值后再来进一步设置。
具体的使用流程,可以参考examples/client
文件夹。
设置 AES 密钥
AES 解密的密钥可以在命令行通过-K
选项输入,也可以通过下面的函数输入:
1 | int swupdate_set_aes(char *key, char *ivt); |
这里固定了是 AES-256 算法,所以key
的长度是 64 字节的字符串,ivt
是 32 字节的字符串。
控制swupdate
函数ipc_send_cmd
用于给swupdate
发送命令,这个函数是同步发送的:
1 | struct ipc_message { |
source
的值为SWUPDATE_SUBPROCESS
cmd
的类型可以参考network_ipc.h
在升级过程中获取信息
除了可以获取升级的成功与失败外,还可以获取升级过程中的进度。客户端可以注册一个函数,来获取升级过程中的状态。
swupdate-progress 是一个工具用于获取升级过程中的状态
客户端注册的方式是通过connect
来连接/tmp/swupdateprog
,然后使用recv
来获取消息即可,消息的结构如下:
1 | struct progress_msg { |
status
:大的状态,包含START, RUN, SUCCESS, FAILURE, DOWNLOAD, DONE
dwl_percent
:当状态为DOWNLOAD
时,表示下载的状态dwl_bytes
:已经下载的总字节数nsteps
:总共运行的 handler 的数量cur_step
:当前运行的 handler 的序号cur_percent
:当前运行的handler
的进度cur_images
:当前正在安装的镜像名hnd_name
:当前正在运行的 handler 名infolen
:info 数组中包含的数据长度info
:安装过程的附加信息
整个过程的示例代码位于:tools/swupdate-progress.c
bootloader 接口
bootloader 接口主要是对其环境变量进行设定的接口,其位于include/bootloader.h
文件中,包含的有:
1 | // 获取环境变量 |
这些函数需要一开始被注册:
1 | static bootloader trunk = { |
在 bootloader/uboot.c
中有使用示例。