默認的 OTA
方案是基於 recovery
系統完成的。某個產品考慮產品形態和 flash
容量以後,計劃去掉 recovery
系統(不考慮掉電安全),這就須要 OTA
方案能支持在只有單個系統的狀況下完成升級動做。linux
先介紹下默認使用的基於 recovery
系統的升級方式。shell
主系統由內核和根文件系統組成,分別保存在 flash
上的 kenrel
和 rootfs
分區。另外設置一個 recovery
分區,用於保存 recovery
系統。安全
此處的 recovery
系統,是一個帶 initramfs
的內核,OTA
所需的應用和庫都包含在 initramfs
中,所以啓動到 recovery
系統以後,可再也不依賴 flash
上的其餘分區。ide
當須要進行系統升級時,先設置標誌並重啓,bootloader
檢測到標誌後會啓動進入 recovery
系統。在 recovery
系統中,kernel
和 rootfs
分區都是處於未使用狀態,直接將新的數據寫入分區中便可。函數
更新完主系統以後,設置標誌,重啓到新的主系統便可。oop
系統默認是將 flash
上的 rootfs
分區掛載爲根文件系統,即系統運行時隨時均可能會讀寫 rootfs
分區的數據。rest
若 OTA
不重啓到 recovery
系統中,直接在正常系統中,即在 rootfs
分區仍被掛載爲根文件系統的狀況下,直接從塊設備接口將數據寫入 rootfs
分區,會有機率致使系統崩潰。code
畢竟 OTA
應用和庫自己都是放在 rootfs
中的,系統其餘活躍進程也隨時有可能對文件系統發出請求。orm
問題很明確,不能再掛載着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
模塊中,會預留一些寄存器,供用戶自定義使用,不掉電重啓數據是不會丟的。
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
腳本,很容易即可以移植到其餘的環境中使用的。