news 2026/10/20 9:09:36

嵌入式Linux BusyBox根文件系统完善指南:从能跑到好用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux BusyBox根文件系统完善指南:从能跑到好用

做嵌入式Linux系统移植的朋友应该都有这种经历:U-Boot起来了、内核也打印到Freeing unused kernel memory,结果最后一步却卡在根文件系统上。屏幕上要么是Kernel panic - not syncing: No init found,要么是串口shell死也不出来,连个登录提示符都看不到。我这个系列做到第二十一期,前面已经带着大家把引导程序、内核、BusyBox编译这些大头都过了一遍,今天的主题还是继续填坑:在Ubuntu20.04开发环境下,把手头这块ARM开发板的BusyBox根文件系统从“能跑”完善到“能用”,再打磨到“好用”。

这套完善工作做下来,你会发现根文件系统并不是把BusyBox编完复制过去就结束了。一个能真正启动到shell、能挂网卡、能跑应用程序的根文件系统,需要解决目录结构、设备节点、init流程、动态库依赖、账号配置这一连串问题。每一步都有坑,但每一步也都有固定的排查套路。这篇文章我会按照我实际调试的顺序,把完善BusyBox根文件系统的全过程掰开揉碎讲清楚,包含可直接抄作业的脚本和配置,以及Ubuntu20.04环境下特有的编译和调试注意事项,适合正在做系统移植、刚把BusyBox跑起来但不知道怎么往下走的人。

1. 为什么BusyBox根文件系统需要“完善”

1.1 BusyBox到底给了我们什么

BusyBox本质上是一个“命令集合体”。它把几百个常用Linux命令整合进了一个可执行文件里,通过符号链接或者调用参数来区分到底执行哪个命令。比如你在目标板上敲ls,实际上执行的是/bin/busybox这个程序,BusyBox根据argv[0]判断出你要的是ls。这种设计大幅节省了Flash空间,非常适合嵌入式设备。

但是BusyBox本身只解决“命令从哪来”的问题。它不负责创建/dev/console设备节点,不负责挂载/proc和/sys文件系统,不负责配置网卡IP,也不负责告诉你它编译时依赖了哪个版本的glibc。举个不太恰当但贴切的例子:BusyBox像是一套多功能工具箱,箱子本身倒是挺全,但你的毛坯房里电没通、水没接、门锁还是坏的,工具再全也住不进去。这个“毛坯房装修”的过程,就是本篇要讲的根文件系统完善工作。

1.2 完善工作的优先级规划

我在做完第一批BusyBox构建实验后,给自己定了一个完善顺序,这个顺序后来成了我做其他板子的标准流程:

第一优先级是让系统能启动。具体来说就是目录结构齐全、/dev/console设备节点存在、/etc/inittab正确、init进程能顺利执行rcS脚本。这个阶段的目标是上电能到shell提示符。

第二优先级是让系统能用起来。网络要通,动态库要补齐,root能登录,主机名和DNS要设置好,应用程序依赖的库文件不能报错。

第三优先级才是让系统好用。环境变量、PATH路径、快捷脚本、开机自启动服务这些,都是在前面两步稳定之后才去碰的。

很多人一上来就折腾/etc/profile和自启动脚本,结果设备根本没到那个阶段就panic了,排查半天还以为是脚本写错,其实是设备节点没创建。所以我强烈建议按“先启动、再功能、后体验”的顺序来,一步一验证,别跳步。

2. 基础目录与设备节点方案

2.1 标准目录一次建齐

BusyBox构建完成后,你通常会得到一个包含bin、sbin、usr等目录的rootfs雏形。但它的目录不一定齐全,尤其是dev、proc、sys、tmp这些运行时目录,往往需要手动补上。我在Ubuntu20.04的开发机上是通过下面的命令一次建齐整套目录的:

