ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

switch_root原理与实操:Linux启动流程中的根文件系统切换

switch_root原理与实操:Linux启动流程中的根文件系统切换 1. switch-root是干什么的启动流程里的“换地盘”动作1.1 从内核自举到initramfs为什么会有临时根文件系统很多刚接触Linux底层机制的朋友第一次看到switch_root这个命令时往往一脸懵根文件系统还能“切换”切来切去是为了什么这得从Linux内核的启动流程说起。计算机通电之后BIOS或者UEFI先把引导加载程序GRUB之类拉起来引导加载程序再把内核镜像vmlinuz载入内存。内核开始运行后的第一件事就是挂载根文件系统——但这时候问题就来了磁盘控制器驱动还没加载ext4、xfs这些文件系统驱动也不一定在内核里内核拿什么去读磁盘上的/呢解决方案就是initramfs。它是一个被打包进内核镜像的微型根文件系统里面塞了必要的驱动模块、udev规则、cryptsetup工具等等。内核启动后先把initramfs挂载成临时的/在这个“迷你系统”里完成驱动加载、设备节点创建、磁盘解锁等脏活累活最后把真实的磁盘根文件系统挂载好再“换挡”切到那个真正的根上继续启动。这个“换挡”的动作就是switch_root干的活。你可以把initramfs理解成装修前的临时板房工人先在板房里把工具备好、把图纸看明白等真正的大楼磁盘根文件系统能进场了再把家伙什搬进大楼把板房拆掉。switch_root就是那个负责“搬家伙拆板房”的工头。我在实际调试嵌入式Linux和定制发行版启动过程中反复跟这个命令打交道可以负责任地说搞明白switch_root你基本就打通了Linux启动流程的“最后一公里”。1.2 switch_root、pivot_root、chroot三者到底差在哪很多人把switch_root和chroot搞混甚至有人觉得switch_root就是把chroot封装了一下其实完全不是一回事。三者之间有本质区别我画个对比表就清楚了命令/调用本质对当前根的影响典型用途chroot改变某个进程的根目录视角不改变系统全局的根只影响特定进程及其子进程修复系统、容器隔离、构建环境pivot_root系统调用把当前根移到指定目录再把新根挂为根全局切换根文件系统但旧根不会自动清理容器运行时切换根、initramfs切换switch_root基于pivot_root思路的封装工具切换后清理旧根全局切换根文件系统并释放旧根占用内存initramfs切换到真实根文件系统chroot好理解它只是把进程的/换个指向旧根还在进程还能通过路径访问到。但switch_root要的是彻底“搬家”initramfs本身是一块内存文件系统tmpfs或rootfs如果不清理掉这块内存就白白占着。嵌入式设备内存本来就紧几百MB的initramfs残留能直接拖垮系统。pivot_root是内核提供的系统调用能把当前根和即将成为新根的目录互换位置。但直接用pivot_root很别扭因为它要求你把旧根挂到一个子目录切换后还得自己手动卸载旧根。switch_root把这些琐碎步骤全部封装好了检查新根、清理旧根里的残留文件、移动挂载点、chroot、执行init一步到位。所以一般我们在initramfs脚本里只会看到switch_root很少看到有人直接裸调pivot_root。2. 搞清楚原理再动手switch_root内部做了什么2.1 switch_root的执行路径既然switch_root这么方便那它内部到底做了什么我不建议你把它当黑盒因为它失败时的报错信息非常反直觉不懂原理根本无从排查。以busybox实现的switch_root为例它的执行流程大致是这样的校验参数必须提供两个参数第一个是新根挂载点路径第二个是init程序路径。确认新根是一个挂载点switch_root会检查新根目录的挂载点属性这一步报错非常常见后面我单独说。递归清理新根目录下的残留文件注意这一步不是清理旧的initramfs而是清理新根文件系统里不该有的临时文件比如mount point目录下的旧内容。把当前根也就是initramfs上所有挂载点全部移动到新根之上用的就是mount --move的机制把/proc、/sys、/dev这些从initramfs挂载的伪文件系统整个搬到新根对应的路径下。执行chroot到新根目录。执行指定的init程序通常是/sbin/init。这里最关键的是第4步“搬挂载点”。因为/proc、/sys这些伪文件系统是挂载在initramfs里的如果切换根之后不把它们挪走新根里的进程就访问不到/proc和/sys很多程序会直接崩溃系统根本起不来。switch_root的“搬挂载点”操作保证了你从initramfs过渡到新根时伪文件系统是无缝衔接的不需要也不应该在新根的init脚本里重新挂载一遍。另外要注意switch_root在执行到后期会二度清理initramfs中的文件。因为initramfs是rootfs类型的tmpfs占用的内存只有在所有引用它的进程都退出、文件都清空之后才会释放。switch_root在chroot后已经无法直接访问旧根了所以它会先把能找到的文件删掉再通过pivot_root的机制把旧根彻底解开实现内存释放。我自己在做过一次试验不清干净旧根就切根系统能起来但free -m看内存少了几十MB全被孤儿文件占着而且永远还不回来除非重启。所以别小看这个“拆板房”的动作。2.2 调用参数的前置条件switch_root的原型很简单就两个参数switch_root newroot init但这两个参数背后的前置条件才是真正的坑。我先列一下newroot必须是一个真实挂载点。也就是说你必须在调用switch_root之前先把目标根文件系统挂到某个目录上比如/newroot。很多人直接写switch_root /mnt/sda1 /sbin/init但/mnt/sda1根本没挂载或者只是普通目录这时候switch_root直接报错“newroot is not mountpoint”起不来。init参数必须是新根文件系统里的绝对路径。注意这个路径是在切根之后解析的不是切根之前。所以你得保证新根里确实存在这个文件而且有执行权限。常见错误是写成相对路径或者写了个只在initramfs里存在的init脚本切根后直接“No such file or directory”系统就卡死了。调用前必须清空不需要的挂载点。switch_root会把当前根下的所有挂载点移到新根但如果你在initramfs里挂载了额外的设备比如把U盘挂到了/usb切根后它会被挪到/newroot/usb。这通常不是你想要的结果所以切根前最好把临时挂载的设备都umount掉。调用前需要把必要的伪文件系统挂好。/proc、/sys、/dev必须在initramfs阶段先挂载switch_root才能帮你搬到新根。如果这些没挂切根后就算init跑起来系统也会因为缺少设备节点和内核接口而行为诡异比如mount警告找不到/proc/misc之类的。我把这些前置条件整理成一张速查卡片方便你写initramfs脚本时对照检查检查项要求失败后果newroot路径已挂载的真实设备文件系统报错“newroot is not mountpoint”init路径新根内存在的绝对路径可执行内核panic或系统卡死/proc /sys /dev已挂载切根后系统功能残缺临时设备挂载已卸载挂载点被搬到新根造成混乱当前目录可访问不要在已被卸载的目录里段错误或找不到新根3. 动手实操自己做一个initramfs体验switch_root3.1 准备工具和目录结构光说不练假把式。我带你亲手做一个最小可用的initramfs体验完整的switch_root切换过程。这个实验做完你对Linux启动机制的理解会上一个台阶。先说环境准备。我们需要一台Linux机器装上busybox和cpio工具。Debian/Ubuntu上直接sudo apt install busybox-static cpio如果你的发行版是yum系对应的是busybox和cpio两个包。这里建议用busybox-static保证编出来的二进制是静态链接的不依赖动态库方便塞进initramfs这个迷你环境。然后建一个临时工作目录模拟initramfs的根mkdir -p /tmp/initramfs/{bin,dev,proc,sys,newroot,etc}解释一下每个目录的用途bin/放busybox二进制和init脚本用到的命令链接。dev/设备节点目录虽然devtmpfs会自动挂载但initramfs早期还是得先手动建几个基础节点。proc/、sys/伪文件系统挂载点。newroot/用来挂载真实根文件系统的临时挂载点。etc/放一些基础配置比如fstab可能用不到但init脚本需要的环境变量文件可以放这里。把busybox复制进去cp /usr/bin/busybox /tmp/initramfs/bin/然后再创建busybox的符号链接让initramfs里有sh、mount、switch_root等命令可用cd /tmp/initramfs/bin for cmd in sh mount umount switch_root grep cat ls mkdir sleep; do ln -sf busybox $cmd done这里有个小细节ln -sf busybox $cmd创建的是相对符号链接切根之后链接依然有效因为busybox会通过argv[0]判断要执行哪个命令。如果你用了绝对路径的符号链接切根后如果路径变了命令就失效了。3.2 编写init脚本接下来是核心的init脚本。这个脚本会被内核作为initramfs的PID 1进程执行它要做的事很明确挂载proc、sysfs、devtmpfs。挂载真实根文件系统到/newroot。调用switch_root切换到新根并执行真正的init。脚本内容我写了一个示例你可以直接抄作业#!/bin/busybox sh # 挂载基础伪文件系统 mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev # 设备节点兜底万一devtmpfs没挂上手动建关键节点 mkdir -p /dev/pts mount -t devpts none /dev/pts echo Initramfs: waiting for root device... # 这里要根据你的实际情况修改/dev/sda1只是示例 mkdir -p /newroot mount -t ext4 /dev/sda1 /newroot # 检查新根是否挂载成功 if [ ! -f /newroot/sbin/init ]; then echo ERROR: /newroot/sbin/init not found! exec sh fi # 切换到新根 exec switch_root /newroot /sbin/init这个脚本有几个值得说道的细节。第一个是exec switch_root的写法。一定要用exec因为它会把当前init进程直接替换成switch_root这样PID 1进程不会产生多余的子进程PID保持连续。如果你不用execinit脚本会fork出一个子进程跑switch_root切根后父进程还赖在initramfs里不退旧根被switch_root清掉之前先被这个残留进程卡住轻则警告重则直接失败。第二个是mount -t devtmpfs none /dev。devtmpfs可以让内核自动生成设备节点省去手动mknod的麻烦。但注意devtmpfs必须在内核配置里启用CONFIG_DEVTMPFS现在的主流发行版内核都开了不用操心。如果你在定制内核时把这个关了那就只能手动创建/dev/console和/dev/null节点了mknod /dev/console c 5 1 mknod /dev/null c 1 3第三个是exec sh这个兜底逻辑。新根挂载失败时会掉进一个交互shell方便排错。这个shell循环在真实的initramfs里非常有用很多发行版的initramfs失败时都会丢给你一个类似(initramfs)的shell原理就是这里。3.3 打包与测试init脚本写好后给它加上执行权限然后打包成initramfs镜像chmod x /tmp/initramfs/init cd /tmp/initramfs find . | cpio -o -H newc | gzip /boot/initramfs-test.img这里find . | cpio -o -H newc是标准的initramfs打包方式-H newc指定newc格式这是内核要求的cpio格式千万不要用老旧的bin格式。管道后面接gzip压缩成gz包内核能识别。测试的话有两种方式。如果你用的是物理机直接改GRUB启动项在kernel命令行加initrd /boot/initramfs-test.img同时把root参数指向你的真实根分区。如果不想冒险用QEMU模拟更稳妥qemu-system-x86_64 -kernel /boot/vmlinuz-$(uname -r) \ -initrd /boot/initramfs-test.img \ -append consolettyS0 root/dev/sda1 rw \ -hda /dev/null注意这里root/dev/sda1要和init脚本里的设备一致。不过用QEMU测试需要你准备一个磁盘镜像真实体验更佳的做法是直接把你的系统盘做成qcow2虚拟磁盘。我在调试时常用这种组合拳效果和真实机器几乎一致。实验做完你会发现当switch_root成功执行后屏幕上会出现新根文件系统里init进程的输出比如systemd的启动日志或者sysvinit的欢迎信息。这里要额外提一个极易踩的坑如果你拿一个正在运行的系统的根分区做新根切根后新根里的/sbin/init可能会尝试重新挂载已经在使用的分区导致systemd重入或者挂载冲突。所以我建议你用独立的测试分区来做或者用KVM虚拟机专门准备一个测试磁盘。4. 常见问题与排查实录4.1 常见错误速查表我在调过的嵌入式项目和自定义发行版里总结出了switch_root最常见的几个报错场景整理成速查表遇到问题直接对照着查报错信息原因解决方案switch_root: newroot is not mountpointnewroot参数指向的目录并没有挂载任何设备在调用前先执行mount挂载目标设备switch_root: cant execute /sbin/init: No such file or directoryinit程序不存在或不是新根内的绝对路径检查新根文件系统里是否有对应init程序Kernel panic - not syncing: Attempted to kill init!init进程崩溃被杀死通常是切根后关键目录缺失检查新根的/etc、/lib、/proc是否完整switch_root: failed to move root当前根没有被正确挂载为rootfs确认是在initramfs的PID 1进程里调用而不是普通shell环境系统起来后free显示内存被占用几百MB切根前initramfs文件没有被清理干净检查是否有进程/文件占用initramfs改用exec switch_root调用第2个报错最有迷惑性。很多人明明看到新根里/sbin/init存在但switch_root就是找不到。这是因为switch_root检查init路径时是在切根之后解析的。如果新根文件系统挂载到/newroot而你写的路径是/newroot/sbin/init那就不对了。正确写法是switch_root /newroot /sbin/init找的是新根里面的/sbin/init。第4个报错也经常出现。switch_root要求调用时的当前根确实是rootfs。如果你在普通shell里直接switch_root当前根是磁盘根不是initramfs工具会拒绝执行。switch_root本身设计成只能从initramfs环境调用因为普通环境下没人需要也没人能释放根文件系统。4.2 两个容易忽略的坑第一个坑是关于LVM和LUKS的。现在的服务器基本都会上LVM或者磁盘加密这时候真实根文件系统不是直接挂在/dev/sda1上而是挂在/dev/mapper/xxx或者需要密码解锁。我见过很多人在initramfs里急着调switch_root结果新根还没解锁或者LVM卷组还没激活挂载直接失败系统卡在(initramfs)提示符前。正确的流程应该是在initramfs阶段先把LVM卷组激活或者用cryptsetup先解锁设备再挂载、再切根。顺序不能乱否则switch_root只能报错。第二个坑跟内存释放有关。我在第2节提过switch_root会清理initramfs文件来释放内存但有个前提initramfs里的文件必须没有被占用。比如你写了这样的脚本switch_root /newroot /sbin/init 或者忘了加exec那么那个残留的进程会一直占用initramfs里的某个文件比如init脚本本身导致switch_root清理目录时删不掉只能带着一堆“僵尸文件”切根。最终表现是系统能启动但内存少了如果你在嵌入式设备上这可能是致命的直接触发OOM Killer把主服务杀了。解决办法我在前面说过一定用exec switch_root让init进程的PID延续下去不会产生额外占用。个人经验调这个功能时最好在initramfs的脚本里加上日志输出用echo或者logger把关键步骤打出来。有人觉得initramfs里看日志不方便其实你完全可以把日志写到一个/dev/console设备或者内核日志缓冲区里切根失败后通过dmesg回看。我习惯在每一步后面加一行echo [initramfs] Mounting root device... /dev/kmsg内核日志会记录下这些消息启动失败后用dmesg就能看到最后执行到了哪一步省去在(initramfs)shell里盲猜的麻烦。这套switch_root的机制从我第一次接触到现在可以说历久弥新。不管你用的是busybox的initramfs还是systemd的initramfs抑或是自己手写的微型根切换脚本核心思想一致把临时根的使命完成交给真正的根干净利落不拖泥带水。好好掌握它你在Linux启动链路上就不再是“知其然不知其所以然”了。
返回列表