十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

从原理到实战:用BusyBox构建嵌入式Linux根文件系统

从原理到实战:用BusyBox构建嵌入式Linux根文件系统 BusyBox 这名字玩嵌入式 Linux 的人应该都不陌生江湖人称“嵌入式 Linux 的瑞士军刀”。但说实话很多人对它的理解就停在“一个压缩了很多命令的小程序”这个层面真正问起来它为什么能这么小、怎么把一套根文件系统跑起来、跟 glibc 里的那些命令到底啥关系能讲清楚的人就不多了。这篇文章我就从原理讲到实战带着你从零把一个可用的 BusyBox 根文件系统做出来再把它跟内核、开发板串起来跑一遍把那些“文档里不写、教程里略过”的坑也一并填了。这篇文章适合谁刚入门嵌入式 Linux、被各种镜像和文件系统概念绕晕的新手或者已经能编译内核但一碰根文件系统就发怵的同学。看完你能搞明白三件事BusyBox 到底凭什么一个二进制就能顶几十个命令一个能启动的根文件系统需要哪些“内脏”在真实板子上调试时最常踩的坑都是什么。1. 内容整体设计与思路拆解先聊点底层逻辑。BusyBox 的核心设计理念非常粗暴把所有常用的 Unix 命令做成一个多调用二进制multi-call binary然后用符号链接symlink或者 shell 内置调用的方式让不同的命令名都指向同一个可执行文件。程序启动后它先看自己被调用时用的名字是ls还是cp再进入对应的函数入口。就这么简单但带来的收益极其可观。1.1 为什么需要 BusyBox 而不是直接用完整工具集嵌入式设备的存储空间很宝贵Flash 动不动就 16MB、32MB一颗 NOR Flash 甚至只有几 MB。完整的 GNU Coreutils 全部装进去光/bin和/usr/bin就能吃掉几十 MB再加上 shell、init、mount 这些基础组件还没放应用程序呢存储就告急了。BusyBox 单一个静态编译的二进制一般也就 500KB 到 1MB 左右里面却集成了 ls、cp、mv、rm、sh、init、mount、ifconfig、ping 等几百个命令的“精简版”。这一下就把根文件系统的“体重”控制在了可接受范围内。拿我做的一个项目举例整套根文件系统我做出来后压缩成.tar.gz才 2.1MB解压开也就 6MB 出头放在 8MB 的 SPI Flash 里绰绰有余。这要是换完整工具集光动态库依赖就够你头疼的。1.2 多调用二进制的实现原理简析BusyBox 的 main 函数里有一个分发表applet table每个命令对应一个名字和函数指针。启动时通过argv[0]解析出命令名然后去表里查找到就直接调用相应的applet_main。你在系统里看到的ls、cp本质都是指向/bin/busybox的符号链接。# 在 BusyBox 根文件系统里查看效果类似这样 $ ls -l /bin/ls lrwxrwxrwx 1 root root 7 Jan 1 00:00 /bin/ls - /bin/busybox所以当你敲ls的时候内核加载的其实是同一个 busybox 二进制只是 argv[0] 不一样BusyBox 内部就自动“变身”成了 ls。这就是它体积小的核心原因——代码重用达到了极致不需要为每个命令单独维护一个 ELF 文件头、动态链接信息所有命令的逻辑都在同一个进程空间里。1.3 方案选型静态编译还是动态编译我在实际项目里强烈建议静态编译CONFIG_STATICy。动态编译省空间但代价是目标板上必须有一套匹配的 C 库glibc 或 musl和动态链接器而且版本要严格对应。交叉编译环境里 glibc 的版本如果跟板子上的库不一样程序跑起来就各种version GLIBC_2.34 not found排查起来非常折磨。静态编译后BusyBox 不依赖任何外部库文件拷贝到板子上就能直接跑。代价就是二进制大一点但换来的是“一把梭”的省心。特别是你第一次调试根文件系统问题本来就多要再叠加一个动态库缺失的变量心态很容易崩。2. 核心细节解析与实操要点做根文件系统之前得先把 Linux 用户空间启动的几个关键点理顺。根文件系统不是“弄一堆文件夹和命令就行”它有一套固定的启动协作机制任何一个环节脱节都会导致启动卡住。2.1 内核启动用户空间的交接点内核启动到最后阶段会做两件事挂载根文件系统然后执行根文件系统里的 “init” 程序。init 程序的路径是内核启动参数rdinitinitramfs 场景或init真实块设备场景指定的。如果没指定内核默认会按/sbin/init、/etc/init、/bin/init、/bin/sh的顺序去找。这里就出现第一个经典问题很多人把 BusyBox 里的 init 路径搞错。BusyBox 编译安装后init 通常在/sbin/init它其实是一个指向 busybox 的符号链接。如果你内核启动参数写的是init/bin/sh那就完全不经过 init 初始化直接进 shell虽然也能用但 /dev 下的设备节点、主机名、profile 环境变量都不会帮你设置。刚开始调试可以这么干真正做成产品必须走完整的 init 流程。2.2 init 进程与 inittab 的配合逻辑BusyBox init 启动后会读取/etc/inittab文件这个文件定义了系统初始化要跑哪些脚本、哪些进程需要常驻、哪些是按下 CtrlC 就终止的“询问型”程序。默认/etc/inittab不存在时BusyBox 会有一个默认行为但这个默认行为比较简陋通常只保证能启动/etc/init.d/rcS和一个 shell。我习惯在自己的 inittab 里写这几行::sysinit:/etc/init.d/rcS ::askfirst:-/bin/sh ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -rsysinit内核 init 阶段最先执行的系统初始化脚本一般放挂载 proc、sysfs、devtmpfs配置网络等操作。askfirst在串口上显示“Please press Enter to activate this console”按回车才给 shell防止误操作。ctrlaltdel捕获 CtrlAltDel 组合键做重启。shutdown关机时卸载文件系统这对 SD 卡、U 盘这类块设备很重要避免数据损坏。2.3 设备节点的三种管理方式根文件系统要能正常用/dev必须准备好。常见做法有三种我分别说下devtmpfs 自动挂载内核编译时开启CONFIG_DEVTMPFSy和CONFIG_DEVTMPFS_MOUNTy内核启动时会自动挂载 devtmpfs 并自动创建设备节点。这是最省事的方式我强烈推荐。mdev 动态管理BusyBox 内置的 mdev 是 udev 的简化版通过内核 uevent 机制在热插拔时创建/删除节点还要在 rcS 里写入echo /sbin/mdev /proc/sys/kernel/hotplug并执行mdev -s扫描已有设备。手动静态创建用mknod提前创建好设备节点跟着文件系统打包走。适合设备固定、内核自制的情况但每次加设备都要改文件系统很不灵活。新手最容易犯的错是内核开了 devtmpfs同时又用 mdev 去管理同一批设备两边打架出现莫名其妙的 “Device or resource busy”。我自己是默认只开 devtmpfs热插拔需求多的时候再叠加 mdev。2.4 shell 与可选组件的取舍BusyBox 的 shell 比完整 bash 精简得多但这不意味着它弱。BusyBox 里默认的 sh 是 ash 的一个变种支持常用语法、变量、函数、通配符写个部署脚本完全够用。有时脚本写得比较野用了[[ ]]、${var^^}这种 bash 高级特性BusyBox sh 会直接报语法错误。此时有两个选择一个是改脚本用 POSIX 兼容语法重写另一个是编译时把 CONFIG_BASH_IS_ASH 或 CONFIG_FEATURE_* 开起来但别指望它真的 100% 兼容 bash。还有一堆实用小工具比如awk、sed、grep的 BusyBox 版都保留但正则引擎是简化版某些复杂正则不支持。我踩过最深的坑是sed -i在某些版本上不支持后来统一改用sed s/xxx/yyy/ file tmp mv tmp file的方式稳得一批。3. 实操过程与核心环节实现理论说够了直接动手。下面我按自己的习惯完整走一遍根文件系统的制作流程。这里用最通用的方法交叉编译 BusyBox、手工搭建目录结构、chroot 验证、然后打包成镜像。3.1 环境准备与交叉编译工具链建议用一个独立的 Linux 机器虚拟机也行做宿主环境交叉编译工具链用 arm-linux-gnueabihf-gcc 或者 aarch64-linux-gnu-gcc取决于你的板子架构。下面以 32 位 ARM 为例。# 安装交叉编译工具链Ubuntu/Debian 系 sudo apt-get update sudo apt-get install -y build-essential flex bison libncurses-dev \ bc u-boot-tools tree sudo apt-get install -y gcc-arm-linux-gnueabihf这一步别用太老的工具链太老的 binutils 对某些新内核的编译选项支持有问题。用发行版仓库自带的就行。接着下载 BusyBox 源码我建议直接用 git 拉稳定分支不要用网上不知道从哪转存的神秘版本。git clone git://busybox.net/busybox.git cd busybox git checkout 1_36_stable3.2 配置 BusyBox 的编译选项BusyBox 的配置菜单跟内核类似# 先清理默认配置 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- distclean # 生成默认配置 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- defconfig # 进入菜单配置 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfigmenuconfig 里我重点开这几个选项Settings - Build static binary (no shared libs)选上[*]Settings - Destination path for make install填你要安装的目录前缀比如${HOME}/rootfsSettings - vi-style line editing commands如果你习惯 vi 键位可以开Coreutils - cp、mv、rm默认都有不用动Linux System Utilities - mdev建议打开CONFIG_FEATURE_MDEV_CONF这样支持/etc/mdev.conf做自定义规则注意在 menuconfig 里填安装路径容易手滑写错我的习惯是编译的时候用make install CONFIG_PREFIX/path/to/rootfs临时指定这样更灵活。配置完成后直接编译make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc) make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- install CONFIG_PREFIX$HOME/rootfs执行完$HOME/rootfs下会出现bin、sbin、usr目录里面塞满了指向 busybox 的符号链接。看一眼ls -l $HOME/rootfs/bin/ls确认链到了bin/busybox。3.3 搭建完整的根文件系统目录骨架编译安装后只是把 BusyBox 相关文件放进去一个能启动的根文件系统还需要其他目录。手动建一下cd $HOME/rootfs mkdir -p etc/init.d etc/network proc sys dev tmp mnt opt var/log home/root目录建好后接下来是/etc/inittab和/etc/init.d/rcS脚本。这一步很多新手会漏结果启动后卡在 “Kernel panic - not syncing: Attempted to kill init!”。rcS 脚本内容我习惯写成这样#!/bin/sh # 挂载内核虚拟文件系统 mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev # 设置主机名 hostname myboard # 配置 loop 设备便于挂载镜像文件测试 mkdir -p /dev/pts mount -t devpts none /dev/pts # 如果需要 mdev 管理设备节点取消下面两行注释 # echo /sbin/mdev /proc/sys/kernel/hotplug # mdev -s这里有个细节mount -t devtmpfs none /dev内核必须开CONFIG_DEVTMPFS否则会报mount: mounting none on /dev failed: No such device。如果内核没开那你就要用 mdev 的方式或者提前通过 mknod 创建/dev/console和/dev/null。还有一点console设备必须存在。就算你偷懒不建任何设备节点至少也得有/dev/console否则内核 init 阶段无法打开控制台启动会直接失败。3.4 配置系统启动必需的 etc 文件除了 inittab 和 rcS还要处理几个经典的 etc 文件。/etc/fstab主要用于mount -aBusyBox 的 mount 支持读它。我一般这样写# device mountpoint type options dump pass proc /proc proc defaults 0 0 sysfs /sys sysfs defaults 0 0 devtmpfs /dev devtmpfs defaults 0 0/etc/passwd和/etc/group也不可少不然一些程序在解析用户 ID 时会怪怪的。最简写法root:x:0:0:root:/root:/bin/sh daemon:x:1:1:daemon:/usr/sbin:/bin/false/etc/group同理root:x:0: daemon:x:1:再补一个/etc/shadow把 root 密码留空或用!锁定默认不允许登录等你需要时再设置。刚开始调试我都是直接通过串口 askfirst 进 shell不需要密码所以 shadow 里写root:*:0:0:99999:7:::就行。3.5 用 chroot 在本机验证根文件系统没有板子的时候怎么验证这套文件系统能不能用chroot是最好用的工具。因为我是 x86 主机交叉编译 ARM 的根文件系统不能直接 chroot 执行 ARM 二进制所以需要先安装 qemu-user-static。sudo apt-get install -y qemu-user-static sudo cp /usr/bin/qemu-arm-static $HOME/rootfs/usr/bin/ sudo chroot $HOME/rootfs /bin/sh如果能进入一个 shell说明 BusyBox 动态链接没问题、文件系统基本结构没问题。这时候你可以试试ls /bin、mount、cat /proc/version这些命令看看输出是不是正常。chroot 里最容易被忽略的是/proc没挂载导致ps、mount这类依赖 proc 的命令行为异常。所以在 chroot 之前先执行sudo mount -t proc /proc $HOME/rootfs/proc sudo mount -t sysfs /sys $HOME/rootfs/sys sudo mount --bind /dev $HOME/rootfs/dev用 qemu-user-static 验证完后记得把qemu-arm-static从 rootfs 里删掉别把它打进最终镜像否则生产环境里多一个没用的文件不说还可能被人恶意利用。3.6 打包成可烧写的镜像文件验证没问题后打包成 ext4 镜像或者 tar.gz。我比较常用 ext4 镜像因为可以直接用dd写到 SD 卡分区也能给 QEMU、U-Boot 用。# 先生成一个 64MB 的空镜像 dd if/dev/zero ofrootfs.ext4 bs1M count64 # 格式化成 ext4 mkfs.ext4 -F rootfs.ext4 # 挂载并拷贝文件 mkdir -p /mnt/rootfs sudo mount -o loop rootfs.ext4 /mnt/rootfs sudo cp -a $HOME/rootfs/* /mnt/rootfs/ sudo umount /mnt/rootfs如果内核用 initramfs 方式加载根文件系统那就是另一套玩法。把 rootfs 打成 cpio 格式cd $HOME/rootfs find . | cpio -o -H newc ../rootfs.cpio gzip -9 ../rootfs.cpio # 得到 rootfs.cpio.gz可直接编进内核或由 bootloader 加载提示cpio 打包要特别注意文件权限和符号链接建议在干净的 rootfs 目录里执行并且用find . | cpio而不是cpio -a直接打包源目录避免把宿主机多余文件带进去。3.7 让内核找到并挂载根文件系统这一步是把根文件系统真正“用起来”的关键。以 U-Boot 启动 ext4 镜像为例要保证内核启动参数里把 root 指定为根分区。假设 SD 卡的第二个分区是 ext4 根文件系统setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rw rootwait init/sbin/initroot/dev/mmcblk0p2根文件系统所在分区rw以读写方式挂载千万别生产环境也这样干但这阶段调试很方便rootwait等待设备节点出现再挂载对 SD 卡、USB 设备非常关键不然内核启动太快、设备还没枚举完就挂载会直接 panicinit/sbin/init显式指定 init 程序防止默认查找顺序出问题如果是 initramfsbootargs 要加rdinit/sbin/init并且内核配置里打开CONFIG_BLK_DEV_INITRDy。3.8 给根文件系统集成 DropbearSSH 服务做产品时串口调试只是起步很多时候必须要 SSH 远程登录。BusyBox 本身不带 SSH 服务端通常搭配 Dropbear。我这块也踩过不少坑简单说下流程。交叉编译 Dropbeargit clone https://github.com/mkj/dropbear.git cd dropbear ./configure --hostarm-linux-gnueabihf --disable-zlib \ --disable-pam --disable-lastlog make PROGRAMSdropbear dropbearkey scp \ MULTI1 STATIC1生成的dropbearmulti静态二进制拷贝到/usr/sbin/或者/usr/local/sbin/。再创建密钥目录mkdir -p /etc/dropbear dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key在 rcS 里加上/usr/sbin/dropbear -R-R表示密钥不存在时自动生成但这只是临时方案正式产品还是建议提前生成好密钥避免每次启动都变。Dropbear 集成进去后wifi 或者网线一连就能远程 SSH 进来操作调试体验瞬间起飞。4. 常见问题与排查技巧实录这一节是精华中的精华。我把做根文件系统时最容易翻车的几个问题列出来每个都附上排查思路这些经验都是我一点一点踩坑踩出来的。4.1 启动卡在 Kernel panic - not syncing: VFS: Unable to mount root fs这个错误基本宣告“内核找不到或不认识根文件系统”。排查顺序先确认 bootargs 里的root设备写没写对。SD 卡是mmcblk0p2还是mmcblk1p2U 盘是sda1还是sdb1不同板子不一样。在 U-Boot 里执行mmc list、part list mmc 0看清楚。确认内核有没有编译对应的文件系统驱动。ext4 要开启CONFIG_EXT4_FSy如果做成 initramfs 还要CONFIG_BLK_DEV_INITRDy。确认有没有开启CONFIG_DEVTMPFS如果没有 devtmpfs且根文件系统里没有/dev/mmcblk0p2节点rootwait等再久也没用。4.2 启动卡在 init 之后没有任何输出如果内核日志显示挂载成功、init 已执行但串口没有任何 shell 提示符多半是控制台配置问题。检查 bootargs 的console是否与板子实际串口匹配比如ttyS0、ttyAMA0、ttymxc0不同平台名字差异非常大。还有波特率年久失修的老板子上我用过 115200也遇到过必须 9600 才能正常显示的情况串口不对就啥都看不见。另一个低级错误是/etc/inittab里askfirst对应的 console 跟实际串口不一致或者 shell 路径写错导致 init 起不了 shell。4.3 shell 脚本执行报 syntax error: unexpected这是 BusyBox 自带 ash 与 bash 语法不兼容的典型表现。最常见的是[[ ... ]]、${var^^}、echo -e这类 bash 特有扩展。解决策略脚本开头写#!/bin/sh而不是#!/bin/bash让系统知道自己走的是 POSIX shell。避免使用[[ ]]换成[ ]加-a/-o或者用case语句。echo -e在 BusyBox 里默认不支持用printf代替。比如判断文件是否存在写成if [ -f /etc/config/network ]; then echo network config found fi而不是if [[ -f /etc/config/network ]]; then echo network config found fi这些坑在写自动化部署脚本、启动脚本时特别容易踩养成写 POSIX 兼容脚本的习惯能省很多事。4.4 time 命令找不到 / ntpdate 没有BusyBox 里有些命令名字或用法与完整版不完全一致。比如开头热搜词里提到的busybox v1.22.1 (kylin1:1.22.0)这就是某发行版内核自带的早期 BusyBox 版本。老版本可能缺少一些后来新增的 applet比如ntpd、time等。遇到这种情况优先确认版本和编译配置busybox --list这条命令会列出当前 BusyBox 支持的所有 applet。如果缺少你要的命令要么换新版本要么手动补一个静态编译的独立工具进去。千万不要觉得自己写的代码少就自己造轮子能用现成的尽量用现成的。4.5 NFS 挂载根文件系统时的权限问题开发阶段我特别推荐用 NFS 挂载根文件系统这样宿主机上改文件、板子上立刻生效免去反复烧写存储介质的痛苦。但 NFS 根文件系统有一个经典问题权限不足或端口不通。宿主机导出目录sudo apt-get install -y nfs-kernel-server sudo mkdir -p /srv/nfs/rootfs sudo cp -a $HOME/rootfs/* /srv/nfs/rootfs/ sudo vim /etc/exports/etc/exports里加一行/srv/nfs/rootfs *(rw,sync,no_subtree_check,no_root_squash)no_root_squash必须加否则开发板上的 root 用户对 NFS 文件系统没有完整权限写文件会报 Permission denied。板子端 bootargssetenv bootargs consolettyS0,115200 root/dev/nfs nfsroot192.168.1.100:/srv/nfs/rootfs,v3,tcp ip192.168.1.50:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off rw init/sbin/initNFS 方式调试根文件系统非常爽但真正产品上一般不用因为依赖网络和服务器稳定性没法保证。我的习惯是开发期 NFS发布期打到 ext4 或 squashfs 镜像里。4.6 根文件系统脏掉之后如何抢救开发过程中经常把 rootfs 搞坏比如权限不对、删了关键库、写错 fstab。这时候别慌有两个骚操作如果板子上有 U-Boot可以先从网络启动一个小 ramdisk比如 Linux 内核自带 initramfs 调试模式或者一个最小 BusyBox 镜像把对应分区挂载上来修复。如果坏的是 ext4 分区拔下 SD 卡在宿主机上用e2fsck修复然后挂载检查文件结构。我最快的抢救路子就是准备一个万能的最小 rootfs.cpio.gz遇到板子起不来就直接用 tftp 加载这个 initramfs进去以后随便折腾原系统分区。建议你也做一个放边上关键时刻能救命。5. 进阶扩展与常见优化方向根文件系统做出来能启动只是第一步往产品化走还有几件事值得做。5.1 用 squashfs 做只读根文件系统嵌入式中很多根文件系统是只读的这种场景 squashfs 是首选。squashfs 是一个高度压缩的只读文件系统常用于恢复分区、路由器固件等。配合 overlayfs 可以把只读根文件系统和可写上层叠加既保证系统不被篡改又能保存配置数据。sudo apt-get install -y squashfs-tools sudo mksquashfs $HOME/rootfs rootfs.squashfs -comp xz生成 rootfs.squashfs 后内核要开启CONFIG_SQUASHFSybootargs 里 root 指向 squashfs 分区再配合 overlayfs 挂载一个可写分区到/etc或/var。5.2 优化 BusyBox 的运行时内存占用有人会问BusyBox 已经这么省了还能不能再压缩能。常见思路编译时开启CONFIG_FEATURE_USE_BSS_TAIL让没用的全局变量放入 BSS 段运行时共享静态编译时能省一部分内存。把不需要的 applet 全部关掉通过 menuconfig 逐项裁剪只保留需要的命令这样二进制体积会进一步缩减。如果芯片内存非常紧张考虑用 musl 替代 glibc 交叉编译动态库和静态程序体积都会小很多。我做过一个极端裁剪的项目根文件系统最终压到 1.2MB 的 squashfs 镜像启动后常驻内存不到 4MB跑一个 MQTT 客户端加串口透传程序毫无压力。5.3 让 BusyBox 集成到产品里的最佳实践产品化阶段我一般这样做用 version 和 applet 列表生成一份命令白名单回头做安全审计时能直接对账。开启CONFIG_BUSYBOX_EXEC_PATH并把它指向/proc/self/exe这样通过PATH调用命令时始终走同一个 BusyBox避免某些命令找不到。把/etc/inittab里的askfirst改为respawn或者去掉防止普通用户随便敲回车就拿到 root shell。关闭不需要的网络相关 applet比如 telnetd、tftpd减小被攻击面。root 密码必须设置SSH key 用 ed25519 并禁止密码登录。这几点看着不起眼但真出安全事故时每一条都可能是救命的防线。6. 写在最后的一点经验做嵌入式 Linux 这几年我最大的体会是根文件系统看着简单但它是内核和用户程序之间的“桥梁”所有上层应用都站在它上面。BusyBox 作为这套体系里最经典的工具不仅解决存储焦虑更重要的是它把 Unix 工具的设计哲学压缩到了一个二进制里。我建议你拿到这篇文章后先别急着抄命令找个 QEMU 模拟器或者一块便宜的学习板从交叉编译 BusyBox 开始一步一步把自己定制的根文件系统跑起来。只有亲手把存储、内核、bootargs、设备节点、启动脚本这一整条链路走通一遍再回头看那些“一行命令打包 rootfs”的教程才算真正看懂了。最后再分享一个小技巧在 rcS 脚本开头加一句exec /dev/kmsg 21这样 init 阶段的所有输出都会进内核日志配合dmesg就能看到启动脚本到底卡在哪一步。这个习惯帮我排查了不下十次“根文件系统起不来”的问题比盲猜串口输出高效得多。
返回列表