sudo mkdir -p rootfs/{bin,sbin,usr/bin,usr/sbin,lib,etc/init.d,dev,proc,sys,tmp,var,root,home,mnt,opt,run}

这里每个目录都是有讲究的,不是随便建的。dev是设备节点所在目录,内核和用户空间通过它访问硬件;proc和sys是两个虚拟文件系统,分别提供进程和内核设备模型的信息,很多命令如ps、mount、mdev都强依赖它们;tmp是临时文件目录,通常挂tmpfs用内存承载;var放日志和锁文件;root是root用户的home目录;mnt是给调试挂载NFS之类用的临时挂载点。

搞完目录之后,我还会顺手把usr目录下的内容做个整理。BusyBox的rootfs里usr/bin和usr/sbin一般直接复制工具即可,如果是为了省空间,也可以把usr/bin和usr/sbin做成指向bin和sbin的符号链接。不过这部分不影响启动,可以放到后面再优化。

2.2 设备节点:静态mknod还是动态mdev

设备节点这块是新手最容易懵的地方。有些教程让你用mknod手工创建一堆设备文件,有些教程则说只要配置mdev就能自动管理。我的建议是两者结合,分阶段处理。

起步阶段用静态mknod创建最关键的几个节点就够了:

sudo mknod -m 660 rootfs/dev/console c 5 1 sudo mknod -m 666 rootfs/dev/null c 1 3 sudo mknod -m 666 rootfs/dev/zero c 1 5

这其中有console和null是必须的。console是内核启动早期和串口交互的基础,内核在init进程跑起来之前就要往console上打印信息;null是很多命令的垃圾桶,没有它某些命令会直接报错。zero、random、urandom这些也可以顺手建上,成本很低。

等系统能启动到shell之后,再引入mdev做动态设备管理。mdev是BusyBox自带的轻量级设备管理工具,它通过扫描sysfs来创建设备节点,比手工mknod省心太多。配置方式是在/etc/init.d/rcS里加上:

echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s

其中第一行告诉内核,当热插拔事件发生时调用/sbin/mdev来处理,第二行让mdev立即扫描一次/sys目录,把当前存在的所有设备节点都补建出来。需要说明的是,mdev依赖内核的sysfs和uevent机制,编译内核时要确认CONFIG_SYSFS已经打开。

静态和动态方案的取舍也很直接:静态mknod优点是简单直接、不依赖内核配置,缺点是设备一多就没法维护,外设插拔也不会自动生成节点;动态mdev优点是省心、全自动,缺点是依赖sysfs、配置错了排查起来更麻烦。实际产品里绝大多数用mdev或者devtmpfs,只有极简教学环境才还在用全套静态节点。

3. init初始化流程与启动脚本

3.1 内核交接给用户空间的流程

内核启动到最后,会去文件系统里找init进程来执行。这个交接过程是有固定搜索顺序的:内核先尝试/sbin/init,找不到再试/bin/init,然后是/etc/init,最后是/bin/sh。如果这些都没有,内核就直接panic,打印出Kernel panic - not syncing: No working init found。

这句报错我实在见过太多次,几乎每个自己动手做rootfs的人都绕不过去。原因无非两种:一是init路径不对,二是文件系统里确实没有可执行的init。BusyBox编译安装后,/sbin/init通常是busybox的符号链接,所以只要BusyBox本体没问题、目录结构正确,这一步一般能过。如果实在找不到init,也可以在内核启动参数里手动指定init=/linuxrc或者init=/bin/sh来排查问题。这个参数在调试阶段非常好用,能让你绕过init直接进shell看环境。

内核把控制权交给init之后,BusyBox init会读取/etc/inittab这个配置文件,按里面的定义来启动各个进程。换句话说,inittab就是用户空间的“开机启动项”,它的写法直接决定你的串口有没有shell、rcS跑不跑。

3.2 /etc/inittab动作与写法

BusyBox的inittab格式和桌面Linux的systemd配置完全不同,它沿用了经典的SysV init风格,每行格式是:

