三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从零构建最小Linux根文件系统:BusyBox实战与QEMU验证

从零构建最小Linux根文件系统:BusyBox实战与QEMU验证

1. 项目概述:为什么需要制作最小Linux文件系统?

在嵌入式开发、系统定制或者内核调试的日常工作中,我们经常会遇到一个核心需求:需要一个极简、可控、能快速启动的Linux运行环境。这个环境的核心,就是一个“最小Linux文件系统”。它不像我们桌面用的Ubuntu或CentOS那样动辄几个GB,而是可能只有几十MB甚至几MB,只包含让Linux内核能够完成初始化、挂载根文件系统、并启动一个基础Shell所必需的最少文件集合。

制作这样一个最小文件系统,远不止是“复制几个文件”那么简单。它要求你对Linux的启动流程、文件系统层次结构标准(FHS)、动态链接库依赖以及设备节点有深刻的理解。这个过程,是理解Linux从“内核”到“可用系统”这一飞跃的关键。无论是为新的ARM开发板构建基础运行环境,还是为定制化的IoT设备创建轻量级系统,亦或是进行内核和驱动模块的独立测试,掌握最小文件系统的制作都是一项基本功。

网络上有很多关于BuildrootYocto或者直接使用busybox构建根文件系统的教程,但很多都停留在步骤罗列。这篇文章,我将从一个资深嵌入式开发者的角度,结合我踩过的无数个坑,手把手带你从零构建一个能在真实硬件(以QEMU模拟器为例,原理通用)上跑起来的最小文件系统,并完成烧写。我们会深入每一个步骤背后的“为什么”,而不仅仅是“怎么做”。

2. 核心思路与工具选型解析

在动手之前,明确目标和选择正确的工具至关重要。我们的目标是:制作一个包含init程序(通常是busybox)、基础工具集、必要的配置文件和设备节点的最小根文件系统(rootfs),并将其打包成适合目标平台烧写的镜像文件。

2.1 为什么选择BusyBox作为核心?

BusyBox被称为“嵌入式Linux的瑞士军刀”,它是这个项目的绝对核心。一个完整的GNU工具集(ls,cp,mkdir,vi,sh等)体积庞大,而BusyBox将这些常用工具的精简版实现,通过符号链接的方式,集成到一个单一的可执行文件中。这带来了两个巨大优势:

  1. 体积极致减小:一个静态链接的busybox二进制文件可能只有1-2MB,却提供了上百个常用命令。
  2. 一致性高:所有工具共享相同的代码库,行为一致,减少了兼容性问题。

注意BusyBox虽然功能强大,但它是“精简版”。例如,它的vi编辑器功能非常基础,它的shell(ash) 可能不支持某些Bash的高级特性。在追求最小化的场景下,这些妥协是完全可接受的。

2.2 开发环境与目标平台准备

为了确保过程的通用性和可复现性,我们将在x86_64的Linux主机上进行交叉编译和制作,但最终的文件系统是面向ARM架构的。这里使用QEMU来模拟ARM硬件环境进行测试,其原理与烧写到真实SD卡或Flash完全一致。

主机环境准备:

# 以Ubuntu/Debian为例,安装必要的工具 sudo apt-get update sudo apt-get install -y gcc-arm-linux-gnueabihf qemu-user-static binfmt-support \ build-essential libncurses5-dev libssl-dev bc flex bison libelf-dev \ git make gcc g++ cpio unzip rsync
  • gcc-arm-linux-gnueabihf: ARM硬浮点交叉编译工具链,用于编译ARM架构的二进制文件。
  • qemu-user-static: 关键工具!它允许我们在x86主机上直接运行ARM架构的可执行文件。这在制作文件系统时,用于在chroot环境下执行目标架构的busybox进行配置。
  • binfmt-support: 配合QEMU,自动识别并调用对应模拟器来运行不同架构的二进制文件。

创建工作目录:

mkdir -p ~/minimal_rootfs cd ~/minimal_rootfs export ROOTFS_DIR=$(pwd)/rootfs mkdir -p $ROOTFS_DIR

保持工作目录清晰,所有操作都在~/minimal_rootfs下进行。

3. 构建最小根文件系统详解

接下来,我们一步步填充$ROOTFS_DIR目录,构建出完整的根文件系统树。

3.1 获取与编译BusyBox

首先下载并编译BusyBox。建议使用长期支持版本,稳定性更好。

