ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零制作最小根文件系统:QEMU+BusyBox+initramfs实战

从零制作最小根文件系统:QEMU+BusyBox+initramfs实战 不知道你有没有经历过这种时刻辛辛苦苦把 Linux 内核编译完满怀期待地敲下 QEMU 启动命令结果屏幕上只剩下一行Kernel panic - not syncing: VFS: Unable to mount root fs。我刚开始学内核的时候这句话陪我度过了整整一周当时根本想不明白——内核不是已经编译出来了吗为什么还不让启动问题其实不复杂你有了一台“没有硬盘系统的电脑主机”也就是内核但还没有给它配上一块装有操作系统的“硬盘”也就是根文件系统。在内核开发、嵌入式调试、学习启动流程这些场景里QEMU 虚拟机是最方便的实验场而根文件系统就是让它从“有 CPU 有内存”变成“能跑 shell 能跑命令”的关键一环。这篇文章会带你完整走一遍从零制作一个最小可用的根文件系统把它打包成 initramfs再和编译好的 Linux 内核一起放进 QEMU 虚拟机里启动。做完之后你得到的不只是一个能跑的系统更重要的是你会明白根文件系统里每个目录、每个脚本、每个节点到底在启动流程里扮演什么角色。这份理解对后续学习内核驱动、调试用户态程序、做嵌入式系统移植都特别有帮助。1. 先把问题讲清楚根文件系统到底缺了什么1.1 那行 Kernel panic 背后缺了什么Linux 整个系统的启动流程可以粗略分成两段第一段是内核自己初始化硬件、调度器、内存管理这些不需要外部存储第二段是内核要把控制权交给用户态程序这时候就必须有一个能被它识别和挂载的根文件系统。根文件系统里至少有 init 程序、动态库、设备节点和最基本的目录结构如果内核在启动时找不到根文件系统它就像一个新入职的员工工位、电脑、资料全都没有只能原地报错。所谓“根文件系统”英文叫 root filesystem不只是文件系统格式本身它指的是“根目录 / 上挂载的那一整套文件树”。它不一定是硬盘上的 ext4 分区也可以是一块内存盘、一个网络文件系统甚至是一段压缩后的 cpio 归档——只要内核能在启动阶段把它挂载到/上就行。理解了这一点你就会发现“制作根文件系统”这件事其实并不神秘本质就是整理出一个符合 Linux 目录规范的文件夹再把整个文件夹打包成内核能识别的格式。1.2 为什么选 QEMU 而不是直接装个虚拟机QEMU 这类虚拟机选项很多VMware、VirtualBox 都常见但做内核实验首选 QEMU 的理由很直接它是纯软件模拟器能跑标准 Linux 内核镜像不需要经历完整的硬盘分区安装流程。你只需要给它传-kernel和-initrd两个参数它就可以把一个内核和一个 rootfs 归档直接带起来整个过程可以脚本化、自动化坏了重建也就几秒钟的事。而且 QEMU 的串口输出非常干净可以用-nographic把虚拟机的串口直接映射到当前终端启动日志、内核打印、shell 交互全都能看到。这种体验特别适合调试比如你想验证某个内核配置项、某个驱动的早期初始化逻辑直接在 QEMU 里启动比在实体机上反复重启要高效太多。1.3 整体方案选型BusyBox initramfs制作根文件系统的工具很多常见的有 BusyBox、Buildroot、Yocto 等。这里我推荐 BusyBox原因很简单它把所有常用的 Unix 命令ls、cat、mount、sh 等压缩成一个二进制文件再用符号链接的方式复用同一个程序体积小、依赖少是搭建最小系统的事实标准。Buildroot 更适合做完整发行版Yocto 面向大规模定制产品我们用不到那么重的东西。rootfs 的承载格式我选 initramfs而不是 ext4 磁盘镜像。initramfs 本质是一段 cpio 归档由内核解压到内存中直接作为根文件系统优点是不需要预先建分区、不需要写引导程序、改起来也快。它和早期 initrd 的区别在于initrd 是一个块设备镜像内核需要先模拟出一个块设备再挂载initramfs 则直接使用内核的 tmpfs 页面缓存机制整个 rootfs 内容就在页缓存里读取更快实现也更简单。对学习和调试来说initramfs 明显是优先选择。2. 制作前必须搞懂的底层逻辑2.1 根文件系统的最小目录骨架一个 Linux 根文件系统长什么样很多人第一反应是那不就是ls /看到的那一堆目录吗对但真正的细节在于每一个目录的职责。完整的发行版有几十个目录但最小可用的系统只需要保留以下这些核心目录/bin普通用户和管理员常用的命令比如sh、ls、cat、mountBusyBox 默认就跑在这里。/sbin系统管理类命令比如init、ifconfig也由 BusyBox 提供符号链接。/etc配置文件所在目录最简单的情况下我们要放一个inittab或者配合 init 脚本决定开机做什么。/dev设备文件所在目录最小系统至少要有/dev/console和/dev/null否则内核启动早期连标准输入输出都没法工作。/proc和/sys这两个是虚拟文件系统内核信息通过它们暴露给用户态需要在 init 阶段用mount挂载。/tmp临时文件目录很多程序运行时会往这里写东西。/usr和/var完整系统里它们内容很多但最小系统里可以只留空目录甚至让/usr链接到/也可以。理解这些目录不能只靠背你要把它们看成“内核和用户态程序之间约定的接口”。比如/proc里能看到进程列表和内核参数是因为内核把数据映射到了这个虚拟目录/dev里能看到设备是因为内核通过 devtmpfs 或设备管理器把设备节点暴露出来。最小系统里我们亲手建的每一个目录都是为了满足某个特定约定。2.2 启动链上的三个关键角色VFS、rootfs 和 init要理解根文件系统的启动过程绕不开 VFSVirtual Filesystem虚拟文件系统。VFS 是 Linux 内核里的一个抽象层它让上层程序不用关心底下的文件系统是 ext4、vfat 还是 proc统一以文件的形式访问。根文件系统能挂载到/正是借助了 VFS 这个框架。内核启动过程是这样的内核先建立自己的内存管理、中断、调度等基础设施然后创建一个初始的 rootfs这个初始 rootfs 通常是内存里的一个空目录树接着如果启动参数里指定了 initramfs内核会把它解压到这个初始 rootfs 上覆盖成实际的根文件系统内容。再接下来就是找 init 程序。内核的查找顺序大致是先看启动参数里有没有rdinit或init如果没有默认在 rootfs 的/init找不到再找/sbin/init、/etc/init、/bin/sh等。init 程序是系统启动后的第一个用户态进程PID 1它的任务是把整个系统拉起来挂载文件系统、启动服务、最后给你一个可以交互的终端。可以说内核只负责把系统带到“init 前一步”剩下的日常运营全部交给 init 来处理。2.3 静态链接还是动态链接BusyBox 有两种编译方式动态链接和静态链接。动态链接的话二进制体积小但在目标系统上必须找到对应的动态链接器通常是/lib/ld-linux-x86-64.so.2和动态库比如 libc.so.6否则一启动就会报not found。静态链接则把依赖库全部打进二进制里体积更大但运行不依赖外部环境特别适合实验阶段。从我的经验看第一次做根文件系统优先用静态链接。原因很简单少一个变量。动态链接需要你手动拷贝动态库文件到 rootfs 的/lib目录还得保持路径和链接器路径一致一不留神就启动失败。静态链接只要CONFIG_STATICy打开编译完直接拷贝进 rootfs 就能跑省去很多麻烦。等后面熟悉了再回头尝试动态链接顺便用ldd命令看看 busybox 依赖哪些库对理解动态链接机制也很有帮助。2.4 /dev 目录为什么不能随便空着很多初学 Linux 的人以为/dev下的设备文件是普通文件其实它只是“设备节点”本质是一个主设备号加次设备号的记录。内核通过设备号找到对应的驱动然后完成 I/O 操作。比如/dev/console的主设备号是 5次设备号是 1它是系统控制台设备/dev/null的主设备号是 1次设备号是 3用来丢弃数据。在完整系统里udev或devtmpfs会自动挂载并动态创建这些设备节点。但在最小 rootfs 里如果 devtmpfs 还没挂载完就需要你提前手动创建/dev/console和/dev/null。这就是很多人看到unable to open an initial console报错的原因内核想打开/dev/console作为标准输入输出结果找不到这个节点自然就 panic 了。3. 动手实操从零构建可启动根文件系统3.1 准备工具链与源码先说环境。我这里以 Ubuntu/Debian 系为例其他发行版命令大同小异。你需要安装编译工具链和 QEMUsudo apt update sudo apt install build-essential flex bison libssl-dev libncurses-dev \ qemu-system-x86 cpio gzipflex和bison是内核编译时候解析语法用的libssl-dev是编译新版本内核时处理签名模块需要的libncurses-dev用于make menuconfig。这些少一个都可能在半路报错建议一次装齐。源码方面需要两个Linux 内核源码和 BusyBox 源码。内核可以从 kernel.org 下载选一个长期稳定版本比如 6.6.xBusyBox 去官网下载推荐 1.36.x 稳定版。老版本对较新 gcc 的兼容性有时会有小问题选新一些的稳定版更省心把源码解压到工作目录mkdir -p ~/kernel-lab cd ~/kernel-lab wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.x.tar.xz wget https://busybox.net/downloads/busybox-1.36.x.tar.bz2 tar -xf linux-6.6.x.tar.xz tar -xf busybox-1.36.x.tar.bz2如果网速慢也可以直接去官网下载再传上来。整个实验对版本没有严格硬性要求只要是较新的稳定版都能跑通。3.2 编译安装 BusyBox进入 BusyBox 源码目录先做默认配置再打开静态链接选项cd busybox-1.36.x make defconfig make menuconfig在menuconfig里进入Settings找到Build static binary (no shared libs)勾选它。这一项决定最终编译出来的是不是独立二进制。然后退出保存开始编译make -j$(nproc) make install编译结束后当前目录下会生成一个_install目录。里面的结构就是一套最小 rootfs 的雏形_install/bin/busybox核心二进制_install/sbin/init指向/bin/busybox的符号链接_install/bin/sh、_install/bin/ls等各种命令的符号链接都指向/bin/busybox可以file _install/bin/busybox确认一下看到statically linked说明静态链接成功。接下来要用这个_install来填充我们自己构建的 rootfs。3.3 搭建目录骨架、补齐库和设备节点新建一个rootfs目录把刚才_install里的内容整体拷贝进去mkdir -p ~/kernel-lab/rootfs cd ~/kernel-lab/rootfs cp -a ~/kernel-lab/busybox-1.36.x/_install/* .再创建标准的目录骨架mkdir -p etc/init.d proc sys dev tmp mnt home root lib usr/bin usr/sbin var/log正常情况下静态链接的 busybox 不需要拷贝动态库所以lib目录可以留空。但为了以后扩展提前建好没有问题。接着创建设备节点这是最关键的步骤之一不要漏sudo mknod -m 622 dev/console c 5 1 sudo mknod -m 666 dev/null c 1 3/dev/console用来接收内核日志和打印输出缺失会导致启动早期直接死掉/dev/null的作用更常用很多命令和脚本都往它写数据。设备节点需要 root 权限创建这也是整个实验里为数不多要用sudo的地方。3.4 编写 init 脚本并打包 initramfs现在需要在 rootfs 根目录下写一个/init脚本。这个脚本是内核在 initramfs 场景下的第一入口它要做的事很简单挂载几个必要的虚拟文件系统然后启动真正的 init 程序。cat init EOF #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev mkdir -p /dev/pts mount -t devpts devpts /dev/pts echo minimal rootfs boot OK exec /sbin/init EOF chmod x init这里解释一下每一步在干嘛。mount -t proc挂载进程文件系统之后ps、/proc/cpuinfo这些才有数据mount -t sysfs挂载设备驱动和内核对象视图mount -t devtmpfs /dev是让内核自动把已知设备的节点填入/dev这是现代 Linux 的标准做法。最后一行exec /sbin/init会用/sbin/init替换当前 shell 进程它实际上会执行 BusyBox 内置的 init 程序。BusyBox 的 init 程序启动后会读取/etc/inittab来决定运行哪些命令。如果没有这个文件BusyBox 会按默认行为去执行/etc/init.d/rcS然后再弹 shell。为了行为可控我习惯写一个简单的 inittabcat etc/inittab EOF ::sysinit:/etc/init.d/rcS ::respawn:-/bin/sh EOF再写一个 rcS 脚本作为开机启动逻辑cat etc/init.d/rcS EOF #!/bin/sh echo rcS running EOF chmod x etc/init.d/rcS做完这些整个 rootfs 的内容就基本齐了。接下来把它打包成内核能识别的 initramfs 归档。这里有个关键点打包的时候要在 rootfs 目录内执行不要带外层目录路径cd ~/kernel-lab/rootfs find . -print0 | cpio --null -ov --formatnewc | gzip -9 ../initramfs.cpio.gz这条命令的意思是把当前目录下的所有文件以 cpio newc 格式归档再用 gzip 压缩。生成的文件就叫initramfs.cpio.gz。之后你可以用一个命令快速检查归档内容是否正确gzip -dc ../initramfs.cpio.gz | cpio -t | head看到 init、bin/busybox、dev/console 这些条目说明打包成功。3.5 编译内核并用 QEMU 启动验证内核部分其实比 rootfs 简单因为 QEMU 可以直接跑默认配置。先进入内核源码目录做一遍精简配置cd ~/kernel-lab/linux-6.6.x make x86_64_defconfig默认配置已经开启了 initramfs 支持以及大部分常用驱动。有一点值得确认CONFIG_BLK_DEV_INITRD必须要开启如果不放心可以make menuconfig搜索INITRD确认一下。另外CONFIG_DEVTMPFS也要开启它是我们脚本里挂载/dev的前提。确认后开始编译make -j$(nproc) bzImage编译完成后内核镜像在arch/x86/boot/bzImage。如果机器的 CPU 核数多-j$(nproc)会把所有核心都利用上整个过程大概几分钟到十几分钟。现在万事俱备启动 QEMUcd ~/kernel-lab qemu-system-x86_64 \ -kernel linux-6.6.x/arch/x86/boot/bzImage \ -initrd initramfs.cpio.gz \ -append consolettyS0 rdinit/init \ -m 256M \ -nographic逐项解释命令-kernel指定内核镜像-initrd指定 initramfs 归档-append把参数传给内核consolettyS0让内核把日志输出到串口配合-nographic就能在当前终端看到全部输出rdinit/init明确告诉内核用/init作为第一个用户态程序-m 256M给虚拟机分配 256M 内存。如果想要更真实的启动效果加一个-no-reboot也可以防止崩溃后无限重启。如果一切顺利你会看到内核打印一堆日志接着出现 minimal rootfs boot OK 最后进入一个可交互的 shell。这时候敲几个命令试试ls / mount cat /proc/cmdline ps能够执行并能看到输出的瞬间那种成就感是实打实的。你亲手做的 rootfs 被 QEMU 跑起来了而且整个系统内核和用户态完全可控。这套流程不只适用于 x86_64。如果你做 ARM64 方向思路完全一致区别只是用交叉编译工具链make ARCHarm64 defconfig打包出来的镜像让qemu-system-aarch64跑串口参数要改成consolettyAMA0。步骤骨架不变只是平台细节不同。4. 避坑与调试我踩过的那些坑和排查方法4.1 高频报错速查表做根文件系统最容易遇到的问题集中在这几类我把高频的整理成了一张表遇到报错直接对照查报错现象可能原因解决办法Kernel panic - not syncing: VFS: Unable to mount root fs内核找不到根文件系统多半是没传-initrd或 initramfs 打包格式不对检查 QEMU 命令有没有-initrd参数确认打包命令用了--formatnewc确认内核开启了CONFIG_BLK_DEV_INITRDRun /init as init process failed with errno -2/init不存在或权限不对用cpio -t检查归档里有没有 init确认chmod x init如果用动态链接还可能是 libc.so 没拷全unable to open an initial console/dev/console缺失用sudo mknod -m 622 dev/console c 5 1重新创建设备节点/bin/sh: cant access tty; job control turned off当前进程没有控制终端这不影响系统运行不需要处理如果想获得完美的终端交互检查consolettyS0参数和/dev/console节点init: must be run as PID 1你在已经启动的 shell 里手动执行了/sbin/init重新启动 QEMU不要手动执行 initKernel panic - not syncing: No working init foundrootfs 解压成功但找不到可执行的 init 程序检查/init、/sbin/init是否存在检查 rootfs 是否有/bin/busybox4.2 用 init/bin/sh 快速判断问题边界遇到启动失败一个特别实用的经验是用init/bin/sh直接执行 shell跳过 init 脚本和 busybox init 这一层。比如qemu-system-x86_64 \ -kernel linux-6.6.x/arch/x86/boot/bzImage \ -initrd initramfs.cpio.gz \ -append consolettyS0 init/bin/sh \ -m 256M \ -nographic如果这样能进 shell说明 rootfs 和内核基本没问题问题出在 init 脚本或 inittab 上如果连这种情况都报错那多半是 rootfs 内容本身有问题比如 busybox 没拷贝进去或设备节点不对。这个判断方法能帮你快速定位问题的边界省掉大量排查时间。还有一个小细节QEMU 的-nographic模式下退出虚拟机要按CtrlA然后按x不是直接按CtrlC。不记住这个系统卡住时你会很痛苦。4.3 时间省下来内核编译与 QEMU 调试的实用习惯内核编译是整个流程里最花时间的环节有几个习惯能明显加速。第一次做实验make x86_64_defconfig默认配置就足够了不要急着去裁剪内核裁剪能省点空间但会引入大量配置问题编译时用-j$(nproc)全核编译内存紧张的话可以-j4或-j8限制一下如果只是想跑通 rootfs还可以在 menuconfig 里关掉DEBUG_INFO和内核模块支持这两个项对编译时间影响很大。调试阶段尽量写一个启动脚本保存起来不要每次都手敲 QEMU 命令。比如#!/bin/bash qemu-system-x86_64 \ -kernel linux-6.6.x/arch/x86/boot/bzImage \ -initrd initramfs.cpio.gz \ -append consolettyS0 rdinit/init \ -m 256M \ -nographic \ -no-reboot以后每次改完 rootfs 或内核只需要重新打包并执行这个脚本效率会高很多。整个流程需要重复的次数比你想的多这个脚本早晚用得上。最后再说一个我自己的习惯每次对 rootfs 做改动后第一时间重新打包并启动验证不要攒着一堆改动再统一验证。这个系统的启动过程非常直观日志会把问题暴露得很清楚频繁验证反而是最省时间的策略。根文件系统这块一旦跑通后面做驱动的模块加载、调试内核崩溃转储、甚至自己写一个简单的 init 程序就都有了一个可靠干净的实验底座。
返回列表