id:runlevel:action:process

在BusyBox里,id用来标识控制台设备,一般留空;runlevel可以忽略;action是触发条件;process是要执行的命令。重点就在action上,常用动作整理成了一张表:

action触发时机典型用途
sysinitinit启动后最先执行执行/etc/init.d/rcS
respawn进程结束后立即重新启动始终保持某个后台进程存活
askfirst进程启动前在控制台询问“Press enter to continue”给串口终端分配交互shell
wait启动进程并等待其结束执行一次性初始化任务
once只执行一次,不等待执行一次性服务
ctrlaltdel收到Ctrl+Alt+Del组合键时触发重启
shutdown系统关机时卸载文件系统

我实际用来跑通系统的inittab非常简单,就四行:

::sysinit:/etc/init.d/rcS console::askfirst:-/bin/sh ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r

第一行是初始化入口,第二行是串口交互shell,第三行为快捷键重启兜底,第四行关机前卸载文件系统。这里的conssole对应设备节点/dev/console,也可以改成ttyS0或者ttymxc0这类具体串口名。很多人以为inittab内容越多越好,其实不是,刚开始越精简越好排查。

有个细节值得注意:第二行askfirst前的减号“-/bin/sh”表示让shell成为登录shell,会去读取/etc/profile里的环境变量。如果你不写这个减号,登录后PATH可能都是空的,命令都敲不了。

3.3 rcS脚本内容逐行讲解

rcS是整个根文件系统的初始化入口,位于/etc/init.d/rcS。它没有固定的标准写法,但有几个步骤几乎是所有嵌入式Linux的标配。我给出一个验证过能用的版本,并解释每行的作用:

#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t tmpfs tmpfs /tmp mkdir -p /dev/pts mount -t devpts devpts /dev/pts echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s hostname -F /etc/hostname ifconfig lo up ifconfig eth0 up udhcpc -i eth0

第一步挂proc和sysfs,这是内核与用户空间交互的基础。mount -a依赖/etc/fstab,但我在rcS里直接写了mount命令而不是用mount -a,跳过fstab可以减少一层配置。第二步挂tmpfs到/tmp,让临时文件落在内存里,避免频繁读写Flash。第三步挂devpts,这是pty伪终端需要的文件系统,虽然很多场景用不到,但挂上没坏处。第四步是mdev,前面已经解释过。第五步设置主机名。第六步配置回环设备和以太网卡,并用udhcpc自动获取IP。

这个脚本里每一条都有可能是坑。比如mount -t proc这一步如果失败,ps命令会看到“can't open /proc”之类的报错;mdev不执行,/dev下除了手动创建的几个节点外空空如也;ifconfig那两行如果网卡名字不叫eth0,会报错但不影响启动,这种情况用ip addr看下实际网卡名改一下就行。

写完rcS后记得加执行权限,命令是chmod +x /etc/init.d/rcS。另外文件换行符必须是LF,如果在Windows上编辑过变成CRLF,启动时会报“not found”但文件明明存在,这个坑我踩过不止一次。

4. 用户侧配置:账号、网络与动态库

4.1 从“能启动”到“能登录”

系统能起来并进入shell之后,接下来要处理的是用户、密码和环境变量。BusyBox验证登录时读取的是/etc/passwd、/etc/shadow和/etc/group这三个文件。我第一次做的时候以为没有密码文件也能用,结果login命令一直报错。

一个最小可用的passwd文件可以是这样:

root:x:0:0:root:/root:/bin/sh

shadow文件不设密码的情况下可以写成:

root::0:0:99999:7:::

group文件写一行root组:

root:x:0:

这三个文件我通常直接echo重定向进去,不用额外工具。注意passwd字段里的x表示密码存储在shadow文件里,这是标准的shadow机制。如果需要设置root密码,可以在目标板启动后用passwd命令修改,或者Ubuntu20.04上借助交叉环境的工具直接生成加密串。