# 1. 下载BusyBox源码 (以1.36.1为例) wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar -xjf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 # 2. 配置BusyBox make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig # defconfig是默认配置,已经包含了最常用的工具。 # 如果需要进一步裁剪(比如去掉你不需要的命令),可以运行 menuconfig 进行图形化配置: # make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig # 在 menuconfig 中,可以搜索并禁用特定命令。对于最小系统,默认配置通常足够。 # 3. 编译BusyBox # 我们编译成静态链接,这样busybox就不依赖外部的动态库,可以独立运行,进一步简化根文件系统。 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig # 进入菜单后,找到 Settings -> Build static binary (no shared libs) 并选中它。 # 保存退出后,继续编译。 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install CONFIG_PREFIX=$ROOTFS_DIR

编译安装完成后,你会看到$ROOTFS_DIR目录下出现了bin,sbin,usr/bin,usr/sbin等目录,里面都是指向busybox的符号链接,以及linuxrc文件(一个指向busybox的链接,是传统的初始化脚本)。

3.2 构建根文件系统基础目录结构

一个符合FHS标准的根文件系统需要一些必要的目录。busybox的安装已经创建了一部分,我们还需要补充。

cd $ROOTFS_DIR mkdir -p dev etc home lib proc sys run tmp var/log mnt root opt chmod 1777 tmp # 设置/tmp目录的粘滞位,保证用户只能删除自己的文件
  • /dev: 设备文件目录,内核会将检测到的设备节点创建在这里。
  • /etc: 系统配置文件目录。
  • /proc/sys: 虚拟文件系统,内核通过它们向用户空间暴露系统信息和配置接口。必须为空目录,系统启动后由内核自动挂载。
  • /tmp: 临时文件目录。
  • /lib: 存放动态链接库。由于我们使用了静态编译的busybox,这个目录暂时可以空着,但为未来可能的动态链接程序预留。

3.3 创建设备节点

在Linux中,一切皆文件,硬件设备也以文件形式存在于/dev目录下。系统启动时,内核需要至少两个关键设备节点:

  1. /dev/console: 系统控制台,用于内核和init进程输出信息。
  2. /dev/null: 空设备,丢弃所有写入的数据,读取时立即返回EOF。

devtmpfs(由内核在启动早期挂载的临时/dev文件系统)完全生效前,我们需要手动创建这些节点。更可靠的做法是使用mknod命令,但注意这些节点的majorminor设备号是固定的。

sudo mknod -m 622 $ROOTFS_DIR/dev/console c 5 1 sudo mknod -m 666 $ROOTFS_DIR/dev/null c 1 3
  • c表示字符设备。
  • 5 1/dev/console的主次设备号。
  • 1 3/dev/null的主次设备号。
  • 使用sudo是因为创建设备节点通常需要root权限。

实操心得:在现代内核中,如果内核配置了CONFIG_DEVTMPFS并启用了devtmpfs挂载,理论上可以不用手动创建这些节点,内核会自动创建。但手动创建是最保险的做法,可以避免因内核配置或启动参数问题导致系统因找不到控制台而卡死。我曾在早期内核上因为漏了/dev/console,导致系统启动后一片漆黑,没有任何输出,调试起来极其痛苦。

3.4 编写关键的初始化配置文件

/etc目录下的配置文件决定了系统启动后的行为。

1. 创建/etc/inittab文件inittabbusyboxinit进程读取的初始化表,定义了系统启动、关闭和不同运行级别下的动作。

cat > $ROOTFS_DIR/etc/inittab << EOF # /etc/inittab # This defines what to do on each runlevel. # The format: <id>:<runlevels>:<action>:<process> # 系统启动时,首先执行sysinit动作 ::sysinit:/etc/init.d/rcS # 在tty1上启动一个getty(登录管理器), respawn表示如果退出则重新启动 tty1::respawn:/sbin/getty -L tty1 0 vt100 # 按下Ctrl-Alt-Del时执行的动作 ::ctrlaltdel:/sbin/reboot # 系统关机时执行 ::shutdown:/sbin/swapoff -a ::shutdown:/bin/umount -a -r ::shutdown:/sbin/reboot -f EOF

这个简单的inittab做了三件事:系统初始化时运行一个脚本(rcS);在第一个虚拟终端上启动一个登录提示;定义了关机重启的快捷键和流程。

