专栏首页QB杂货铺基于 ramfs 进行 OTA
原创

基于 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 脚本,很容易便可以移植到其他的环境中使用的。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用littlefs-fuse在PC端调试littlefs文件系统

    littlefs是arm面向嵌入式设备推出的一款掉电安全的小型文件系统,具有抗掉电,动态磨损均衡,RAM/ROM需求少等特点,具体介绍可见 https://gi...

    zqb_all
  • 记一个bootloader的cache问题

    最近往一个armv7板子的bootloader中移植了解压算法,移植本身还比较顺利,但移植完了发现,功能是正常的,但效率大打折扣。解压同样的数据,耗时大约是ub...

    zqb_all
  • ubuntu下mediawiki的使用

    https://apps.ubuntu.com/cat/applications/libreoffice-wiki-publisher/

    zqb_all
  • 并不是所有SAP产品的UX,都得遵循Fiori UX风格

    一个朋友提问:求教Commerce 大拿:Spartacus 与 Commerce UX Design - Fiori 之间有什么联系吗?比如 Spartacu...

    Jerry Wang
  • 【进阶篇】安装与编译C-API预测库

    编写|PaddlePaddle 排版|wangp 1 概述 使用 C-API 进行预测依赖于将 PaddlePaddle 核心代码编译成链接库,只需在编译时...

    用户1386409
  • AI行业实践精选:机器学习在Google的昨天,今天与明天

    【AI100 导读】你知道吗?早在十多年前,Google 就已经在内部教自己的工程师机器学习的相关知识了。本篇文章将告诉你 Google 是如何使用机器学习来不...

    AI科技大本营
  • Google为何能在机器学习领域始终居霸主地位?

    这不是武侠世界——她已经做到了。26岁的Holgate得到了第二条跆拳道黑带。这次是算法的黑带。Holgate花费数个星期沉浸于一个程序中,这次比肉搏更...

    智能算法
  • Mac和Xcode常用的快捷键

    Mac电脑一般都不怎么用鼠标,因此除了触摸屏的各种双指、三指甚至四指的操作之外,快捷键的使用可以带来非常大的便利,本文则主要收集整理了自己在Mac常规和Xcod...

    mukekeheart
  • 谷歌打造自己的机器学习大军

    作为机器学习的一种,打败了李世石的AlphaGo让更多的人认识了“深度学习”。 ? 作为机器学习的一种,打败了李世石的AlphaGo让更多的人认识了“深度学习”...

    昱良
  • 设计模式(二) | 装饰模式---穿什么有这么重要?

    谭庆波

扫码关注云+社区

领取腾讯云代金券