BusyBox 这东西,做嵌入式的没几个绕得开。我当年刚接触嵌入式 Linux 的时候,第一反应也是:这玩意儿不就是一堆命令的压缩包吗?直到自己动手做根文件系统,被各种动态链接、符号链接、init 进程问题折磨了几轮之后,才真正理解它为什么被叫做嵌入式 Linux 的瑞士军刀。
这篇文章不聊虚的,直接从一个最小可用根文件系统的构建过程入手,把 BusyBox 的原理、编译配置、部署方式、NFS 挂载调试、以及如何集成 dropbear 实现远程登录这些事,从头到尾捋一遍。整个过程我都会用实际执行的命令和踩过的坑来讲,保证你能照着操作一遍就通。
如果你是刚入坑嵌入式 Linux 的新手,这篇文章可以帮你把 BusyBox 和根文件系统的关系彻底理清。如果你已经做过一些项目,也可以看看我在编译选项、设备节点、开机脚本这些细节上的处理思路,说不定能帮你少走点弯路。
1. 先搞明白 BusyBox 到底是什么
1.1 一个命令,拆成上百个命令
BusyBox 的核心思路其实特别朴素:把上百个常用的 Unix/Linux 命令,比如 ls、cp、mv、cat、sh、mount、ifconfig 这些,全部塞进一个可执行文件里。它通过解析argv[0]来决定自己模拟哪个命令——也就是你调用它的时候,程序名是什么,它就表现出那个命令的行为。
这就像一把多功能刀,刀柄里藏着螺丝刀、开瓶器、剪刀,但外壳只有一个。传统做法是一把刀一个工具,占地方;BusyBox 是把所有工具塞进一把刀里,省空间,特别适合 Flash 容量按 MB 甚至 KB 计算的嵌入式设备。
具体实现上,BusyBox 源码里每个命令对应一个applet,用一个表结构applet_table来注册命令名和对应的处理函数。当你执行busybox ls或者把busybox符号链接成ls再执行时,它会在applet_table里查找命令名,找到就调用对应的函数。从原理上剖析到根文件系统实战,这篇的主要内容就是把这条链路打通。
1.2 为什么会省这么多空间
你可能会问:就算把所有命令合并成一个文件,该有的功能不还是得有吗?代码量能少到哪去?
关键在于精简实现。BusyBox 里的命令基本都是针对嵌入式场景做了裁剪的,去掉了大量桌面版才用得上的功能。比如说 BusyBox 的ls,它不会去实现ls --color=auto在终端里的各种配色策略,不会实现过于复杂的--format=commas输出,接口参数只保留常用项。代码体积直接就下来了。
另一个大头是静态编译(static build)。动态链接的 ls 需要依赖 glibc 或 musl 的动态库文件,那是一个几百 KB 到几 MB 的庞然大物。BusyBox 推荐用静态链接,把需要的 C 库代码直接编进一个文件里,虽然单文件体积会比动态链接大一点,但整体算下来还是省一大截。
我实际测过一个最小系统:内核 3.5 MB,BusyBox 静态编译后约 900 KB,加上各种配置文件和设备节点,整个根文件系统镜像压缩后 1.8 MB 左右。这种体积,放在 4MB Flash 的板子上绰绰有余。如果是完整桌面发行版,光/bin目录里的命令加起来就要几十 MB,这差距是数量级的。
1.3 不只是命令集合,它还是 init
很多人忽略了一点:BusyBox 还能当 PID 1 使用,也就是系统第一个启动的用户态进程。传统桌面 Linux 下 PID 1 是 systemd 或者 sysvinit,负责挂载文件系统、启动服务、拉起登录终端。BusyBox 里的initapplet 实现了类似 sysvinit 的基本功能。
它读取/etc/inittab配置文件,里面定义了系统启动时要执行哪些脚本、要在哪些终端上启动sh、系统重启时执行什么动作。对嵌入式设备来说,这套 init 虽然没 systemd 那么强大,但完全够用,而且资源占用极低。
所以一个基于 BusyBox 的最小根文件系统,通常组成是这样的:
/bin/busybox—— 唯一的可执行文件/sbin/init—— 符号链接,指向 busybox/etc/inittab—— init 的配置文件/dev/console等设备节点 —— 系统启动必需的设备文件/proc、/sys、/tmp等挂载点的空目录- 一些必要的配置文件和脚本,比如
/etc/fstab、/etc/init.d/rcS
这样一套东西,麻雀虽小,五脏俱全。这也是接下来实战部分我们要一步步搞出来的东西。
2. 动手之前:环境准备与编译配置
2.1 交叉编译工具链怎么选
要用 BusyBox 做出能在 ARM 板子上跑的根文件系统,光在 PC 上编译是不够的,必须用交叉编译工具链。我用的是arm-linux-gnueabihf-系列,这套工具链在 Ubuntu 上直接就能装:
sudo apt-get install gcc-arm-linux-gnueabihf装完之后确认一下工具链可用:
arm-linux-gnueabihf-gcc --version这里有个细节:如果你用的是比较老的板子,比如 Cortex-A7 或者更早的 ARM9,可能需要装arm-linux-gnueabi-(软浮点版本),具体要看内核配置和板子是否支持硬件浮点。对大多数现代 ARMv7 板子来说,arm-linux-gnueabihf就行。
如果你用的是 aarch64(ARM64)的板子,那就装gcc-aarch64-linux-gnu,后面编译参数把前缀换成aarch64-linux-gnu-即可。
2.2 下载 BusyBox 源码
BusyBox 的源码托管在官方站点,我习惯用 git 直接拉取,方便切换版本:
git clone git://busybox.net/busybox.git cd busybox如果想用某个稳定版本,可以打上 tag。我用的是 1.36.x 系列,不过下面这套操作在 1.20 到 1.36 之间都适用,差异不大。
打开源码目录看一眼结构,你会发现applets/目录下是所有命令的实现,libbb/是公共库代码,coreutils/、networking/、shell/这些目录则按功能分类存放各种命令源码。理解这个目录结构对后续二次开发很有帮助,因为你要加自定义命令时,就是在对应目录里添加 applet 源文件,然后在applets/applet_table.c里注册。
2.3 配置编译选项的三种方式
BusyBox 的配置方式跟内核一样,基于 Kconfig 体系。进入源码根目录,执行:
make menuconfig如果你是第一次接触,可能有点懵,但别怕,关键就几个点:
Settings -> Build Options -> Build BusyBox as a static binary (no shared libs):这个必须勾上。我之前说过,静态编译是省空间的关键,而且更重要的是一旦编译成静态链接,你的根文件系统里就不需要再放 libc 的动态库,部署时省一堆事。唯一要注意的是,静态编译的 BusyBox 在做 DNS 解析时,如果 glibc 版本与内核不匹配(比如内核太老),可能会出问题。真遇到这种边缘情况,可以改用 musl 工具链编译。
Settings -> Cross Compiler prefix:填
arm-linux-gnueabihf-,注意末尾的短横线不要漏,这是让编译系统知道用哪个交叉编译器。Settings -> Destination path for 'make install':这个可以先不管,等会儿用命令行参数指定。
其他命令的裁剪,我建议初学者先用默认配置,默认就带了绝大部分常用命令。等系统跑起来之后,再根据实际需求精简。别一开始就追求极致体积,不然调试时缺个ls、缺个cat,你就难受了。
配置完保存退出,开始编译:
make -j$(nproc)编译完成后,当前目录下会生成一个busybox可执行文件。用file命令看一眼:
file busybox如果输出里有ARM字样,说明交叉编译生效了。
2.4 安装到临时目录
执行安装,指定安装路径为我们准备的一个空目录:
mkdir -p ~/rootfs make install CONFIG_PREFIX=~/rootfsCONFIG_PREFIX是 BusyBox 的安装目标路径,它会自动在~/rootfs下生成bin/、sbin/、usr/等目录,并在里面创建一堆符号链接,指向/bin/busybox。
装完之后看一下目录结构:
find ~/rootfs -type l | head -n 20你会发现~/rootfs/bin/ls是一个符号链接,指向busybox。这其实就是 BusyBox 运行机制的核心——命令名决定行为。
3. 构建最小根文件系统的完整流程
3.1 创建基础目录结构
make install只会生成 BusyBox 相关的文件和链接,但要组成一个可启动的根文件系统,还需要手动创建一系列目录:
cd ~/rootfs mkdir -p dev proc sys tmp etc/init.d lib mnt root var/log这些目录各自有各自的用途:
/dev—— 设备文件目录,系统启动时必须有/dev/console和/dev/null,不然内核启动会直接报错/proc、/sys—— 内核虚拟文件系统的挂载点,很多命令和工具依赖它们/etc—— 配置文件目录,inittab、fstab、init.d/rcS 都在这里/tmp—— 临时文件目录,很多程序会往这里写文件/root—— root 用户的家目录
3.2 设备节点的创建
这是新手最容易忽略的一步。/dev/console和/dev/null必须提前创建好,否则内核在挂载根文件系统后启动 init 进程时会因为找不到控制台设备而失败。
有两种方式创建设备节点。
第一种,在宿主机上直接用mknod命令创建:
sudo mknod -m 622 ~/rootfs/dev/console c 5 1 sudo mknod -m 666 ~/rootfs/dev/null c 1 3c表示字符设备,5 1是 console 的主次设备号,1 3是 null 的主次设备号。
第二种方式,不用手动创建设备节点,而是让系统启动后用 devtmpfs 自动生成。这需要在内核配置里打开CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT。但就算开了 devtmpfs,/dev/console最好还是手动创建,因为内核在挂载根文件系统到执行 init 之间有一小段窗口期,那时 devtmpfs 还没挂上。
我在实际项目里是两种方式都做了:预创建 console 和 null,剩下的设备节点全部通过 devtmpfs 在开机时自动生成。这样既保证了最基本的输出,又不用手工维护一大串设备节点,省心很多。
3.3 编写 inittab 与 rcS 启动脚本
接下来是让系统启动起来的关键配置文件。
/etc/inittab是 BusyBox init 读取的配置。一个最简可用的版本:
::sysinit:/etc/init.d/rcS ::respawn:/sbin/getty 115200 ttyS0 ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r逐行解释:
::sysinit:/etc/init.d/rcS—— 系统初始化时执行的脚本,后面细说::respawn:/sbin/getty 115200 ttyS0—— 在串口 ttyS0 上以 115200 波特率启动登录终端,如果进程退出,init 会自动拉起它(respawn 的含义)::ctrlaltdel:/sbin/reboot—— 按下 Ctrl+Alt+Del 时重启,嵌入式设备上可能用不上,但写上没坏处::shutdown:/bin/umount -a -r—— 关机前卸载所有文件系统
如果你用的是 USB 转串口且设备名是 ttyUSB0,就把 ttyS0 改成 ttyUSB0;如果是桌面板子用 HDMI 接显示器,可能会用 tty0。这个看你的具体硬件。
/etc/init.d/rcS是系统初始化脚本。用来挂载虚拟文件系统、配置网络、启动必要服务。我的习惯是最小化到只挂载必要的文件系统:
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs none /tmp hostname my-board ifconfig lo 127.0.0.1 up注意系统启动早期,你可能还没有网络,ifconfig lo是配置回环接口。如果板子有以太网口,可以在 rcS 里加:
ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up或者用 udhcpc 动态获取 IP。关于网络这一块,后面 NFS 挂载时会详细讲,那时你就能体会到网络配置的重要性了。
记得给 rcS 加执行权限:
chmod +x ~/rootfs/etc/init.d/rcS3.4 使用 fstab 管理挂载点
虽然 rcS 里可以手动挂载,但更规范的做法是写/etc/fstab,然后在 rcS 里执行mount -a。fstab 内容示例:
proc /proc proc defaults 0 0 sysfs /sys sysfs defaults 0 0 tmpfs /tmp tmpfs defaults 0 0这样 rcS 只要写成:
#!/bin/sh mount -a ifconfig lo 127.0.0.1 upmount -a会按 fstab 里的配置把所有条目都挂载上,代码更简洁,也方便后期加新挂载点。我用这套方式已经好几个项目了,改配置只需要动 fstab,不用改脚本,维护起来舒服得多。
3.5 测试根文件系统完整性
在把系统烧到板子上之前,强烈建议先在宿主机上检查一下整个目录结构是否完整。我常用的检查命令:
cd ~/rootfs find . -maxdepth 2 | sort重点关注这几个东西在不在:
/bin/busybox—— BusyBox 主程序/sbin/init或/linuxrc—— init 入口的符号链接/dev/console、/dev/null—— 设备节点/etc/inittab、/etc/init.d/rcS—— 启动配置
这几个文件缺一不可。init 入口是一个比较特殊的情况:BusyBox 安装时会在/linuxrc建一个指向 busybox 的链接,但如果你想让系统用 BusyBox 的 init 功能,得确保/sbin/init也指向 busybox,或者内核启动参数init=指定了可用的 init 路径。可以在 rootfs 里检查一下:
ls -l ~/rootfs/sbin/init ~/rootfs/linuxrc如果sbin/init不存在,手动补一个:
ln -s /bin/busybox ~/rootfs/sbin/init4. NFS 挂载根文件系统的实战操作
4.1 为什么开发阶段强烈推荐 NFS 挂载
每次改动点代码、改个脚本,都要重新打包根文件系统镜像、烧录到 SD 卡或 Flash 里,这在开发调试阶段简直是一场灾难——一次编译打包烧录可能要几分钟,一天改几十次,光是等待的时间就够喝一壶的了。
NFS(Network File System)挂载解决了这个问题:根文件系统放在开发主机上,目标板通过以太网从主机上挂载它作为自己的根文件系统。这样你在主机上改完文件,板上立即生效,重启板子也只是重新挂载一下,整个迭代速度提升一个数量级。
这个模式的本质是:内核启动时通过root=/dev/nfs参数,让内核在网络协议栈初始化后,发起 NFS 挂载请求,把远程目录挂到本地作为根文件系统。所以内核必须支持 NFS 客户端、IP 协议栈、以及网络设备驱动,这些都是内核配置阶段要考虑的。
4.2 宿主机端 NFS 服务配置
在开发主机(Ubuntu)上先装 NFS 服务:
sudo apt-get install nfs-kernel-server编辑/etc/exports,把 rootfs 目录共享出去:
sudo vim /etc/exports添加一行:
/home/user/rootfs *(rw,sync,no_root_squash,no_subtree_check)逐项解释:
rw—— 允许读写sync—— 写入时先同步到磁盘,避免异常断电数据丢失no_root_squash——这个最关键,它允许远程 root 用户拥有本机 root 权限。如果不加,板子上的 root 会因为权限不够而无法修改根文件系统的文件。no_subtree_check—— 禁用子树检查,提升性能
改完重启 NFS 服务:
sudo exportfs -ra sudo systemctl restart nfs-kernel-server在主机上验证一下共享是否正常:
showmount -e localhost如果看到你共享的目录,说明服务端配置完成。
4.3 内核配置中需要开启的 NFS 相关选项
要让板子能从 NFS 启动,内核必须支持以下功能。这要在内核源码目录下操作:
make menuconfig需要开启的关键项:
CONFIG_NET=y—— 网络支持CONFIG_INET=y—— TCP/IP 协议栈CONFIG_NETDEVICES=y以及你的网卡驱动(比如CONFIG_DM9000、CONFIG_SMC91X,看板子的网卡芯片型号)CONFIG_NFS_FS=y—— NFS 客户端CONFIG_ROOT_NFS=y—— 支持把 NFS 作为根文件系统挂载CONFIG_IP_PNP=y—— 这个要特别注意,它允许内核启动时通过 BOOTP/DHCP 或 RARP 获取 IP 地址。如果不开启,内核无法在启动阶段配置网络,NFS 挂载就无从谈起
其中CONFIG_IP_PNP下面还有CONFIG_IP_PNP_DHCP和CONFIG_IP_PNP_BOOTP,按你的需求勾选。如果板子和开发主机在同一个局域网,且你不想手动配置 IP,用 DHCP 最省事。
4.4 从 U-Boot 传递内核启动参数
NFS 挂载根文件系统的最后一步,是在 U-Boot 环境变量里设置正确的启动参数。我常用的启动参数模板:
setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/home/user/rootfs,v3,tcp rw ip=dhcp init=/sbin/init'参数拆解:
console=ttyS0,115200—— 内核控制台输出到串口 0,波特率 115200root=/dev/nfs—— 告诉内核根文件系统挂载 NFSnfsroot=192.168.1.100:/home/user/rootfs,v3,tcp—— NFS 服务器 IP、共享目录路径、NFS 版本(v3)、传输协议(tcp)ip=dhcp—— 内核启动时通过 DHCP 获取 IP。如果你的环境没有 DHCP 服务器,可以手动指定 IP、网关、服务器 IP:ip=192.168.1.50:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off格式是ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>init=/sbin/init—— 指定 init 程序路径,也可以用 BusyBox 默认行为
设置好后,保存环境变量,重启板子:
saveenv reset如果一切顺利,你会看到内核启动日志里出现 NFS 挂载相关的打印,然后 BusyBox init 跑起来,弹出登录提示符。能走到这一步,恭喜你,一个最简嵌入式 Linux 系统已经活了。
4.5 NFS 挂载过程中踩过的坑
NFS 挂载这个事,对初学者来说坑特别多,我把自己踩过的几个典型问题列一下:
第一个问题是板子无法获取 IP。表现是启动日志卡在Waiting for network configuration或者 DHCP 超时。排查思路:先确认网线插入、网卡驱动是否加载成功(内核日志里搜eth0),再看局域网里是否有 DHCP 服务器。如果你用的是家用路由器,一般都有 DHCP;如果是开发环境直连电脑,那就要在电脑上开一个 DHCP 服务,或者用静态 IP 参数手动指定。
第二个问题是NFS 挂载超时,日志显示VFS: Unable to mount root fs via NFS。原因通常有几种:NFS 版本不匹配(内核侧的 nfsroot 参数里指定 v2/v3/v4 要和服务端匹配)、iptables 防火墙拦截了 NFS 端口、no_root_squash没加导致权限被拒。我一般先在宿主机上关闭防火墙,或者放行 NFS 相关端口(2049、111、以及 rpcbind 的动态端口),然后再试。
第三个问题是挂载成功了,但系统启动后执行命令提示 Permission denied。八成是/etc/exports里少写了no_root_squash,NFS 把板子上的 root 用户映射成了 nobody,权限自然不够。加上no_root_squash重启 NFS 服务就好。
5. 让系统更好用:配置网络、开机脚本与远程登录
5.1 网络配置文件怎么落
如果你在 rcS 里用ifconfig手动配置 IP,地址是硬编码的,换一个网段就得改脚本。更灵活的做法是使用 udhcpc 动态获取 IP,这在 BusyBox 里已经内置了,但需要提供/etc/udhcpc/default.script脚本。
一个最简脚本:
#!/bin/sh case "$1" in deconfig) ifconfig $interface 0.0.0.0 ;; bound) ifconfig $interface $ip netmask $subnet up route add default gw $router dev $interface ;; esac然后 rcS 里启用:
udhcpc -i eth0 &$interface、$ip、$subnet、$router这些都是 udhcpc 调用脚本时通过环境变量传进来的,不用你自己解析数据包。
5.2 集成 dropbear 实现 SSH 远程登录
大多数嵌入式 Linux 系统都没有显示器、键盘,全靠串口调试。串口的缺点是必须有线连着,调试位置受限。这时候如果能通过 SSH 远程登录到板子上,体验会好很多。
SSH 服务端在嵌入式圈子里有两大选择:OpenSSH 和 dropbear。OpenSSH 功能全但是体积大、依赖多;dropbear是专门为嵌入式/物联网设备设计的轻量级 SSH 服务器,静态编译后体积可能只有一两百 KB,特别适合 BusyBox 环境。
编译 dropbear 的步骤:
git clone https://github.com/mkj/dropbear.git cd dropbear ./configure --prefix=/usr --host=arm-linux-gnueabihf --disable-zlib make PROGRAMS="dropbear dropbearkey"编译完成后,把dropbear和dropbearkey拷贝到 rootfs 的/usr/sbin/目录。
然后在 rcS 里加启动逻辑:
if [ ! -f /etc/dropbear/dropbear_rsa_host_key ]; then mkdir -p /etc/dropbear /usr/sbin/dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key fi /usr/sbin/dropbear首次启动会生成 RSA 主机密钥,这是 SSH 服务必需的。之后每次启动直接运行dropbear即可。
注意,要保证 rootfs 里/etc/passwd文件存在,且 root 用户有密码。否则 SSH 登录时即使允许空密码,安全性也堪忧。一个最简单的 passwd 文件内容:
root::0:0:root:/root:/bin/sh这个::表示空密码,配合 dropbear 启动参数-B(允许空密码)能登进去,但这是演示用的,生产环境一定要设置密码。设置方式是在板子上执行passwd root,它会更新/etc/passwd或/etc/shadow中的密码哈希。
5.3 开机自启动服务的管理技巧
有了 inittab 和 rcS,开机启动程序这件事基本就搞定了。但如果你有多达十几个服务要启动,全写在 rcS 里会变得很臃肿。我的做法是按服务拆分脚本,放/etc/init.d/下,然后在 rcS 里统一启动:
#!/bin/sh mount -a for script in /etc/init.d/S*; do [ -x "$script" ] && "$script" start done对应的启动脚本命名规则用S01network、S02dropbear、S99custom这种,前面的数字决定启动顺序。比如:
/etc/init.d/S01network:
#!/bin/sh case "$1" in start) ifconfig lo 127.0.0.1 up udhcpc -i eth0 & ;; stop) killall udhcpc ;; esac这种风格借鉴了桌面 Linux 的 sysvinit 脚本规范,虽然 BusyBox 本身不强制,但维护起来层次清晰,后面加服务只需要新增一个脚本,不用改动其他文件。
5.4 为什么sync、vfs这些基础概念也要了解
在嵌入式开发中,sync这条命令和 VFS(Virtual File System)机制是绕不开的基础。很多新手在 NFS 挂载调试时,改完文件发现板子上没生效,其实就是没搞懂文件系统的缓存机制。
sync命令的作用是把内核缓冲区中尚未写回磁盘的数据强制刷写到底层存储设备。你在宿主机上往 NFS 共享目录里放了一个新文件,如果内核还没把数据同步到服务端,板子那边可能看不到,或者看到的是旧内容。所以我每次在主机上改完 rootfs 里的文件,习惯性地执行一下sync,再重启板子。
VFS 则是 Linux 文件系统的抽象层。它在具体文件系统(ext4、proc、sysfs、NFS)之上提供统一的接口,让用户态程序不用关心底层存储是什么。理解 VFS 有一个实际意义:当你调试一个“文件写入失败”的问题时,要会按 VFS 层→具体文件系统层→块设备驱动层的顺序排查,而不是盲目怀疑代码逻辑。
BusyBox 里的mount、umount、df、lsblk这些命令,本质上都是在和 VFS 打交道。把 VFS 的基本模型(superblock、inode、dentry、file)搞明白,你调试文件系统相关问题时思维会清晰很多。
6. 常见问题排查与性能调优建议
6.1 启动失败的典型现象与对策
系统启动失败是最常见的场景,特征不同,原因也不同。我整理一个排查思路表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 内核启动后黑屏,没有任何用户态输出 | init 程序路径不对、console 参数错误 | 检查 bootargs 里 console= 是否匹配实际串口;确认 /sbin/init 链接存在 |
提示Failed to execute /init | init 文件不存在或没有执行权限 | 用ls -l /sbin/init检查是否是符号链接,chmod +x添加权限 |
提示can't open /dev/console | /dev/console 设备节点缺失或主次设备号不对 | 用 mknod 重新创建,主设备号 5,次设备号 1 |
| init 启动了但 shell 起不来 | /etc/inittab 配置错误,respawn 的终端设备不存在 | 检查 ttyS0 是否被内核注册,用cat /proc/tty/drivers来看 |
| 登录后执行命令报 not found | PATH 环境变量不正确,或者 busybox 位置不对 | 检查 /etc/profile 里 PATH 要包含 /bin:/sbin:/usr/bin:/usr/sbin |
| 文件系统只读,无法写数据 | NFS 不带 rw 参数,或 Flash 分区未正确挂载 | 确认 exports 里是 rw;针对 Flash 用 jffs2/ubifs 时检查挂载参数 |
6.2 体积优化:把根文件系统压到极限
调试阶段不用太在意体积,但产品化阶段就不同了,Flash 容量是按 MB 计费的。几个实用的瘦身方向:
第一个是裁剪 BusyBox 命令。在 menuconfig 里逐个 applet 地确认,把用不到的关掉。比如你的产品不做网络管理,那httpd、tftp、telnetd这些都可以去掉。我做过一个 IoT 项目,把默认的 300 多个 applet 裁剪到 80 个左右,BusyBox 二进制从 900K 降到了 400K。
第二个是去掉调试符号。编译时用make CFLAGS=-Os或make CONFIG_DEBUG=n,代码尺寸能小 10%~20%。另外确保编译器开了-Os(优化尺寸)。在 menuconfig 的 Settings 里有Optimize for size选项,勾上即可。
第三个是精简启动脚本和配置文件。注释和空行删除、无效配置项清理,这些虽然每次只省几百字节,但积少成多。生产环境我还会把 rcS 里需要判断的逻辑从 shell 脚本移到 C 程序里,进一步减少运行时的开销。
6.3 调试技巧:系统日志与内核打印怎么配合用
嵌入式 Linux 调试,最痛苦的是“看不见”。串口输出是唯一的窗口,所以掌握几个日志技巧很有必要。
首先,内核启动时开启更详细的日志。在 bootargs 里加loglevel=8或者ignore_loglevel,能看到内核所有的调试打印。排查驱动问题时这个很有用。生产环境记得改回loglevel=3或者quiet,不然日志刷屏影响性能。
其次,BusyBox 的 init 也提供调试支持。编译时加了CONFIG_FEATURE_INIT_SYSLOG的话,init 的日志会输出到 syslog 和串口,你在系统启动卡住时,看 init 打印到哪一步就能定位问题。比如卡在mount -a,那问题十有八九在 fstab 配置。
第三个是临时加 echo 打印。在 rcS 脚本里、在自定义启动脚本里,关键步骤前后都加一行echo "step 1: ..."到串口,这种方式看起来原始,但在启动过程一片漆黑的时候,效果立竿见影。排查完再把这些打印删掉就好。
6.4 BusyBox 版本升级与兼容性注意
如果你在维护一个老项目,用的是 BusyBox 1.22 甚至更老的版本,想升级到新版本,需要注意几个兼容性问题。
第一,配置文件格式差异。老的 inittab 格式和新的在某些字段上有细微差别,比如::respawn:/sbin/getty在新的默认配置里可能变成console::respawn:-/bin/sh。升级后先别急着覆盖配置文件,把新版源码里的 examples/inittab 拿来参照对比。
第二,命令行为变化。有些命令在旧版本里的行为是那样,新版可能修正了。比如mount在新版里不再默认支持-o nolock(NFS 相关选项),需要额外编译才支持。这类问题建议在升级后做一次全面的命令行为回归测试。
第三,编译环境的差异。新版 BusyBox 可能要求更新版本的 GCC 或内核头文件。老工具链有可能编不过新版源码,实在不行就选择在项目周期内锁定一个稳定的 BusyBox 版本,不必追求追新。我手上有一个项目从 1.29 一直用到 1.33,没出过问题,稳定性比版本号更重要。
7. 扩展思路:从最小系统到完整产品系统
7.1 添加自定义 applet:BusyBox 的二次开发
做到这步,你手上已经有了一个能启动、能登录、能远程操作的嵌入式 Linux 环境。但很多时候,标准 BusyBox 提供的命令并不完全满足项目需求,需要往里加自定义命令。好在 BusyBox 的架构就是为二次开发设计的。
比如你想给板子加一个读取温度传感器的命令temp_show。步骤是这样的:
在源码目录里新建一个文件miscutils/temp_show.c,实现一个函数:
int temp_show_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; int temp_show_main(int argc, char **argv) { // 你的逻辑代码 printf("temperature: 36.5C\n"); return 0; }然后在miscutils/Config.src里添加配置项:
config TEMP_SHOW bool "temp_show" default y help Show temperature sensor value还要在miscutils/Kbuild.src里的lib-y列表中加入temp_show.o,再重新 menuconfig 编译。这样你的 BusyBox 就多了一个自己的命令。这种方式比单独放一个可执行文件在 rootfs 里的好处是:统一在 BusyBox 框架内管理,占用空间最小,启动和调用也更规范。
7.2 使用 mdev 实现设备节点自动管理
当你接入 U 盘、USB 转串口这类热插拔设备时,设备节点需要动态创建。前面提到 devtmpfs 能自动生成内核已知的设备节点,但用户态的处理逻辑(比如挂载 U 盘、通知应用层)还需要一个守护程序。BusyBox 自带的mdev就是干这个的。
在 rcS 里初始化 mdev:
echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s然后在/etc/mdev.conf里配置规则。比如让 U 盘插入后自动挂载:
sd[a-z][0-9]* 0:0 666 @/etc/hotplug/usb.sh当检测到 sd 卡设备节点创建时,mdev 会调用/etc/hotplug/usb.sh脚本,你可以在里面执行 mount 操作。mdev 的功能和桌面系统的 udev 类似,但实现简单得多,非常契合嵌入式场景。
7.3 从 NFS 过渡到 Flash 存储的注意事项
开发调试阶段用 NFS,产品发布时要切回本地存储。这里有一个很多人忽略的坑:NFS 上开发时,很多东西都是网络文件系统,根文件系统的挂载参数跟你最终用 Flash 时的差异很大。
切到 Flash 存储时,注意这几件事:
- 根文件系统类型改成你 Flash 上的实际格式(JFFS2、UBIFS、ext4 等),bootargs 里
root=参数对应改成/dev/mtdblockX或分区名 - 对于 Flash 介质,建议在 fstab 里给根文件系统挂载
noatime选项,减少写操作,延长 Flash 寿命 - 从 NFS 切到 Flash 后,原本很容易忽略的写操作会变成问题。如果某些目录(比如
/var/log、/tmp)写入频繁,记得把它们用 tmpfs 挂载到内存里,避免频繁磨损 Flash - 如果你的最终存储是只读的(比如 squashfs),要在启动时把需要读写的目录用 tmpfs 做 overlay 或者单独挂载
我到现在每次切 Flash 存储,都会先在板子上大规模跑一遍压力测试,重点看/var/log和/tmp的写入量。这两个地方是最容易被忽略、又最容易把 Flash 写挂的地方。
7.4 安全加固:给 BusyBox 系统提个醒
最后聊一下安全。很多嵌入式设备常年裸奔,上面跑着 BusyBox 构建的系统,连个密码都没有,这在互联网上有多危险,不用我多说。
几个基本的安全措施:
- 给 root 用户设置强密码,或者禁用密码登录,改用密钥认证
- dropbear 配置里,监听端口可以改成非标准端口,减少被扫描命中的概率
- 移除或禁掉不用的网络服务。BusyBox 的 telnetd 调试很方便,但生产环境下一定不要开
- 对外网开放的端口越少越好,能用防火墙规则(BusyBox 自带
iptables命令)限制的尽量限制
嵌入式设备的安全问题,本质上是尽量减小攻击面。遵守最小化原则——不需要的服务不开,不需要的端口不监听,不需要的命令不编译进 BusyBox——系统自然就安全多了。
我在实际项目中,一般交付前都会用 nmap 扫一遍板子开放的端口,确认只留下了预期的服务,这一步花不了几分钟,但能避免很多后续的麻烦。