环境变量方面,/etc/profile是每次登录shell时读取的配置。我一般会设置PATH和PS1:

export PATH=/bin:/sbin:/usr/bin:/usr/sbin export PS1='[\u@\h]:\w$ '

PATH设置很关键。如果用默认值,有些命令可能不在当前路径里,登录后敲命令会提示not found,而实际上命令就在/bin下。PS1是命令提示符格式,\u表示当前用户,\h表示主机名,\w是当前路径,调整成自己习惯的格式就行。

4.2 网络配置

网络这块在嵌入式rootfs里有几条路可以走。最简单粗暴的是在rcS里用ifconfig手动配置固定IP:

ifconfig lo 127.0.0.1 up ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up route add default gw 192.168.1.1

如果要动态获取IP,就用BusyBox自带的udhcpc,也就是我前面的rcS里那种写法:

udhcpc -i eth0

udhcpc运行后会自动发起DHCP请求,拿到IP后通过脚本刷新网络配置。它有对应的脚本通常在/etc/udhcpc/default.script里,如果这个脚本缺失,udhcpc可能能拿到地址但无法正确配置网卡。

DNS配置在/etc/resolv.conf文件里,格式很简单:

nameserver 8.8.8.8 nameserver 114.114.114.114

没有这个文件,ping域名会报“bad address”,但ping IP是通的。主机名相关的还有/etc/hostname和/etc/hosts两个文件,hostname里放一个纯名字比如“arm-dev”,hosts里可以放本机的hostname映射,方便网络邻居识别。我在rcS里用hostname -F /etc/hostname来加载主机名,这样比硬编码在ifconfig环境里更规范。

4.3 动态库依赖处理

动态库缺失是BusyBox根文件系统“完善”阶段的头号杀手。我遇到过的情况是:BusyBox编译时依赖的glibc版本和目标板上的库版本不一致,或者干脆没拷贝库文件,结果上电后整个系统根本起不来。

处理依赖的第一步是确认BusyBox的二进制格式和依赖项。在Ubuntu20.04的终端里用file命令看格式,再用交叉编译工具链自带的readelf查看依赖:

file rootfs/bin/busybox readelf -d rootfs/bin/busybox | grep NEEDED readelf -l rootfs/bin/busybox | grep interp

readelf -d命令会列出这个可执行文件依赖了哪些共享库,通常是libc.so.6、libm.so.6这几项。readelf -l里的interp一行标明动态链接器的路径,一般是/lib/ld-linux.so.3或者/lib/ld-linux-armhf.so.3。如果这个路径不对,运行时会报“No such file or directory”,而这句报错其实指的是动态链接器不存在,不是库文件本身的问题。

接下来从交叉编译工具链的sysroot目录把依赖的库拷贝到rootfs/lib下:

cp -a /opt/toolchain/sysroot/lib/libc.so.6 rootfs/lib/ cp -a /opt/toolchain/sysroot/lib/libm.so.6 rootfs/lib/ cp -a /opt/toolchain/sysroot/lib/ld-linux*.so.* rootfs/lib/

这里我特别强调cp -a保留符号链接属性。glibc的库文件往往带有版本号后缀,同时存在libc.so.6这样的软链接指向真实文件。如果你用普通的cp拷贝,软链接会变成普通文件,运行时同样加载失败。拷贝完成后可以顺手用工具链的strip命令瘦身,把调试符号去掉,体积能缩小一半以上。但要注意,strip操作针对的是目标文件,不是开发机上的x86文件,别搞混了。

还有个大坑是glibc版本与内核版本的兼容性。新工具链编出来的BusyBox如果用了较高版本的glibc,拷到老内核的设备上运行时会报“FATAL: kernel too old”。这个问题我在Ubuntu20.04上做某块老平台时碰到过一次,解决办法要么换低版本工具链重新编译,要么直接在BusyBox配置里打开静态编译选项CONFIG_STATIC=y,静态链接之后就不依赖目标板上的动态库了,省去库管理的麻烦。当然静态编译会增加可执行文件体积,对于Flash资源极其紧张的场景要权衡一下。

