分享一个 swupdate 工具使用 ubivol 流式安装的注意事项。
背景
当今存储价格疯涨,项目只能选择容量小的 NAND flash 和 RAM,文件系统还得选择 ubi,在 OTA 的时候把 .swu 升级包下载下来之后根本就没有多少空闲的容量进行解压,只得考虑 swupdate 本身自带的流式安装的方式,在读取 .swu 升级包的同时就以流式的方式直接写 flash,免去解压的步骤。
难点
- 由于文件系统是 ubi 的,还不能直接使用 swupdate 的 flash 直写模块,需要用到 ubivol 模块
- ubivol 模块要配置好参数,以应对各种可能出现的情况
- swupdate 的流式安装是读取 .swu 升级包时就进行的早期行为,像 preinstall 这类的脚本都是在解压后执行的,如果要做前处理,需要使用 embedded-script 的方式
- ubi 卷的一些前后处理也很重要,一方面是要确保流式安装能成功写入,另一方面是要保障重启完仍能加载,这些并没有包含在 ubivol 模块的义务里
ubivol 模块
ubivol 是 swupdate 自带的 UBI 卷写入 handler,即 type = "ubivol"。它只负责把镜像内容写进 UBI 卷,不改动 UBI 布局,行为与 mtd-utils 的 ubiupdatevol 一致,底层复用的也是同一个库 libubi:按卷名找到目标卷后发起 volume update,把数据流式写入。
sw-description 中 ubivol 相关的参数:
| 参数 | 说明 |
|---|---|
| volume | 目标 UBI 卷名,必填 |
| device | MTD 设备节点,如 /dev/mtd12。使用 auto-resize 时必须指定 |
另外两个 property 用来控制写入前对卷的处理:
| property | 说明 |
|---|---|
| always-remove | 写入前无条件删除目标卷再重建,保证旧数据不残留。官方建议与 auto-resize 搭配使用,因为删除之后要按镜像大小重建 |
| auto-resize | 写入前把卷大小调整为恰好装下镜像 |
默认不删卷、就地 update,与 ubiupdatevol 一样;always-remove 则是先删后建,多一次卷操作,换来的是目标卷的干净状态。
sw-description
由于要使用 embedded-script,为了具有更好排版的可读性,我们要使用 libconfig 的格式,大致结构如下:
software = {
version = "1.0.0";
description = "UBI streaming update with embedded precheck";
embedded-script =
"...";
stable = {
nowa_nextb = {
images = (
{
filename = "app";
device = "/dev/mtd12";
type = "ubivol";
volume = "app";
installed-directly = true;
properties = {
always-remove = "true";
auto-resize = "true";
};
}
);
scripts = (
{
filename = "postinstall";
type = "postinstall";
data = "12 app_b";
}
);
};
nowb_nexta = {
images = (
{
filename = "app";
device = "/dev/mtd11";
type = "ubivol";
volume = "app";
installed-directly = true;
properties = {
always-remove = "true";
auto-resize = "true";
};
}
);
scripts = (
{
filename = "postinstall";
type = "postinstall";
data = "11 app_a";
}
);
};
};
};
其中流式安装必须写死 device 位置,ubivol 模块的相关参数如上文所述设置,最后要把 MTD 的设备号传参给后处理脚本做可靠性处理,见下文。
前处理
Swupdate 在启动时会默认扫描一遍所有的 MTD 设备,并且做 ubiattach 操作,而在结束的时候是不会进行 ubidetach 操作的。因此如果在 swupdate 运行的过程中有一些对 ubi 卷的操作不当,数据可能就没有成功保存。这里分几种情况:
- 要写入的分区没有进行 ubiattach,则 swupdate 运行时自动进行 ubiattach,然后 ubivol 删除卷、重建卷、写入数据,这个过程是正常的
- 要写入的分区已经进行了 ubiattach,则 swupdate 运行时自动进行 ubiattach,发现已经 attach 了就忽略默认成功,读取当前 ubi 卷的信息,然后 ubivol 删除卷、重建卷、写入数据,这个过程是依赖于当前 ubi 的卷信息是否还正确,如果在 swupdate 运行前有人 ubiattach 了并且错误操作了 ubi 卷,就有可能导致 ubivol 写入数据后重启就失效了
- 要写入的分区损坏了,swupdate 运行时自动进行 ubiattach 失败,后续的 ubivol 也无法成功
因此,我们的前处理关键是要检查出来分区是否有损坏,如有损坏要使用 ubiformat 工具重新格式化,然后还要保证 ubivol 操作之前这个分区是刚刚 ubiattach 的。操作上的方案就是在前处理中先强制 ubidetach 再 ubiattach,这里的情况就有:
- swupdate 之前分区没有进行 ubiattach,则 swupdate 运行时自动进行了 ubiattach,那么增加 ubidetach + ubiattach 的操作之后还是一样的状态,完全没有风险
- swupdate 之前分区已经进行了 ubiattach,则 swupdate 运行时自动进行的 ubiattach 会忽略默认成功,那么增加 ubidetach + ubiattach 的操作会调整为刚刚 ubiattach 的状态;同时,如果这一步 ubidetach 或者 ubiattach 出错了,说明分区是有损坏的,需要使用 ubiformat 进行修复
- swupdate 运行时自动进行 ubiattach 失败,则 ubidetach 会出错,直接进行 ubiformat 修复;这个时候后续的 ubivol 是不会成功的,需要重新运行 swupdate 走自动 ubiattach 的流程
Embedded script 示例如下:
embedded-script =
"function is_mtd_in_use(mtd_num)\n"
" local mounts = io.open(\"/proc/mounts\", \"r\")\n"
" if not mounts then\n"
" swupdate.error(\"Failed to open /proc/mounts\")\n"
" return false\n"
" end\n"
" for line in mounts:lines() do\n"
" local dev, _, fstype = line:match(\"^(%S+)%s+(%S+)%s+(%S+)\")\n"
" if dev and fstype == \"ubifs\" then\n"
" local ubi_num = dev:match(\"/dev/ubi(%d+)_%d+\")\n"
" if ubi_num then\n"
" local mtd_file = io.open(\"/sys/class/ubi/ubi\" .. ubi_num .. \"/mtd_num\", \"r\")\n"
" if mtd_file then\n"
" local mtd_num_in_use = mtd_file:read(\"*line\")\n"
" mtd_file:close()\n"
" if mtd_num_in_use == tostring(mtd_num) then\n"
" mounts:close()\n"
" return true\n"
" end\n"
" end\n"
" end\n"
" end\n"
" end\n"
" mounts:close()\n"
" return false\n"
"end\n"
"\n"
"local _, mode = swupdate.get_selection()\n"
"if not mode then\n"
" swupdate.error(\"No mode selected\")\n"
" return\n"
"end\n"
"local target_mtd = nil\n"
"if mode == \"nowa_nextb\" then\n"
" target_mtd = 12\n"
"elseif mode == \"nowb_nexta\" then\n"
" target_mtd = 11\n"
"end\n"
"if not target_mtd then\n"
" return\n"
"end\n"
"local mtd_dev = \"/dev/mtd\" .. target_mtd\n"
"swupdate.info(\"Checking if \" .. mtd_dev .. \" is in use\")\n"
"if is_mtd_in_use(target_mtd) then\n"
" swupdate.error(\"Cannot update \" .. mode .. \" because \" .. mtd_dev .. \" is in use\")\n"
" os.exit(1)\n"
"end\n"
"if not os.execute(\"sleep 1 && ubidetach -p \" .. mtd_dev .. \" && ubiattach -p \" .. mtd_dev) then\n"
" swupdate.warn(mtd_dev .. \" is corrupted, repairing...\")\n"
" if not os.execute(\"ubiformat -y \" .. mtd_dev) then\n"
" swupdate.error(\"Failed to format \" .. mtd_dev)\n"
" os.exit(1)\n"
" end\n"
" swupdate.warn(mtd_dev .. \" repaired, might need to re-run swupdate\")\n"
"end\n"
"swupdate.info(\"Protection passed\")\n";
后处理
后处理也是一样的道理,需要用 ubidetach + ubiattach 的方式来确保重启后 ubi 文件系统还是正常的,不然就要用 ubiformat 修复并且要标记升级失败。还有一点需要处理的是,如果这个分区需要可写,要使用 ubirsvol 工具来进行容量扩展。最终调用 ubidetach 确保分区不在加载状态,以免再进行不当的操作(同时也可以再次运行 swupdate 来重新进行升级)。后处理脚本示例如下:
APP_MTD_NUM="$1"
APP_PARTITION="$2"
APP_DISK="/dev/mtd${APP_MTD_NUM}"
sleep 1
if ubidetach -p "${APP_DISK}" && ubiattach -p "${APP_DISK}" -d "${APP_MTD_NUM}"; then
log_info "UBI device ${APP_DISK} detached and re-attached successfully."
else
ubiformat -y "${APP_DISK}"
set_env_value app_upgrade_available 0
set_env_value app_bootcount 0
log_error "Failed to detach and re-attach UBI device ${APP_DISK}, fixed, need retry."
exit 1
fi
sleep 1
USED_BLOCKS=$(cat /sys/class/ubi/ubi${APP_MTD_NUM}_0/reserved_ebs)
AVAILABLE_BLOCKS=$(cat /sys/class/ubi/ubi${APP_MTD_NUM}/avail_eraseblocks)
ubirsvol /dev/ubi${APP_MTD_NUM} -n 0 -S $((USED_BLOCKS + AVAILABLE_BLOCKS))
ubidetach -p "${APP_DISK}"
set_env_value app_partition "${APP_PARTITION}"
set_env_value app_upgrade_available 1
set_env_value app_bootcount 0
log_info "Upgrade completed successfully, please reboot..."
总结
总的来说,流式安装省掉的是解压临时文件的空间开销,省不掉的是 flash 状态的保障:ubivol 只负责把镜像写进卷,分区是否干净、重启后能否正常加载,都要靠前处理和后处理来兜底。前处理用 embedded-script 把目标分区调整到刚刚 ubiattach 的干净状态,后处理用 postinstall 确认写入结果并设置好启动相关的环境变量,两头做扎实,这套方案在小容量 ubi 设备上就能稳定工作。
