zhangguanzhang's Blog

openwrt 的在线升级固件和扩容的研究

字数统计: 3.3k阅读时长: 15 min
2021/12/19

前言

手上有 r2s、N1 和 x86_64 的固件维护,r2s 的参照别人的脚本搞了在线升级固件的脚本,别人的脚本只支持 ext4 升级,而后面我也把 squashfs 格式的固件升级搞出来了。恩山上有的人的固件我也看 x86_64 也可以在线升级,后面我也会去测下 x86_64 的,理论上是通用的。

升级过程

以 r2s 为例讲解。参照目前看到的的 1988 的升级脚本 ,最初的人不知道是谁搞的在线升级,因为很久之前就看到有些人的固件能在线升级了。

升级前准备

相关命令

确保固件有下面命令:

command package name 用途
parted parted 修改分区和获取分区信息
losetup losetup loop device 命令,用于挂载固件里的文件分区
resize2fs resize2fs resize ext4 需要
truncate coreutils-truncate 填充和清空文件,这里是填充扩容
curl curl 下载,以及http 调用一些 api
wget wget 下载命令
mksquashfs squashfs-tools-mksquashfs squashfs格式需要
unsquashfs squashfs-tools-unsquashfs squashfs格式需要

KERNEL_PARTSIZE 和 ROOTFS_PARTSIZE

CONFIG_TARGET_KERNEL_PARTSIZECONFIG_TARGET_ROOTFS_PARTSIZE.config 文件里的,单位是 M,前者是类似常规大型 linux os 里的 /boot 分区,openwrt 默认就只有这两个分区。

r2s 的话 KERNEL_PARTSIZE 一般 12M 就够用了,但是很多网上互相抄的人在 r2s 的 .config 里给 32、64 之类的非常浪费。ROOTFS_PARTSIZE 是最终的根分区大小,给小了因为编译带很多插件,导致最终的打包镜像容量不够,我的固件是 635。然后 r2s 是内存卡,一般现在内存大大小都是 4G 以上,也就是刷完固件后,根分区就是 635M ,卡的剩下空间都没使用,当然,x86_64 也是一样的问题。所以就有了这个升级顺带扩容的步骤。

提前的容量存储新固件

1988 的固件非常小,下载 300M,从一个新手初次尝试来说,很可能尴尬的情况就是卡刷后 rootfs 是 600M,然后可用就200M,固件压缩后350M,所以我固件在初次扩容升级会暂时新建一个分区,用于存储下载升级的固件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 一般很多固件 /opt 单独挂载,或者属于 / ,所以如果已经在升级阶段扩容了,固件就存 /opt下,没扩容过,就存挂载点 /tmp/update/download
if [ $(df -m /opt | awk 'NR==2{print $4}') -lt 2400 ];then
NEED_GROW=1
mkdir -p /tmp/update/download
warning '检测到当前未扩容,先借用初版固件扩容,后续请再执行升级脚本'
df -h
parted /dev/$block_device p
# 该文件存 part_num ,防止机器重启后重复新建了分区表
if [ ! -f '/opt/.parted' ];then
start_sec=$(parted /dev/$block_device unit s print free | awk '$1~"s"{a=$1}END{print a}')
parted /dev/$block_device mkpart p ext4 ${start_sec} 4G
part_num=$( parted /dev/$block_device p | awk '$5=="primary"{a=$1}END{print a}' )
sleep 3 # 此处会自动挂载造成蛋疼
if grep -E /dev/${block_device}p${part_num} /proc/mounts;then
if mountpoint -q /mnt/${block_device}p${part_num};then
touch /mnt/${block_device}p${part_num}/test &>/dev/null || NEED_MKFS=1
umount /mnt/${block_device}p${part_num}
fi
[ -n "$NEED_MKFS" ] && mkfs.ext4 -F /dev/${block_device}p${part_num}
else
mkfs.ext4 -F /dev/${block_device}p${part_num}
fi
echo ${part_num} > /opt/.parted
else
part_num=$(cat /opt/.parted)
fi
mountpoint -q /tmp/update/download || mount /dev/${block_device}p${part_num} /tmp/update/download
USER_FILE=/tmp/update/download/openwrt.img.gz
rm -f ${USER_FILE}
# 因为初次没扩容,我的固件是存放在 docker 镜像里的,可能拉取 docker 镜像就容量满了,所以我单独有个release 存放编译好的固件,直接下载,用于初次
wget https://ghproxy.com/https://github.com/zhangguanzhang/Actions-OpenWrt/releases/download/fs/openwrt-rockchip-armv8-friendlyarm_nanopi-r2s-ext4-sysupgrade.img.gz -O ${USER_FILE}
fi