5. 常见问题排查实录

5.1 启动失败的三个典型场景

做这个第二十一期过程中,我把自己和身边人踩过的问题汇总了一下,频率最高的三个场景很有代表性。

第一个是Kernel panic - not syncing: No init found。排查顺序非常固定:先确认rootfs里是否有/sbin/init或/bin/init;再确认init文件是否有执行权限;然后确认kernel启动参数里的root=路径是否正确指向了rootfs分区;最后如果Rootfs是烧写在SD卡或Flash上,还要确认驱动是否把存储设备识别出来了。这个报错还有一个变种,就是文件系统挂载成功但init脚本权限不对,内核会报“can't run '/etc/init.d/rcS': Permission denied”,这时候chmod +x就能解决问题。

第二个是串口完全没有输出或者直接卡死在Starting kernel。这个其实不完全是rootfs的锅,更多是启动参数和console设备没配对。但只要涉及串口shell没出来,我会按这样的顺序查:内核启动参数有没有配置console=ttyS0,115200;/dev/console节点是否存在;/etc/inittab里askfirst对应的设备名是不是当前串口;最后才是波特率设置是否正确。千万别一上来就去改inittab。

第三个是文件系统变成只读。BusyBox的rootfs如果用ramdisk方式启动,默认是只读的,需要重新挂载为读写:

mount -o remount,rw /

这块我特别想强调:很多初学者在只读文件系统下改了/etc/inittab或rcS,发现重启后修改没了,还以为是自己没保存好,实际上就是没做remount。

5.2 动态库与文件权限相关坑

动态库类的报错有个非常有迷惑性的现象:运行busybox时提示/bin/sh: not found,而你明明在/bin下看到了sh这个软链接。这个“not found”的真实原因是动态链接器路径不存在,不是sh本身缺失。用readelf -l查看interpreter路径,然后把对应的ld-linux库拷到指定位置,问题就会消失。这个坑如果不理解原理,翻遍bin目录都找不到头绪。

还有一个坑是库文件拷贝对了但却是x86的。我第一次做时直接从开发机的/lib目录拷贝.so文件,结果目标板启动后libc相关程序全部段错误。后来才意识到,开发机Ubuntu20.04的glibc是x86架构,ARM板根本执行不了。所以拷贝库文件必须用交叉编译工具链sysroot下的对应架构库,不能图省事从系统目录直接cp。

再就是上电后root登录不了。排查顺序是:/etc/passwd和/etc/shadow是否存在;root用户UID是不是0;shadow里密码字段格式是否正确;如果之前设置了登录shell验证,还要确认/bin/sh是否存在且可执行。这套排查逻辑基本能把登录问题覆盖完。

5.3 Ubuntu20.04开发环境下的经验总结

Ubuntu20.04相比老版本Ubuntu,在交叉编译工具链方面最大的变化是系统自带的gcc版本较新,新版的glibc编译产物在旧内核上跑容易触发kernel too old。这个现象我做实验时反复出现过,最终给出的建议如下表:

现象直接原因解决办法
No init foundrootfs里没有/sbin/init或bin/init检查BusyBox安装结果和权限
No working init foundinit文件存在但无法执行检查动态链接器路径和libc依赖
串口无shellconsole设备未创建或inittab设备名不匹配先用mknod建console节点再调inittab
mount: proc already mounted/proc已被内核自动挂载或重复挂载注释rcS里的重复挂载行
FATAL: kernel too oldglibc版本高于内核兼容范围换旧工具链或静态编译BusyBox
No such file or directory动态链接器ld-linux不存在readelf查看interp路径并补齐
can't run rcS: Permission denied脚本没有执行权限或换行符错误chmod +x并确认LF换行

