基于 ramfs 的 OTA
背景
默认的 OTA
方案是基于 recovery
系统完成的。某个产品考虑产品形态和 flash
容量之后,计划去掉 recovery
系统(不考虑掉电安全),这就需要 OTA
方案能支持在只有单个系统的情况下完成升级动作。
默认的 recovery 系统方式
先介绍下默认使用的基于 recovery
系统的升级方式。
主系统由内核和根文件系统组成,分别保存在 flash
上的 kenrel
和 rootfs
分区。另外设置一个 recovery
分区,用于保存 recovery
系统。
此处的 recovery
系统,是一个带 initramfs
的内核,OTA
所需的应用和库都包含在 initramfs
中,因此启动到 recovery
系统之后,可不再依赖 flash
上的其他分区。
当需要进行系统升级时,先设置标志并重启,bootloader
检测到标志后会启动进入 recovery
系统。在 recovery
系统中,kernel
和 rootfs
分区都是处于未使用状态,直接将新的数据写入分区中即可。
更新完主系统之后,设置标志,重启到新的主系统即可。
没有 recovery 带来的问题
系统默认是将 flash
上的 rootfs
分区挂载为根文件系统,即系统运行时随时都可能会读写 rootfs
分区的数据。
若 OTA
不重启到 recovery
系统中,直接在正常系统中,即在 rootfs
分区仍被挂载为根文件系统的情况下,直接从块设备接口将数据写入 rootfs
分区,会有概率导致系统崩溃。
毕竟 OTA
应用和库本身都是放在 rootfs
中的,系统其他活跃进程也随时有可能对文件系统发出请求。
基于 initramfs 的解决方式
问题很明确,不能再挂载着rootfs的时候更新 rootfs
,那先考虑下,在挂载 rootfs
之前进行OTA
。
原本的内核是直接在内核初始化之后挂载 flash
上的 rootfs
分区作为根文件系统。现在 recovery
系统没了,但我们可以借鉴 recovery
系统的形式,为这个内核加上 initramfs
,在其中包含 OTA
所需的程序。
存在initramfs
的情况下,启动时内核会先挂载 initramfs
并执行 rdinit
指定的程序,到了 initramfs
的 init
脚本中,就可以判断是正常启动还是 OTA
了,若为正常启动则直接挂载 rootfs
分区,并进行根文件系统切换,后续的流程就跟原方案的主系统启动流程一致了。
若判断到正在进行 OTA
,则转而执行 OTA
流程,将新的数据写入 kernel
和 rootfs
分区,此时的环境跟原方案的 recovery
系统是一样的。
这种方案的优点是跟之前的流程较为类似,可复用一些成果。缺点是内核带上 initramfs
之后,不可避免地体积会变大,启动时间会变长。
关于标志传递
如何告知 initramfs
中的启动脚本,当前需要进行 OTA
呢?
方式一:通过自定义分区传递标志,在 flash
上的划定某个分区,例如划定一个 misc
分区,约定好标志,OTA
时更新其中的标志即可
方式二:通过 uboot
的 env
分区传递标志,uboot
原生提供了可以在 linux
用户空间读写 env
分区的应用,编译后使用 fw_printenv
和 fw_setenv
应用即可。详见 uboot
文档。
方式三:通过cmdline
传递标志,initramfs
可直接读取方式一和二设置的标志,也可以请 bootloader
约定好,由bootloader
检测到方式一和二设置的标志后,修改传递给 kernel
的 cmdline
方式四:通过芯片提供的寄存器传递标志。例如某些芯片的 RTC
模块中,会预留一些寄存器,供用户自定义使用,不掉电重启数据是不会丢的。
基于临时 ramfs 的解决方式
initramfs
是在挂载 rootfs
之前进行 OTA
,那有没有办法在挂载 rootfs
之后进行 OTA
呢?也是有的,先把 rootfs
分区卸载掉就可以了。
当然,直接 umount
是不行的,rootfs
分区现在还是尊贵的根文件系统,要想卸载,就得先切换到另一个根文件系统去。那另外的根文件系统从何而来呢?没有现成的,但可以造!
我们看看 openwrt
如何做的。切换根文件之前,先调用 kill_remaining
函数 kill
掉无关进程,这样可以让构造的 ramfs
只需包含 OTA
所需的应用和库。
kill_remaining() { # [ <signal> [ <loop> ] ]
local loop_limit=10
local sig="${1:-TERM}"
local loop="${2:-0}"
local run=true
local stat
local proc_ppid=$(cut -d' ' -f4 /proc/$$/stat)
echo -n "Sending $sig to remaining processes ... "
while $run; do
run=false
for stat in /proc/[0-9]*/stat; do
[ -f "$stat" ] || continue
local pid name state ppid rest
read pid name state ppid rest < $stat
name="${name#(}"; name="${name%)}"
# Skip PID1, our parent, ourself and our children
[ $pid -ne 1 -a $pid -ne $proc_ppid -a $pid -ne $$ -a $ppid -ne $$ ] || continue
local cmdline
read cmdline < /proc/$pid/cmdline
# Skip kernel threads
[ -n "$cmdline" ] || continue
echo -n "$name "
kill -$sig $pid 2>/dev/null
[ $loop -eq 1 ] && run=true
done
let loop_limit--
[ $loop_limit -eq 0 ] && {
echo
echo "Failed to kill all processes."
exit 1
}
done
echo
}
然后拷贝所需文件到 ram
中,构造出所需的 ramfs
switch_to_ramfs() {
# 将一些基础文件拷贝到ram中,构造ramfs
for binary in \
/bin/busybox /bin/ash /bin/sh /bin/mount /bin/umount \
pivot_root mount_root reboot sync kill sleep \
md5sum hexdump cat zcat bzcat dd tar \
ls basename find cp mv rm mkdir rmdir mknod touch chmod \
'[' printf wc grep awk sed cut \
mtd partx losetup mkfs.ext4 nandwrite flash_erase \
ubiupdatevol ubiattach ubiblock ubiformat \
ubidetach ubirsvol ubirmvol ubimkvol \
snapshot snapshot_tool \
# 除了上面列出来的,还可以将自定义的一些文件赋值到 $RAMFS_COPY_BIN 中,这样就无需改动官方的这份文件
$RAMFS_COPY_BIN
do
local file="$(which "$binary" 2>/dev/null)"
[ -n "$file" ] && install_bin "$file"
done
install_file /etc/resolv.conf /lib/*.sh /lib/functions/*.sh /lib/upgrade/*.sh /lib/upgrade/do_stage2 /usr/share/libubox/jshn.sh $RAMFS_COPY_DATA
[ -L "/lib64" ] && ln -s /lib $RAM_ROOT/lib64
接着进行关键的根文件系统切换
supivot $RAM_ROOT /mnt || {
echo "Failed to switch over to ramfs. Please reboot."
exit 1
}
切换后收个尾
#原本的根文件系统,变成挂载在 /mnt 下,现在可以卸载掉
/bin/mount -o remount,ro /mnt
/bin/umount -l /mnt
grep /overlay /proc/mounts > /dev/null && {
/bin/mount -o noatime,remount,ro /overlay
/bin/umount -l /overlay
}
}
最后在 ramfs
中调用真正的 OTA
命令
# Exec new shell from ramfs
exec /bin/busybox ash -c "$COMMAND"
这种做法的好处是,避免了 intiramfs
带来的体积和启动速度问题,且 OTA
过程只有一次重启。
更具体请参考 openwrt
官方的升级脚本(旧版本搜索run_ramfs
,新版本搜索 switch_to_ramfs
)。
毕竟是 shell
脚本,很容易便可以移植到其他的环境中使用的。