对于后面的下载新版本固件,1988 的脚本我看他是 github action 每天定时编译发布存 release,感觉后面他可能会被 github 给 ban了。 我脚本里是存 docker hub 的镜像里,我的固件都自带 docker,docker 拉取镜像后提取镜像文件:

1
2
3
4
5
6
cat >${BUILD_DIR}/Dockerfile << EOF
FROM alpine
LABEL FILE=$file
LABEL NUM=${GITHUB_RUN_NUMBER}
COPY * /
EOF

github action 上利用 buildx 构建存储这个镜像,用 LABEL 指定文件路径名,直接 copy 出来:

1
2
3
4
5
6
docker pull zhangguanzhang/r2s:${VER}
CTR_PATH=$( docker inspect zhangguanzhang/r2s:${VER} --format '{{ .Config.Labels }}' | grep -Eo 'openwrt-.+img.gz' )
docker create --name update zhangguanzhang/r2s:${VER}
docker cp update:/${CTR_PATH} ${USER_FILE}
docker rm update
docker rmi zhangguanzhang/r2s:${VER}

扩容和升级

固件分为两个文件系统,SquashFS 和 Ext4。

SquashFS(推荐):固件文件名带有 “squashfs”,SquashFS 为只读文件系统,支持系统还原(支持物理 Reset按钮 还原),支持后台固件升级,更能避免 SD 卡文件系统触发写保护,适合绝大部分用户使用。

Ext4:固件文件名带有 “ext4”,Ext4 文件系统具备整个分区可读写性质,更适合熟悉 Linux 系统的用户使用,但意外断电有几率造成分区写入保护。

ext4

前面两个章节是下载和存放固件 img.gz ,现在开始扩容和升级,扩容就是利用 truncate 下固件文件的大小,然后修复固件文件里的第二个分区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 因为最终会把修改后的固件写入到根分区所在的块设备,所以固件需要存放在 /tmp 目录下
# 大小和内存挂钩,所以不要size太大
mount -t tmpfs -o remount,size=870m tmpfs /tmp
# 后面的 true 是因为 github action 的打包会影响解压,虽然最终报错,但是解压的固件还是能用的
gzip -dc openwrt.img.gz > /tmp/update/openwrt.img || true

block_device='mmcblk0'
[ ! -d /sys/block/$block_device ] && block_device='mmcblk1'
bs=`expr $(cat /sys/block/$block_device/size) \* 512`
# 修改文件大小
truncate -s $bs /tmp/update/openwrt.img
# 修改第二个分区大小,1988用的是 echo ", +" | sfdisk -N 2 /tmp/update/openwrt.img 可读性不好
parted /tmp/update/openwrt.img resizepart 2 100%

# 将镜像文件虚拟成块设备,类似于 windows 的那种双击 iso 后的装载 iso ,对于块设备的操作都会时刻写入到 img 文件里
lodev=$(losetup -f)
losetup -P $lodev /tmp/update/openwrt.img