调试手段上,我强烈推荐在Ubuntu20.04开发机上配置NFS根文件系统。把rootfs目录通过NFS共享给开发板,内核用root=/dev/nfs nfsroot=192.168.1.100:/path这样的参数启动,这样所有文件和脚本的修改都能即时生效,不用反复烧写SD卡,调试效率能提升一个量级。我后期几乎所有的配置调整都是在NFS启动模式下完成的,确认稳定后再打包烧进板载存储。

我在这个系列里踩得最深、耽误时间最长的坑,几乎都集中在“文件存在但执行不了”这一类问题上。动态链接器路径不对、库里混了x86版本、脚本换行符是CRLF、权限位没设置——这些问题从现象上看全是not found、Permission denied这类模糊提示,很容易让人以为操作错了方向。实际上它们的排查手段都很固定,就是把readelf、file、ls -l这几个工具用熟,先确认文件的架构和依赖,再确认权限和格式,基本能解决八成的问题。我建议你手头常备一张最小rootfs的备份,每次做大改动之前先复制一份,保证随时能回退到能启动的状态。接下来再做系统移植,思路就很清晰了:先把启动流程跑通,再逐个功能验证,最后才是优化体积、精简启动时间这些进阶活儿。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 9:09:34

《幸福的勇气》解读:自立不是独立,而是主动去爱的能力

1. 先聊聊:为什么《幸福的勇气》要把“自立”摆上台面1.1 这是一本回答“自由之后怎么办”的书读完《被讨厌的勇气》,很多人会记住一个结论:自由就是不再寻求认可,就是有勇气被别人讨厌。当时我觉得自己悟了——课题分离嘛&#x…

作者头像 李华
网站建设 2026/10/20 9:09:10

NPU开发板实战:从零跑通RKNN模型转换与部署

前阵子接手一块带NPU的开发板,第一反应不是兴奋,是有点发怵。CPU、GPU、FPGA都折腾过,NPU这东西听起来总是隔着一层,网上资料也杂,动不动就甩一堆算子、量化、交叉编译的术语。真正静下心把官方手册里“你好&#xff0…

作者头像 李华
网站建设 2026/10/20 9:08:57

PHP特性安全与CTF绕过实战:弱类型、哈希碰撞与变量覆盖全解析

开头PHP特性刷题,是CTF Web方向绕不过去的一段旅程。CTFshow这套PHP特性专题到89-115题这个区间,正好是从“认识特性”到“组合利用”的分水岭——前面的题大多是一个点一个洞,一眼能看出来;到了这一段,你开始接触变量…

作者头像 李华
网站建设 2026/10/20 9:08:45

稳压补水机组控制逻辑设计与现场调试实战指南

干楼宇自控和给排水安装这些年,稳压补水机组是我每次去现场都得碰面的老熟人。不管你是做中央空调机房的冷冻水系统,还是做供热站的一次网补水,甚至工业循环水系统,只要管道里的水会漏、会蒸发、会被排污,就得有套东西…

作者头像 李华
网站建设 2026/10/20 9:08:29

AI Agent入门:先手写循环再学框架,避开新手三大坑

1. 为什么“先啃框架”是新手最容易踩的坑我见过太多想入门 AI Agent 的朋友,第一周就抱着某个开源框架的文档硬啃,第二周开始配环境,第三周卡在某个依赖版本冲突上,第四周就放弃了。这个路径几乎成了转行者的标准死法。问题不在于…

作者头像 李华
网站建设 2026/10/20 9:08:13

程序员AI编程选型指南:从上下文窗口到成本计算的实用方法

最近后台好多刚入行的朋友来问我同一个问题:“Gemini 3.1 Pro 是不是现在最适合程序员的模型?我到底选它还是选别的?”我特别能理解这种纠结。刚入行的时候,时间最不值钱,但也最容易被各种“最强模型”“编程神器”的标…

作者头像 李华