2. 创建/etc/init.d/rcS初始化脚本这是一个Shell脚本,在系统启动的sysinit阶段执行,用于挂载文件系统、配置网络、设置主机名等。

mkdir -p $ROOTFS_DIR/etc/init.d cat > $ROOTFS_DIR/etc/init.d/rcS << 'EOF' #!/bin/sh # /etc/init.d/rcS # This script is executed at system startup. # 挂载 proc 和 sysfs 虚拟文件系统 mount -t proc none /proc mount -t sysfs none /sys # 挂载 devtmpfs, 让内核自动管理设备节点 mount -t devtmpfs none /dev # 为了方便,也挂载 tmpfs 到 /tmp 和 /run mount -t tmpfs none /tmp mount -t tmpfs none /run # 创建一些必要的设备节点(冗余操作,确保存在) mknod -m 622 /dev/console c 5 1 2>/dev/null mknod -m 666 /dev/null c 1 3 2>/dev/null # 设置主机名 hostname -F /etc/hostname # 配置环回网络接口 ifconfig lo 127.0.0.1 up # 打印欢迎信息 echo "=========================================" echo " Minimal Linux RootFS is up and running!" echo "=========================================" EOF chmod +x $ROOTFS_DIR/etc/init.d/rcS # 不要忘记添加执行权限!

3. 创建其他必要配置文件

# 主机名文件 echo "minimal-linux" > $ROOTFS_DIR/etc/hostname # 主机文件,至少需要 localhost 映射 cat > $ROOTFS_DIR/etc/hosts << EOF 127.0.0.1 localhost localhost.localdomain ::1 localhost localhost.localdomain EOF # 创建基本的用户和组文件 # 对于最小系统,可以直接从主机复制简化版的 passwd 和 group 文件,或者手动创建。 cat > $ROOTFS_DIR/etc/passwd << EOF root::0:0:root:/root:/bin/sh EOF cat > $ROOTFS_DIR/etc/group << EOF root:x:0: EOF

3.5 处理动态链接库(可选但重要的步骤)

虽然我们的busybox是静态链接的,但如果你未来想在这个最小系统上运行其他动态链接的程序(比如你自己写的测试程序),就需要将对应的库文件拷贝过来。

首先,找到你的交叉编译工具链的库路径:

# 通常类似 /usr/arm-linux-gnueabihf/lib/ TOOLCHAIN_LIB_PATH=$(arm-linux-gnueabihf-gcc -print-sysroot)/lib

然后,使用ldd命令查看你的程序依赖哪些库,再手动拷贝。但更通用的方法是,把最基础的C库(如glibcuclibc)拷贝过来。

# 假设我们使用glibc,拷贝基础库(注意:这会显著增加文件系统大小) cp -a $TOOLCHAIN_LIB_PATH/libc.so.6 $ROOTFS_DIR/lib/ cp -a $TOOLCHAIN_LIB_PATH/ld-linux-armhf.so.3 $ROOTFS_DIR/lib/ # 你可能还需要 libpthread, libm, librt, libdl 等,取决于你的应用。 # 可以使用一个脚本来自动拷贝程序的所有依赖库,这里不展开。

注意事项库的版本和架构必须严格匹配。从错误工具链或主机系统拷贝库文件,会导致目标板无法启动或程序运行时出现“非法指令”错误。在不确定的情况下,优先使用静态编译来避免库依赖问题。

4. 文件系统镜像制作与烧写

现在,我们有了一个完整的根文件系统目录树($ROOTFS_DIR)。接下来需要把它打包成一个镜像文件,方便烧写到存储设备。

4.1 制作EXT4文件系统镜像

EXT4是Linux上最常用的日志文件系统之一,稳定且性能良好。

cd ~/minimal_rootfs # 1. 创建一个指定大小的空镜像文件 (例如 64MB) dd if=/dev/zero of=rootfs.ext4 bs=1M count=64 # 2. 在这个空文件上创建EXT4文件系统 mkfs.ext4 rootfs.ext4 # 3. 创建一个临时挂载点,并将镜像文件挂载上去 mkdir -p mnt_point sudo mount -o loop rootfs.ext4 mnt_point # 4. 将我们制作好的根文件系统内容拷贝到挂载点 sudo cp -a $ROOTFS_DIR/* mnt_point/ # 5. 同步数据并卸载 sync sudo umount mnt_point # 6. 检查并修复文件系统 (e2fsck) sudo e2fsck -p -f rootfs.ext4 # 7. 调整镜像文件大小为实际内容大小,节省空间 (可选) sudo resize2fs -M rootfs.ext4