# 挂载 rootfs 解压备份文件
mkdir -p /mnt/img
mount -t ext4 ${lodev}p2 /mnt/img
# op 的备份命令
sysupgrade -b back.tar.gz
tar zxf back.tar.gz -C /mnt/img
if ! grep -q macaddr /etc/config/network; then
warning '注意:由于已知的问题,“网络接口”配置无法继承,重启后需要重新设置WAN拨号和LAN网段信息'
rm /mnt/img/etc/config/network;
fi
mountpoint -q /mnt/img && umount /mnt/img
# openwrt 存在 auto mount,此处取消挂载
grep -q ${lodev}p1 /proc/mounts && umount ${lodev}p1
grep -q ${lodev}p2 /proc/mounts && umount ${lodev}p2
# 修复固件里扩容的分区
e2fsck -yf ${lodev}p2 || true
resize2fs ${lodev}p2
# 取消 img 文件的挂载
losetup -d $lodev

echo 1 > /proc/sys/kernel/sysrq
echo u > /proc/sysrq-trigger && umount / || true
# 这个 ddnz 命令从他那里复制的
/tmp/ddnz /tmp/update/openwrt.img /dev/$block_device
printf '%b\n' "\033[1;32m[SUCCESS] 刷机完毕,正在重启...\033[0m"
# 重启
echo b > /proc/sysrq-trigger

squashfs

openwrt 的另一种文件系统固件,就是一个只可读写的压缩的 rootfs 解压开作为 overlay 的 lower dir 只读,提供给用户的是 overlay 的 upper dir 去写入,长按设备上的 reset 按钮恢复出厂设置就是把 overlay 的上层丢弃掉,所以 squashfs 类型的固件带快照功能。当然市面上搜了下也没找到 squashfs 类型的固件在升级的时候扩容的步骤,自己研究了下搞出来了。

1
2
3
4
5
6
7
# 挂载 rootfs 解压备份文件
mkdir -p /mnt/img
# 这里会报错 wrong fs type
# mount -t ext4 ${lodev}p2 /mnt/img
# 被我改成这样
mount ${lodev}p2 /mnt/img
IMG_FSTYPE=$(df -T /mnt/img | awk 'NR==2{print $2}')

取到了 IMG_FSTYPE 后走不同的逻辑,这里它的值是 squashfs ,而挂载后的 /mnt/img 是无法写入任何文件的。然后搜了下 squashfs 相关,自己折腾的话需要 mksquashfsunsquashfs 的两个命令玩。一开始是尝试解压 ${lodev}p2 ,结果经常 oom ,去找 squashfs-tools 源码作者询问如何限制内存得到下面信息。

1
2
# https://github.com/plougher/squashfs-tools/issues/139#issuecomment-991779738
unsquashfs -da 10 -fr 10 ${lodev}p2

基本一直卡着,毕竟最后肯定要重新 mksquashfs 打包的,然后直接 cp 得了:

