路由器固件升级通常有两种方案:
对于双系统,直接写入到另外一个系统,然后切换到另外一个系统启动。这种方案是很安全的,因为升级的时候,另外一个系统是空闲状态,可以安全的写入。
对于单系统,情况要复杂很多,因为当前要升级的分区,已经挂载为root了,不能直接进行升级,否则进程运行可能会出现错误。可以将升级程序拷贝到tmpfs运行,这样升级程序可以安全运行。但是系统中已经运行的进程(可以杀死运行中的进程,但是至少1号进程不能被杀死)在缺页重新加载file的时候,就可能导致错误。虽然这种错误可能并不影响升级,但可能导致未知异常,所以并不安全。
对于单系统,openwrt的方案和上述方案差不多,openwrt会杀死大部分进程(init进程通常不会发生缺页),然后切换rootfs到tmpfs。openwrt会尝试卸载旧的rootfs. 但是使用lazy选项,也就是说,openwrt可能也不能完整的卸载旧的rootfs.
要安全的卸载一个mount,需要满足以下条件:
1 子目录没有其它mount
2 此mount上没有打开的文件描述符
3 此mount上没有正在运行的进程,或者共享库
4 此mount上没有目录是运行进程的working directory
1的要求是要umount所有的子目录mount point, 或者move到其它目录。
2 一般满足,守护进程一般不长时间打开文件rootfs的文件,因为rootfs是只读的,打开的主要是配置文件,打开就关闭了。
3 是关键要解决的问题,下面的方案一和方案二解决。
linux通过mmap的方式加载可执行程序和共享库到进程的地址空间:
/ # cat /proc/1/maps
00010000-000f4000 r-xp 00000000 00:0f 2 /bin/busybox
00104000-00105000 r--p 000e4000 00:0f 2 /bin/busybox
00105000-00106000 rw-p 000e5000 00:0f 2 /bin/busybox
00106000-00127000 rw-p 00000000 00:00 0 [heap]
76d60000-76eb6000 r-xp 00000000 00:0f 4 /lib/libc.so.6
76eb6000-76ec6000 ---p 00156000 00:0f 4 /lib/libc.so.6
76ec6000-76ec8000 r--p 00156000 00:0f 4 /lib/libc.so.6
76ec8000-76ec9000 rw-p 00158000 00:0f 4 /lib/libc.so.6
76ec9000-76ed3000 rw-p 00000000 00:00 0
查看进程的maps, 可以确认进程使用了哪些文件。
4 一般也可以解决,因为pivot_root的时候,内核会将所有运行进程的cwd切换到新rootfs的/
本文的目标是,在使用pivot_root切换rootfs到tmpfs后,完全的卸载旧的rootfs, 这样就可以安全的进行rootfs升级。
pivot_root切换到tmpfs后,rootfs要卸载的唯一障碍,就是有运行中的进程。最关键的就是init进程,其它进程都可以杀死,init进程不行。
但是init进程,可以替换自己,这是一个方案,切换到tmpfs后,给init发信号,init进程使用exec系统调用,用tmpfs中的init进程替换自己。这样就可以释放对rootfs的引用了。
这个方案的缺点,必须要杀死其它进程,比如hostapd。杀死了hostapd有个问题,就是无线掉线,如果用户在无线终端上发起升级,升级连接就断了。所以这个方案不好。
这个方案的目的是不杀死其它关键进程(如hostapd),这样可以保证,在升级的时候,网络是ok的。用户在升级页面可以看到所有的升级日志,以及升级进度。
不杀死就能umount rootfs, 意味着那些进程不能使用rootfs中的可执行文件和共享库。因此,我们在执行init的时候,将这些需要保留的进程以及依赖的共享库,拷贝到tmpfs, 这样程序就完全使用tmpfs中的文件,不依赖rootfs了。
rootfs目录设计如下:
bin/
├── busybox -> ../ram/busybox
lib/
├── ld-linux-armhf.so.3 -> ../ram/ld-linux-armhf.so.3
├── libc.so.6 -> ../ram/libc.so.6
├── libm.so.6 -> ../ram/libm.so.6
├── libresolv.so.2 -> ../ram/libresolv.so.2
sbin/
├── init -> ../bin/busybox
preinit
ram/
├── busybox
├── ld-linux-armhf.so.3
├── libc.so.6
├── libm.so.6
└── libresolv.so.2
ram一开始还是存放在rootfs的,所以我们需要在执行init进程之前将ram拷贝到tmpfs, 然后在将tmpfs bind mount到/ram。否则init进程运行的时候,使用的还是rootfs的ram.
修改内核配置:
CONFIG_CMDLINE="init=/preinit ubi.mtd=rootfs ubi.block=0,ubi_rootfs root=/dev/ubiblock0_1 rootwait"
将init路径修改为/preinit
/preinit是一个sh脚本,内容如下:
1 2 3 4 5 6 7 8 | #!/bin/sh # copy ram to ramfs mount -t tmpfs tmpfs /tmp cp -a /ram/ . /tmp/ mount --move /tmp /ram exec /sbin/init |
preinit刚运行的时候,使用的还是rootfs的ram目录里面的可执行文件和共享库,还是依赖rootfs的。但是preinit将ram拷贝到tmpfs,
然后挂载到/ram. 在exec /sbin/init替换自己,此时的/sbin/init已经指向tmpfs中的busybox了。这样init进程就不依赖rootfs了。
脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | change_root () { mount -o bind $1 $1 mkdir $1/$2 $1 /proc $1 /sys $1 /dev $1 /ram $1 /run $1 /tmp mount --bind /ram $1 /ram cd / pivot_root $1 $1/$2 || { echo "privot root failed" 1>&2 umount -l $1 $1 return 1 } mount --move /$2 /proc /proc mount --move /$2 /sys /sys mount --move /$2 /dev /dev mount --move /$2 /tmp /tmp mount --move /$2 /run /run mount --move /$2 /ram /ram umount /$2 } RAM_ROOT= /tmp/upgrade_root PUT_OLD=old-root prepare_ramfs $RAM_ROOT change_root $RAM_ROOT $PUT_OLD |
prepare_ramfs将可执行文件和共享库拷贝到RAM_ROOT.
mount -o bind $1 $1
这一命令的目的是满足pivot_root的要求:
new_root must be a path to a mount point, but can't be "/". A path that is not already a mount point can be converted into one by bind mounting the path onto itself.
pivot_root后面的一堆move, 是为了满足rootfs的子目录没有挂载点,这样就可以卸载rootfs了。
最终的结果:
/ # mount
devtmpfs on /dev type devtmpfs (rw,relatime,size=89324k,nr_inodes=22331,mode=755)
tmpfs on /ram type tmpfs (rw,relatime)
proc on /proc type proc (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
sysfs on /sys type sysfs (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
none on /tmp type tmpfs (rw,nosuid,nodev,noatime,size=65536k)
none on /run type tmpfs (rw,nosuid,nodev,noatime,size=4096k)
none on / type tmpfs (rw,nosuid,nodev,noatime,size=65536k)
tmpfs on /ram type tmpfs (rw,relatime)
有2个注意点:
/ram先是bind mount到新rootfs的/ram。这样新旧rootfs的ram就ok了。后面将旧rootfs的/ram move到新/ram, 这样是为了保证旧rootfs的/ram没有挂载点,这里move到其它目录也是可以的。
第一个旧rootfs的tmpfs又移动到新rootfs的tmp目录。相当于把自己挂载到自己的子目录。这样也可以~
延伸思考:
第二方案,好是好,可以是生成rootfs的时候,就比较麻烦,需要将文件拷贝到/ram, 并在/bin /lib等目录建立符号链接。可以不可以直接按照原来的目录结构,直接安装到/ram呢?比如
/ram
bin/busybox
lib/libc.so.6
然后,将/ram/bin加入到PATH环境变量,将/ram/lib加入到LD_LIBRARY_PATH. 但是这样做也麻烦。
可以将/ram拷贝到tmpfs, 然后tmpfs overlay挂载到原来的rootfs. 这样就合并了,不需要修改PATH环境变量。
但是这样的话,进程打开了overlay的文件,overlay不能umount, 那rootfs也不能umount了。此方案行不通。