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