现在,rootfs.ext4就是一个包含了我们最小系统的、可以直接被内核挂载的EXT4格式镜像文件。

为什么是EXT4?因为它支持日志,在意外断电时能更好地保护数据完整性,且内核支持完善。对于更小的系统,也可以考虑EXT2(无日志)或initramfs(内存文件系统)。

4.2 使用QEMU进行本地验证

在烧写到真实硬件前,强烈建议用QEMU进行验证。我们需要一个匹配的Linux内核。

1. 获取或编译一个简单的ARM内核:你可以使用主线内核,但配置较复杂。这里推荐使用为QEMU预编译的vexpress开发板内核(一种ARM虚拟平台)。

# 下载预编译的内核和dtb (设备树文件) wget https://releases.linaro.org/components/kernel/linux-linaro-stable/latest/linux-linaro-stable-vexpress.tar.xz tar -xf linux-linaro-stable-vexpress.tar.xz # 假设解压后得到 zImage 和 vexpress-v2p-ca9.dtb 文件

2. 使用QEMU启动测试:

qemu-system-arm -M vexpress-a9 -m 512M -kernel path/to/zImage \ -dtb path/to/vexpress-v2p-ca9.dtb \ -append "root=/dev/mmcblk0 rw console=ttyAMA0" \ -drive file=rootfs.ext4,if=sd,format=raw \ -nographic

参数解释:

  • -M vexpress-a9: 指定模拟的机器类型。
  • -m 512M: 分配512MB内存。
  • -kernel-dtb: 指定内核镜像和设备树二进制文件。
  • -append: 传递给内核的命令行参数。root=/dev/mmcblk0指定根文件系统在模拟的SD卡上,rw表示可读写,console=ttyAMA0指定控制台设备。
  • -drive file=rootfs.ext4,if=sd,format=raw: 将我们制作的rootfs.ext4作为一块raw格式的SD卡挂载给虚拟机。
  • -nographic: 不使用图形界面,将串口输出到当前终端。

如果一切顺利,你将看到内核启动日志,最后出现/etc/init.d/rcS脚本中的欢迎信息,并提示minimal-linux login:。输入root(无需密码)即可登录进入我们制作的最小系统!

4.3 烧写到物理介质(SD卡或Flash)

通过QEMU验证无误后,就可以烧写到真实硬件了。以最常见的SD卡为例。

1. 识别SD卡设备:将SD卡插入读卡器并连接到电脑。使用lsblkdmesg | tail命令确认SD卡对应的设备名,例如/dev/sdb请务必确认,选错会格式化你的硬盘!)。

2. 烧写镜像:

# 假设SD卡是 /dev/sdb sudo dd if=rootfs.ext4 of=/dev/sdb bs=4M status=progress sync

dd命令将镜像文件逐块写入SD卡。bs=4M设置块大小,可以提高大文件写入速度。status=progress显示写入进度。

3. 扩展分区(可选):由于我们制作的镜像是固定大小(如64MB),而SD卡容量可能更大。烧写后,SD卡上只有一个64MB的分区。你可以使用gparted工具或resize2fs命令,在首次启动后在线扩展根文件系统以使用全部SD卡空间。

踩坑实录dd命令非常危险。有一次我在深夜加班,误将/dev/sda(系统盘)当作SD卡设备,一个命令下去,工作站系统瞬间崩溃。务必反复核对of=参数后的设备名!一个保险的做法是,先umount掉SD卡的所有自动挂载分区,再用lsblk确认哪个设备消失了,那个就是你的SD卡。

5. 常见问题排查与深度优化

即使按照步骤操作,你也可能会遇到各种问题。这里记录一些典型的“坑”和解决方案。

5.1 系统启动失败问题排查表

