ARTICLE DETAIL

资讯详情

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

QEMU+BusyBox定制最小Linux根文件系统从零到启动

QEMU+BusyBox定制最小Linux根文件系统从零到启动 1. 项目概述与整体思路1.1 为什么要自己做根文件系统先说个背景。我在学习和调试Linux内核时经常需要验证一些内核模块、驱动或者系统调用的行为。直接用发行版比如Ubuntu启动虽然方便但很多细节被封装得看不清楚。而且每次修改内核配置、重新编译一套流程走下来效率很低。后来我开始用QEMU模拟一台“裸机”自己定制内核、自己制作根文件系统把启动过程完全掌握在自己手里。这篇文章就是这个系列第三篇重点讲根文件系统的制作以及怎么和编译好的内核一起在QEMU里跑起来。所谓根文件系统简单说就是内核启动后挂载的第一个文件系统它包含/bin、/sbin、/etc、/lib、/dev、/proc、/sys这些基本目录以及初始化进程通常是init和必要的动态库。内核启动时会根据启动参数找到根文件系统挂载它然后执行其中的init程序。没有根文件系统内核启动到最后会直接panic屏幕显示“VFS: Unable to mount root fs”什么都做不了。注意根文件系统不是某种特定的文件系统格式而是一个“目录结构内容”的集合。它可以是ext4、ext2、squashfs、ramfs、initramfs等形式。只要内核能识别并挂载就能作为根文件系统使用。这篇文章面向的是两类读者一类是像我一样想深入理解内核启动机制的爱好者另一类是做嵌入式开发、需要定制最小系统的工程师。整个过程不依赖复杂的构建工具用一台装了Linux的电脑虚拟机也行加上BusyBox就能完成。1.2 QEMU方案选型的理由做这个实验业界常见的有三条路线用真实的开发板比如树莓派、全志、瑞芯微、用VMware/VirtualBox装个完整发行版、用QEMU模拟。开发板的问题是成本高、调试不便改内核参数要反复烧写没有串口日志很难定位问题。VMware一类的方案是把整个系统虚拟化内核里很多东西已经被发行版定制过了不适合做内核实验。QEMU的方案有两个优势一是它模拟的是完整机器CPU、内存、外设都是虚拟的内核从零开始引导和真实硬件的行为非常接近二是QEMU支持-kernel参数直接加载内核镜像支持-initrd加载initramfs还能通过-append传内核启动参数整个过程就是一条命令的事改起来非常快。我实测下来从编译内核到在QEMU里启动最快可以做到三分钟内跑通一个最小系统前提是编译过一次后续增量编译。这个效率是开发板远远比不上的。所以做内核早期启动、驱动调试、文件系统实验QEMU是最合适的平台没有之一。2. 核心细节解析与实操要点2.1 一张图看懂内核、根文件系统、init进程的关系先把概念理清后面操作才有方向。一台Linux机器从开机到进入命令行走的是这么一条链路BootLoaderQEMU内置的或者直接用-kernel参数跳过加载内核镜像到内存内核解压、初始化各个子系统内存管理、调度器、中断、驱动等内核根据启动参数中的root找到根文件系统所在设备执行挂载挂载成功后内核在根文件系统中寻找init程序路径由init参数指定默认是/sbin/init并把控制权交给它init程序根据配置文件通常是/etc/inittab完成系统初始化包括挂载/proc、/sys、配置网络、启动shell等。这里有几个容易混淆的概念需要区分清楚概念作用常见的实现方式内核镜像操作系统的核心负责管理硬件和提供系统调用vmlinuz、Image、zImage根文件系统内核挂载的第一个文件系统提供目录结构和用户态程序ext4镜像、initramfs、squashfsinit程序用户态的第一个进程PID为1是所有进程的祖先BusyBox init、systemd启动参数内核启动时的命令行参数告诉内核如何挂载根文件系统root、init、console理解这个链路后你会发现内核和根文件系统其实是“分工合作”的关系内核负责硬件和管理根文件系统负责提供用户态工具和初始化逻辑。两者通过启动参数建立了联系。2.2 BusyBox、init、inittab三件套怎么配合在最小系统中最常用的用户态工具集是BusyBox。它最大的特点是“一个二进制文件集成几百个命令”通过busybox ls、busybox sh这种方式调用也可以创建符号链接来模拟传统命令。这意味着根文件系统不需要为每个命令单独拷贝一个可执行文件极大减小体积。BusyBox的init程序遵循经典的SysV init流程读取/etc/inittab文件决定启动哪些服务。inittab的格式是id:runlevels:action:process但在BusyBox里runlevels字段被忽略而action字段常见的有sysinit系统初始化时要执行的任务比如挂载/proc、/sysrespawn当进程退出后自动重新启动一般用于getty登录终端askfirst在终端上提示“Please press Enter to activate this console”按回车后启动shellrestartinit重启时执行once只执行一次不等待shutdown关机时执行的脚本。我们做实验时最常用的inittab配置就三行一行sysinit执行/etc/init.d/rcS一行askfirst在串口控制台上启动shell一行shutdown干净的卸载文件系统。看起来简单但这里面有个坑console配置必须和内核的console参数一致否则你会在QEMU的窗口里看不到任何输出。2.3 initramfs vs ext4镜像到底选哪个制作根文件系统核心问题是用什么形式让内核访问到它。两种主要方案initramfs和磁盘镜像。initramfs本质是一个cpio格式的压缩包通常是gzip压缩被内核直接加载到内存中作为临时根文件系统。它不需要真实的块设备支持启动流程是内核把initramfs解压到内存tmpfs然后将其作为根文件系统挂载。这种方式的优点是制作简单、不依赖磁盘控制器驱动、启动极快非常适合做内核实验和嵌入式系统的早期启动。缺点是所有内容都在内存里掉电即消失不适合做持久化存储。磁盘镜像则是一个真实的分区镜像文件比如rootfs.ext4。内核需要对应的文件系统和磁盘驱动virtio_blk或IDE才能挂载它。优点是数据可以持久化保存缺点是镜像的制作过程稍微复杂一些需要用到dd、mkfs.ext4和mount循环设备。我的建议是做实验优先用initramfs。原因很简单——内核配置里只要开启CONFIG_BLK_DEV_INITRD和CONFIG_RD_GZIP然后在启动参数里加initrd即可。省去了分区、格式化的步骤修改根文件系统的内容只需要重新打包cpio非常灵活。这篇文章的实操部分采用initramfs方案。不过如果你想模拟更真实的启动场景比如做内核模块的rootfs挂载实验磁盘镜像会更有价值。我在文末也会简单提一下磁盘镜像怎么转换。3. 实操过程与核心环节实现3.1 环境准备内核源码、BusyBox、QEMU先列一下我实验用的环境你不需要完全一样但版本不能太旧。宿主机Ubuntu 22.04 x86_648核CPU16GB内存编译内核建议至少4GB可用内存内核源码Linux 5.15.x版本差异不影响本文步骤BusyBox1.36.xQEMU7.0以上版本通过apt install qemu-system-x86安装。我假设你已经把内核源码准备好了并且至少成功编译过一次。如果内核还没编译过建议先跟着编译出bzImage再回来看这篇文章。内核源码目录我用$KERNEL表示。BusyBox的下载和配置可以直接命令完成wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar xjf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 make defconfig make menuconfig在menuconfig里进入Settings - Build Options把Build static binary (no shared libs)选上。这个选项很重要——静态编译的BusyBox不依赖任何动态库拷贝到根文件系统里就能跑省去了拷贝/lib的麻烦。如果你选择动态编译还需要手动拷贝glibc或musl的.so到根文件系统里步骤多但能压体积。对刚开始做实验的人来说静态编译是最省心的选择。3.2 根文件系统的目录骨架和BusyBox安装编译安装BusyBox之前先把根文件系统的目录骨架建好。我会在一个临时目录里操作比如~/rootfs。mkdir -p ~/rootfs/{bin,sbin,etc/init.d,proc,sys,dev,lib,usr/{bin,sbin},root,tmp,var}目录作用对照目录作用bin用户命令如ls、cat、shsbin系统命令如init、ifconfigetc配置文件如inittab、fstab、rcSproc、sys虚拟文件系统挂载点dev设备文件节点所在目录lib动态库静态编译时可不拷贝root、tmproot用户家目录和临时目录目录建好后开始安装BusyBoxmake -j$(nproc) make CONFIG_PREFIX~/rootfs installCONFIG_PREFIX指定了安装目录BusyBox会自动在~/rootfs下创建bin/busybox并在bin、sbin等目录里生成一大批符号链接指向BusyBox二进制文件。安装完成后~/rootfs就是一个能看能用的最小用户态环境了。提示make install不会覆盖你手工创建的目录但会把bin、sbin等目录补齐。如果你有自定义的脚本文件放在里面不要担心它会原样保留。3.3 写inittab和rcS让系统“活”起来BusyBox安装完成只是把“工具”放进去了系统要真正启动还差两个关键文件/etc/inittab和/etc/init.d/rcS。先创建~/rootfs/etc/inittab内容如下::sysinit:/etc/init.d/rcS ::askfirst:-/bin/sh ::restart:/sbin/init ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r逐行解释第一行sysinit系统启动时第一个执行的任务运行初始化脚本rcS第二行askfirst在第一个终端上提示用户按回车然后启动一个shell。这里的-前缀表示login shell会读取/etc/profile第三行restartinit进程重启时调用/sbin/init第四行ctrlaltdel按下CtrlAltDel时执行重启QEMU窗口里可能触发不了但配置上没坏处第五行shutdown关机时先卸载所有文件系统-r参数表示失败也继续执行。然后是~/rootfs/etc/init.d/rcS这个脚本负责挂载虚拟文件系统和设置主机名#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev echo Hello from initramfs!写完后记得给脚本加执行权限chmod x ~/rootfs/etc/init.d/rcS之所以在rcS里挂载devtmpfs是为了让/dev目录下自动出现ttyS0、null、zero等基本设备节点。如果没有这一步你在shell里执行echo /dev/null会报“No such file or directory”。实际使用中发现devtmpfs比手动mknod方便太多了强烈推荐。3.4 打包initramfscpio和gzip的细节根文件系统目录就绪后用cpio把它打包成一个内核能识别的initramfs镜像。注意打包时必须从~/rootfs目录内部执行否则cpio会把绝对路径打进镜像内核解压时会出问题。cd ~/rootfs find . | cpio -H newc -o --owner root:root ../initramfs.cpio gzip -9 ../initramfs.cpio解释一下参数find .列出当前目录下所有文件包括目录cpio -H newc使用newc格式这是内核initramfs唯一支持的cpio变体-o表示输出归档--owner root:root把所有文件的属主设为root避免你宿主机用户ID混入镜像gzip -9压缩-9是最大压缩比体积能压掉70%以上。两条命令可以连起来写成一行cd ~/rootfs find . | cpio -H newc -o --owner root:root | gzip -9 ~/initramfs.img生成的~/initramfs.img就是内核启动时要加载的initramfs镜像。注意这里有个容易踩的坑。cpio打包时find .会输出一个./开头的路径cpio会把它原样记录在归档里。内核解压时路径开头的./会被忽略所以一般没问题。但如果你用了find *或者find | sort之类的变体产生的归档可能在内核解压时出现“Empty file”之类的错误。用find .是最稳妥的不需要画蛇添足。3.5 QEMU启动第一个完整的KernelRootfs内核镜像和initramfs都准备好了下面就是见证“奇迹”的时刻。假设你的内核编译产物在$KERNEL/arch/x86/boot/bzImage对应的启动命令是qemu-system-x86_64 \ -kernel $KERNEL/arch/x86/boot/bzImage \ -initrd ~/initramfs.img \ -append consolettyS0 root/dev/ram rdinit/sbin/init \ -nographic逐项参数说明-kernel指定内核镜像-initrd指定initramfs镜像-append传给内核的命令行参数。consolettyS0是把内核日志输出到串口配合-nographic使用root/dev/ram告诉内核根文件系统在内存中的ramfs即initramfsrdinit/sbin/init指定initramfs中第一个执行的程序-nographic不用图形窗口输入输出直接在当前终端进行。实验环境没有显示器的场景下特别有用。如果顺利你会看到一串内核日志滚动最后停在BusyBox的shell提示符类似Please press Enter to activate this console.按回车进入命令行/ # ls bin etc proc root sbin sys tmp usr var / #到这里一个最小的、完全由自己定制的Linux系统就跑起来了。你可以试试ls /bin会看到BusyBox自动创建的那些符号链接执行cat /proc/meminfo能看到虚拟机的内存信息。这个最小系统有一个特点干净干净得连ifconfig都配不上ip。这也是为什么后续做网络实验时需要额外编译网络工具和配置脚本。但作为内核启动验证这一步已经足够了。3.6 ext4磁盘镜像制作扩展方案initramfs适合做启动验证和临时系统但如果你需要持久化存储比如往/root里写文件重启后保留就需要一个磁盘镜像。制作一个空白的ext4磁盘镜像dd if/dev/zero of~/rootfs.img bs1M count64 mkfs.ext4 ~/rootfs.img然后借助loop设备把镜像里的文件系统替换成我们的rootfs目录内容mkdir -p /mnt/rootfs sudo mount ~/rootfs.img /mnt/rootfs sudo cp -a ~/rootfs/* /mnt/rootfs/ sync sudo umount /mnt/rootfs启动命令相应变为qemu-system-x86_64 \ -kernel $KERNEL/arch/x86/boot/bzImage \ -drive formatraw,file~/rootfs.img \ -append consolettyS0 root/dev/sda rw \ -nographic对比initramfs启动不同的地方在于-initrd换成-drive把磁盘镜像挂成虚拟硬盘root/dev/ram换成root/dev/sda告诉内核根文件系统在第一个虚拟磁盘上加了rw让根文件系统以读写方式挂载。这种方案做出来的系统在QEMU里完全可以当一台小服务器用不夸张地说几十兆的磁盘空间就能做很多实验了。4. 常见问题与排查技巧实录做内核启动实验最大的挑战不是“编译不过”而是“启动不起来后不知道从哪里排查”。下面这几个问题是我实际踩过的坑整理成速查表方便你对照。现象可能原因排查命令/方法启动后停在Kernel panic - not syncing: VFS: Unable to mount root fs内核没有initramfs支持或initramfs路径不对或cpio格式错误检查内核配置CONFIG_BLK_DEV_INITRDy确认-initrd指向的镜像存在重新打包cpio启动了但看不到shell提示只有一个黑屏/空白终端console参数和内核对不上或无askfirst配置确认-append里有consolettyS0且inittab里askfirst对应的终端是sh不是tty1执行ls等命令提示not foundBusyBox没有正确安装或动态编译缺少库确认make CONFIG_PREFIX~/rootfs install执行过用file ~/rootfs/bin/busybox检查是否是静态编译文件系统目录是空的符号链接没生成make install可能覆盖了路径或者CONFIG_PREFIX没有生效手动执行~/rootfs/bin/busybox --install -s重建符号链接挂载proc返回No such device内核没有开启CONFIG_PROC_FSmake menuconfig里搜索PROC_FS确认已开启initramfs内看到的是宿主机目录不是rootfscpio打包时目录进入方式不对确保在~/rootfs目录内执行find .不要加绝对路径4.1 内核日志最后一行的“魔咒”Kernel panic - not syncing: VFS: Unable to mount root fs大概是内核实验里最常见的panic信息了。很多人一看到这个就慌其实它想表达的意思很简单内核找不到合适的根文件系统。排查顺序确认-initrd路径正确文件存在且非空确认内核配置里CONFIG_BLK_DEV_INITRDy和CONFIG_RD_GZIPy如果initramfs是gzip压缩的确认cpio打包时用了-H newc而不是默认的odc或bin格式确认-append里有root/dev/ram或rdinit/sbin/init。这四步走完90%的panic都能解决。剩下10%是内核根本没起来还没到挂载根文件的阶段那就要看更早的日志了。4.2 为什么我的shell不出现内核日志一切正常但QEMU窗口里只有一个“Please press Enter to activate this console”按回车没反应。这个问题我折腾过半小时最后发现是console参数不对。如果你用-nographic模式内核日志默认输出到第一个串口ttyS0但inittab里askfirst对应的终端默认是tty1。两者不一致就会出现内核日志刷屏但shell无响应的怪现象。解决方案inittab里把askfirst那一行改成::askfirst:-/bin/sh不指定具体tty或者明确指定console与串口一致。我建议用后者更干净::askfirst:-/bin/shBusyBox init会自动把它分配到当前活动的控制台设备上。这一行是最不容易踩坑的写法。4.3 静态编译的BusyBox还缺依赖理论上静态编译的BusyBox不依赖任何动态库但有一个场景会“破功”你调用了BusyBox不支持的某个命令或者系统缺少必要的设备节点。比如在initramfs里执行free它需要/proc/meminfo。如果rcS脚本里没挂载/procfree就会报错。这个错误不是BusyBox的问题而是系统环境不完整。排查口诀遇到“No such file or directory”先看/proc、/sys、/dev挂没挂遇到“No such device”先看内核配置有没有开对应的驱动。4.4 在QEMU窗口里怎么退出刚开始做实验时最尴尬的是不知道QEMU的-nographic模式怎么退出。快捷键是CtrlA然后按X即先按CtrlA松开再按一次X。如果你当前在BusyBox的shell里也可以直接执行poweroff -f或reboot -f优雅退出。如果你忘了这个快捷键还有一个粗暴的方式另开一个终端执行pkill qemu-system。不过不推荐因为非正常退出可能会让磁盘镜像出现未同步的数据。5. 内核与根文件系统联调的进阶技巧5.1 只用一条命令完成内核编译与initramfs更新做内核实验最烦的就是每改一次代码都要重复编译、打包、启动。我后来写了一个小脚本放在$KERNEL目录下名字叫build-and-run.sh#!/bin/bash set -e KERNEL_DIR$(pwd) ROOTFS_DIR~/rootfs OUTPUT/tmp/kernel-boot make -j$(nproc) bzImage cd $ROOTFS_DIR find . | cpio -H newc -o --owner root:root | gzip -9 $OUTPUT/initramfs.img cd $KERNEL_DIR qemu-system-x86_64 \ -kernel arch/x86/boot/bzImage \ -initrd $OUTPUT/initramfs.img \ -append consolettyS0 root/dev/ram rdinit/sbin/init \ -nographic这个脚本的逻辑是先编译内核再重新打包initramfs最后启动QEMU。每次改完代码只需要跑这个脚本最多一两分钟就能看到效果。实测下来比手动一条条敲命令效率高了非常多。5.2 用9p共享目录实现宿主机与VM文件互传QEMU有一个9p virtio功能可以把宿主机的一个目录直接共享给虚拟机类似VMware的共享文件夹。在initramfs实验里这个功能特别好用——你不需要手动修改initramfs直接在宿主机编辑文件虚拟机能立即看到最新内容。宿主机侧启动命令qemu-system-x86_64 \ -kernel $KERNEL/arch/x86/boot/bzImage \ -initrd ~/initramfs.img \ -append consolettyS0 root/dev/ram rdinit/sbin/init \ -virtfs local,path/home/user/shared,mount_taghost0,security_modelnone,idhost0 \ -nographic虚拟机制作挂载点并加载9p驱动mkdir -p /mnt/shared mount -t 9p -o transvirtio,version9p2000.L host0 /mnt/shared这样就可以在/mnt/shared里看到宿主机/home/user/shared下的文件了。注意security_modelnone意味着VM内所有文件都以宿主机当前用户权限访问仅限实验环境使用不要在生产环境配这个。5.3 完整的实验日志怎么抓调试内核时光靠肉眼盯着QEMU窗口是不行的。把内核日志完整保存下来才能反复分析。两种方式启动命令里加-serial file:/tmp/kernel.log内核的串口输出会同时写到文件里在宿主机终端用tee命令把QEMU输出落盘qemu-system-x86_64 ... 21 | tee /tmp/qemu.log。我个人习惯用第一种因为-serial file:是QEMU原生支持的不会影响终端的交互输入。5.4 initramfs体积优化的思路initramfs做大了启动时解压和拷贝都会变慢。实测下来单纯BusyBox静态编译的最小系统initramfs压缩后大约1~2MB。如果你的initramfs体积明显偏大检查这几个地方有没有拷贝宿主机的/lib动态库进去有没有多余的内核模块.ko文件被make modules_install INSTALL_MOD_PATH...装进去有没有包含文档、man page之类的非必要文件。体积优化的核心思路是“只留能跑通的最小闭环”一个可用shell、一个init进程、必要的挂载点和配置就足够。等你需要某个功能时再按需加入而不是一开始就塞满所有东西。6. 一些个人体会折腾内核和根文件系统这件事看起来是个纯技术的活儿多做几次就会发现它其实是在帮你建立“从零到一”的完整视角。以前用发行版你对系统启动的理解只停留在“开机转圈圈”这个层面。亲手做完这套流程后你再看到/etc/inittab、/init、VFS挂载这些名词脑子里会浮现出一台QEMU机器从无到有、从内核到shell的画面。这个画面感挺值钱的尤其在做驱动调试、系统裁减、嵌入式开发的时候能帮你快速定位问题到底出在哪个环节。再分享一个很小但实用的技巧我在调试时经常临时改rcS脚本但initramfs是内存文件系统改完要重新打包才能生效。后来我发现直接在QEMU里用mount -t 9p共享目录后把脚本放在共享目录里再在rcS里加一行source /mnt/shared/init_extra.sh这样改脚本只需要保存宿主机文件不用重新打包调试效率提高了非常多。如果你也经常改初始化脚本这个思路可以试试。最后说一下后续扩展方向。这篇文章做的是x86_64平台的最小系统但同样的思路完全可以移植到ARM64模拟上QEMU的qemu-system-aarch64加上-machine virt核心步骤几乎一致。另外如果你想把rootfs从initramfs换成NFS挂载适合做内核NFS根文件系统调试原理也相通只是启动参数从root/dev/ram变成root/dev/nfs nfsroot...。内核实验这条路越往里走越有意思希望这篇文章能帮你跨过第一道门槛。
返回列表