1
2
3
4
5
6
7
8
9
10
11
if [ "$IMG_FSTYPE" = 'squashfs' ];then
info "检测到使用 squashfs 固件,开始导出文件系统"
# https://github.com/plougher/squashfs-tools/issues/139#issuecomment-991779738
# unsquashfs -da 10 -fr 10 /dev/loop0p2
# 这个解压太耗时了,只能拷贝整了
mkdir -p /mnt/img_sq
cp -a /mnt/img/* /mnt/img_sq
umount /mnt/img/
rm -rf /mnt/img
mv /mnt/img_sq /mnt/img
fi

然后这个目录写入备份文件,然后就打包:

1
mksquashfs /mnt/img /opt/op.squashfs

结果打包也经常 oom ,看了下命令的帮助,发现有内存限制的,加上也偶尔 oom

1
mksquashfs /mnt/img /opt/op.squashfs -mem 20M 

最后逼我用 oom_score_adj 调整 oom 优先级了

1
echo -998 > /proc/$$/oom_score_adj 2>/dev/null || true

当然,实际打包很多选项的,可以先利用 unsquashfs 看下

1
2
3
4
5
6
7
8
9
10
11
unsquashfs -s ${lodev}p2 > squashfs.info

comp=$(awk '$1=="Compression"{print $2}' squashfs.info)
sq_block_size=$(awk '$1=="Block"{print $NF}' squashfs.info)
xattrs='-xattrs' # CONFIG_SELINUX=y
grep -Eq 'Xattrs.+?not' squashfs.info && xattrs='-no-xattrs'

echo -998 > /proc/$$/oom_score_adj 2>/dev/null || true

mksquashfs /mnt/img /opt/op.squashfs -comp ${comp} \
-b $[sq_block_size/1024]k $xattrs -mem 20M

然后写入到块设备上

1
dd if=/opt/op.squashfs of=${lodev}p2

然后卸载 ${lodev} 刷入固件发现无法开机,在 lede 的源码里 find grep 后找到了 mksquashfs 参数来源于源码下 ./include/image.mkSQUASHFSOPTdefine Image/mkfs/squashfs-common

1
2
3
4
5
6
7
8
9
10
11
12
13
CONFIG_TARGET_SQUASHFS_BLOCK_SIZE=1024k
SQUASHFS_BLOCKSIZE := $(CONFIG_TARGET_SQUASHFS_BLOCK_SIZE)k
SQUASHFSOPT := -b $(SQUASHFS_BLOCKSIZE)
SQUASHFSOPT += -p '/dev d 755 0 0' -p '/dev/console c 600 0 0 5 1'
SQUASHFSOPT += $(if $(CONFIG_SELINUX),-xattrs,-no-xattrs)
SQUASHFSCOMP := gzip
LZMA_XZ_OPTIONS := -Xpreset 9 -Xe -Xlc 0 -Xlp 2 -Xpb 2
ifeq ($(CONFIG_SQUASHFS_XZ),y)
ifneq ($(filter arm x86 powerpc sparc,$(LINUX_KARCH)),)
BCJ_FILTER:=-Xbcj $(LINUX_KARCH) # 例如此处 -Xbcj x86
endif
SQUASHFSCOMP := xz $(LZMA_XZ_OPTIONS) $(BCJ_FILTER)
endif

本地搞个编译 openwrt 的时候 make 带上 -V=s 开详细信息看到下面信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ /home/guanzhang/lede/staging_dir/host/bin/mksquashfs4 /home/guanzhang/lede/build_dir/target-aarch64_generic_musl/root-rockchip /home/guanzhang/lede/build_dir/target-aarch64_generic_musl/linux-rockchip_armv8/root.squashfs -nopad -noappend -root-owned -comp xz -Xpreset 9 -Xe -Xlc 0 -Xlp 2 -Xpb 2  -b 1024k -p '/dev d 755 0 0' -p '/dev/console c 600 0 0 5 1' -no-xattrs -processors 6
Pseudo file "/dev" exists in source filesystem "/home/guanzhang/lede/build_dir/target-aarch64_generic_musl/root-rockchip/dev".
Ignoring, exclude it (-e/-ef) to override.
Parallel mksquashfs: Using 6 processors
Creating 4.0 filesystem on /home/guanzhang/lede/build_dir/target-aarch64_generic_musl/linux-rockchip_armv8/root.squashfs, block size 1048576.
[=============================================================-] 8430/8430 100%

Exportable Squashfs 4.0 filesystem, xz compressed, data block size 1048576
compressed data, compressed metadata, compressed fragments,
no xattrs, compressed ids
duplicates are removed
Filesystem size 135525.99 Kbytes (132.35 Mbytes)
25.39% of uncompressed filesystem size (533693.75 Kbytes)
Inode table size 60908 bytes (59.48 Kbytes)
20.00% of uncompressed inode table size (304503 bytes)
Directory table size 85796 bytes (83.79 Kbytes)
38.42% of uncompressed directory table size (223339 bytes)
Number of duplicate files found 1164
Number of inodes 9212
Number of files 8077
Number of fragments 123
Number of symbolic links 647
Number of device nodes 1
Number of fifo nodes 0
Number of socket nodes 0
Number of directories 487
Number of ids (unique uids + gids) 1
Number of uids 1
root (0)
Number of gids 1
root (0)

大致参数就是:

1
-nopad -noappend -root-owned -comp xz -Xpreset 9 -Xe -Xlc 0 -Xlp 2 -Xpb 2  -b 1024k -p '/dev d 755 0 0' -p '/dev/console c 600 0 0 5 1' -no-xattrs -processors 6

但是 openwrt 和 Centos 上安装的 squashfs-tools 的 mksquashfs xz 压缩时候都没有 -Xpreset 9 -Xe -Xlc 0 -Xlp 2 -Xpb 2 这些参数,后面发现了是 openwrt 编译的时候下载 squashfs-tools 后打了 patch 编译后才有的,不过后面测试这几个选项不影响。同时看到了打包的脚本:

1
PADDING=1 /home/guanzhang/lede/scripts/gen_image_generic.sh /home/guanzhang/lede/build_dir/target-aarch64_generic_musl/linux-rockchip_armv8/tmp/openwrt-rockchip-armv8-friendlyarm_nanopi-r2s-squashfs-sysupgrade.img.gz 18 /home/guanzhang/lede/build_dir/target-aarch64_generic_musl/linux-rockchip_armv8/tmp/openwrt-rockchip-armv8-friendlyarm_nanopi-r2s-squashfs-sysupgrade.img.gz.boot 635 /home/guanzhang/lede/build_dir/target-aarch64_generic_musl/linux-rockchip_armv8/root.squashfs 32768

得到了参数:

1
2
3
4
5
+ dd if=/dev/zero of=/home/guanzhang/lede/build_dir/target-aarch64_generic_musl/linux-rockchip_armv8/tmp/openwrt-rockchip-armv8-friendlyarm_nanopi-r2s-squashfs-sysupgrade.img.gz bs=512 seek=131072 conv=notrunc count=1300480
1300480+0 records in
1300480+0 records out
665845760 bytes (666 MB, 635 MiB) copied, 2.12896 s, 313 MB/s
+ dd if=/home/guanzhang/lede/build_dir/target-aarch64_generic_musl/linux-rockchip_armv8/root.squashfs of=/home/guanzhang/lede/build_dir/target-aarch64_generic_musl/linux-rockchip_armv8/tmp/openwrt-rockchip-armv8-friendlyarm_nanopi-r2s-squashfs-sysupgrade.img.gz bs=512 seek=131072 conv=notrunc

可以看到是直接写 img 文件的,这里虽然显示的是 img.gz ,但是如果压缩后的文件的话,那 seek 大小就不对。实际上后面才压缩的,所以我的参数为

1
2
3
4
5
6
7
8
9
part2_seek=$(parted /tmp/update/openwrt.img u s p | awk '$1==2{print +$2}')
mksquashfs /mnt/img /opt/op.squashfs -nopad -noappend -root-owned \
-comp ${comp} ${LZMA_XZ_OPTIONS} \
-b $[sq_block_size/1024]k \
-p '/dev d 755 0 0' -p '/dev/console c 600 0 0 5 1' \
$xattrs -mem 20M

losetup -l -O NAME -n | grep -Eqw $lodev && losetup -d $lodev
dd if=/opt/op.squashfs of=/tmp/update/openwrt.img bs=512 seek=${part2_seek} conv=notrunc

然后写入即可

最终参考

我的脚本当前存放在 test 分支,可能后续切到 main 分支:

1
2
3
4
5

https://github.com/zhangguanzhang/Actions-OpenWrt/blob/test/build/scripts/update.sh

https://github.com/zhangguanzhang/Actions-OpenWrt/blob/main/build/scripts/update.sh

参考

CATALOG
  1. 1. 前言
  2. 2. 升级过程
    1. 2.1. 升级前准备
      1. 2.1.1. 相关命令
      2. 2.1.2. KERNEL_PARTSIZE 和 ROOTFS_PARTSIZE
      3. 2.1.3. 提前的容量存储新固件
    2. 2.2. 扩容和升级
      1. 2.2.1. ext4
      2. 2.2.2. squashfs
  3. 3. 最终参考
  4. 4. 参考