ILD

pivot_root完全切换rootfs到tmpfs方案设计
作者:Yuan Jianpeng 邮箱:yuanjp89@163.com
发布时间:2022-10-10 站点:Inside Linux Development

路由器固件升级通常有两种方案:


对于双系统,直接写入到另外一个系统,然后切换到另外一个系统启动。这种方案是很安全的,因为升级的时候,另外一个系统是空闲状态,可以安全的写入。


对于单系统,情况要复杂很多,因为当前要升级的分区,已经挂载为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了。


切换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了。此方案行不通。


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.