背景
預設的 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
指令碼,很容易便可以移植到其他的環境中使用的。