现象可能原因排查步骤与解决方案
内核panic,提示“VFS: Unable to mount root fs”1. 内核命令行参数root=设置错误。
2. 内核缺少对应文件系统驱动(如EXT4)。
3. 根文件系统镜像损坏或格式不对。
1. 检查QEMU或bootloader传递的root=参数,确保指向正确的设备(如/dev/mmcblk0p1)。
2. 在内核编译时,确保勾选了CONFIG_EXT4_FS=y(或你使用的文件系统)。
3. 在主机上用fsck检查镜像,并确认是用mkfs.ext4正确格式化的。
内核启动后卡住,无任何输出1. 控制台设备console=参数错误或缺失。
2./dev/console设备节点缺失。
3.init进程(即busybox)无法执行。
1. 确保内核参数包含console=ttyAMA0(针对QEMU ARM)或对应硬件的串口设备。
2. 检查根文件系统/dev/console是否存在且主次设备号正确。
3. 检查busybox是否被正确安装,且是静态链接、ARM架构。用file $ROOTFS_DIR/bin/busybox确认。
可以启动,但提示“/bin/sh: not found”或“Kernel panic - not syncing: No working init found.”1.init程序路径错误。
2. Busybox依赖的动态库缺失。
3. 文件系统权限错误。
1. 内核参数可以指定init=,默认是/sbin/init,/etc/init,/bin/sh。确保busybox安装在了这些路径之一,或使用init=/bin/busybox指定。
2. 如果busybox是动态链接,必须将工具链中的库文件拷贝到/lib下。强烈建议使用静态编译
3. 确保busybox二进制文件具有可执行权限(chmod +x)。
登录后,命令无法执行(如ls, cat)Busybox的符号链接未正确创建。$ROOTFS_DIR中,检查/bin/ls是否是指向../bin/busybox的符号链接。busyboxmake install应该已经创建了它们。如果没有,可以进入busybox源码目录,执行make CONFIG_PREFIX=/path/to/rootfs install重新安装。
系统时间错误,或hwclock命令不可用最小系统缺少时间设置功能和RTC驱动。这是一个正常现象。最小系统通常不包含复杂的硬件时钟管理。可以通过网络时间协议(NTP)来更新时间,但这需要网络支持。对于调试,可以手动用date -s设置。

5.2 进阶优化与裁剪技巧

制作出能启动的系统只是第一步,嵌入式开发中,我们常常需要进一步优化。

1. 使用initramfs(内存文件系统)initramfs是一个被编译进内核或作为独立镜像加载到内存中的cpio归档文件。系统启动初期,内核将其解压到内存中的一个tmpfs中,并作为初始根文件系统。它的优势是:

  • 速度快:完全在内存中运行,访问速度极快。
  • 无需驱动:在挂载真正的根文件系统(如来自SD卡)之前,就可以提供必要的工具和驱动模块。 制作方法:
cd $ROOTFS_DIR find . | cpio -H newc -ov --owner root:root > ../initramfs.cpio cd .. gzip initramfs.cpio # 在内核配置中,指定 Initramfs source file 为这个 .cpio.gz 文件即可。

2. 使用Buildroot自动化构建对于复杂的项目,手动制作文件系统效率低下。Buildroot是一个自动化构建嵌入式Linux系统的框架。你只需要通过make menuconfig进行配置(选择架构、工具链、需要的软件包等),它就能自动下载、交叉编译、集成,最终生成包括内核、根文件系统在内的完整镜像。它管理了复杂的依赖关系,是产品级开发的必备工具。

3. 极致的体积裁剪

  • 编译选项优化:在busyboxmenuconfig中,仔细检查每一个命令,去掉绝对不需要的。例如,如果你不需要vi,可以去掉它。
  • 使用uclibcmusl-libc:它们比glibc体积小得多,是嵌入式系统的常见选择。Buildroot可以方便地切换C库。
  • 删除调试符号:使用交叉编译工具链中的strip命令,去掉二进制文件中的调试符号。
    arm-linux-gnueabihf-strip $ROOTFS_DIR/bin/busybox
  • 压缩文件系统:使用squashfs等只读压缩文件系统,可以极大减小镜像体积,适合系统分区。

从手动制作一个最小文件系统开始,你才能真正理解Linux用户空间的构成和启动脉络。这个过程会强迫你去了解inittabrcS、设备节点、动态链接、文件系统格式等基础知识。当你在QEMU中看到自己构建的系统成功启动,或者在开发板上通过串口看到登录提示时,那种成就感是无可替代的。这不仅仅是完成了一个任务,更是打通了嵌入式Linux开发中“内核”与“应用”之间的关键桥梁。后续无论是转向Buildroot/Yocto这样的自动化工具,还是调试复杂的系统启动问题,这段亲手构建的经验都会成为你最坚实的底气。

← 返回列表