基於 ramfs 的 OTA

背景

默認的 OTA 方案是基於 recovery 系統完成的。某個產品考慮產品形態和 flash 容量以後,計劃去掉 recovery 系統(不考慮掉電安全),這就須要 OTA 方案能支持在只有單個系統的狀況下完成升級動做。linux

默認的 recovery 系統方式

先介紹下默認使用的基於 recovery 系統的升級方式。shell

主系統由內核和根文件系統組成,分別保存在 flash 上的 kenrelrootfs 分區。另外設置一個 recovery 分區,用於保存 recovery 系統。安全

此處的 recovery 系統,是一個帶 initramfs 的內核,OTA 所需的應用和庫都包含在 initramfs 中,所以啓動到 recovery 系統以後,可再也不依賴 flash 上的其餘分區。ide

當須要進行系統升級時,先設置標誌並重啓,bootloader 檢測到標誌後會啓動進入 recovery系統。在 recovery 系統中,kernelrootfs 分區都是處於未使用狀態,直接將新的數據寫入分區中便可。函數

更新完主系統以後,設置標誌,重啓到新的主系統便可。oop

沒有 recovery 帶來的問題

系統默認是將 flash 上的 rootfs 分區掛載爲根文件系統,即系統運行時隨時均可能會讀寫 rootfs 分區的數據。rest

OTA 不重啓到 recovery 系統中,直接在正常系統中,即在 rootfs 分區仍被掛載爲根文件系統的狀況下,直接從塊設備接口將數據寫入 rootfs 分區,會有機率致使系統崩潰。code

畢竟 OTA 應用和庫自己都是放在 rootfs 中的,系統其餘活躍進程也隨時有可能對文件系統發出請求。orm

基於 initramfs 的解決方式

問題很明確,不能再掛載着rootfs的時候更新 rootfs,那先考慮下,在掛載 rootfs 以前進行OTA接口

本來的內核是直接在內核初始化以後掛載 flash 上的 rootfs 分區做爲根文件系統。如今 recovery 系統沒了,但咱們能夠借鑑 recovery 系統的形式,爲這個內核加上 initramfs,在其中包含 OTA 所需的程序。

存在initramfs的狀況下,啓動時內核會先掛載 initramfs 並執行 rdinit 指定的程序,到了 initramfsinit 腳本中,就能夠判斷是正常啓動仍是 OTA 了,若爲正常啓動則直接掛載 rootfs 分區,並進行根文件系統切換,後續的流程就跟原方案的主系統啓動流程一致了。

若判斷到正在進行 OTA,則轉而執行 OTA 流程,將新的數據寫入 kernelrootfs 分區,此時的環境跟原方案的 recovery 系統是同樣的。

這種方案的優勢是跟以前的流程較爲相似,可複用一些成果。缺點是內核帶上 initramfs 以後,不可避免地體積會變大,啓動時間會變長。

關於標誌傳遞

如何告知 initramfs 中的啓動腳本,當前須要進行 OTA 呢?

方式一:經過自定義分區傳遞標誌,在 flash 上的劃定某個分區,例如劃定一個 misc 分區,約定好標誌,OTA 時更新其中的標誌便可

方式二:經過 ubootenv 分區傳遞標誌,uboot 原生提供了能夠在 linux 用戶空間讀寫 env 分區的應用,編譯後使用 fw_printenvfw_setenv 應用便可。詳見 uboot 文檔。

方式三:經過cmdline傳遞標誌,initramfs可直接讀取方式一和二設置的標誌,也能夠請 bootloader 約定好,由bootloader檢測到方式一和二設置的標誌後,修改傳遞給 kernelcmdline

方式四:經過芯片提供的寄存器傳遞標誌。例如某些芯片的 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 腳本,很容易即可以移植到其餘的環境中使用的。

相關文章
相關標籤/搜索