3、BusyBox工具集:麻雀虽小,五脏俱全

做嵌入式Linux,你迟早会碰到一个叫BusyBox的东西。我第一次接触它是在一个存储空间只有4MB的项目上,当时我还在想,这玩意儿能跑完整的Linux吗?后来发现,BusyBox就是答案。

说白了,BusyBox就是一个“瑞士军刀”式的工具集。它把上百个常用的Linux命令(ls、cp、mv、mount、init等等)打包成一个单一的可执行文件。你想想看,一个完整的文件系统,核心工具加起来可能就几百KB,这在资源受限的嵌入式设备上简直是救命稻草。

核心概念: BusyBox通过符号链接机制工作。当系统调用/bin/ls时,实际上执行的是/bin/busybox ls。BusyBox根据传入的argv[0]参数,判断要执行哪个命令。

3.1 BusyBox简介

BusyBox由Bruce Perens在1996年发起,最初是为了制作Debian的安装盘。后来逐渐演变成嵌入式Linux的事实标准。我个人习惯把它比作“Linux命令的压缩包”——你只需要一个二进制文件,就能获得一个基本完整的用户空间环境。

它的核心优势就三个:

  • 体积小: 一个完整的BusyBox二进制文件通常只有几百KB到1MB左右
  • 功能全: 涵盖了init、shell、文件操作、网络工具、进程管理等核心功能
  • 可定制: 你可以按需选择要编译哪些命令,不需要的统统砍掉

我在项目中遇到过最极端的情况——一个路由器项目,Flash总共只有2MB。去掉内核和Bootloader,留给根文件系统的空间不到500KB。没有BusyBox,这种项目根本没法做。

3.2 BusyBox的编译与安装

编译BusyBox其实不复杂,但有几个坑需要注意。我先说说标准流程。

3.2.1 获取源码

从官网或Git仓库拉取源码:

git clone git://busybox.net/busybox.git
cd busybox

我个人建议用稳定版本,别追新。比如1.36.x系列就很成熟。我曾经在某个项目里用了开发版,结果有个网络工具的行为跟文档对不上,排查了两天才发现是代码还没合进去。

3.2.2 配置交叉编译环境

嵌入式开发必须用交叉编译器。假设你的交叉编译器前缀是arm-linux-gnueabihf-:

export CROSS_COMPILE=arm-linux-gnueabihf-
export CC=${CROSS_COMPILE}gcc
export AR=${CROSS_COMPILE}ar
export LD=${CROSS_COMPILE}ld

注意: 交叉编译器必须与你的目标架构匹配。ARM、MIPS、RISC-V的编译器不能混用。我曾经见过有人用x86的编译器编译ARM的BusyBox,结果放到板子上直接Segmentation Fault。

3.2.3 配置与编译

BusyBox的配置方式和Linux内核类似,使用menuconfig:

make menuconfig

配置完成后,执行:

make -j$(nproc)
make install

make install默认会把编译好的文件安装到_install目录下。这个目录结构就是根文件系统的雏形:

_install/
├── bin/
│   ├── busybox
│   ├── ls -> busybox
│   ├── cp -> busybox
│   └── ...
├── sbin/
│   ├── init -> ../bin/busybox
│   └── ...
└── usr/
    └── ...

嗯,这里要注意:make install只是创建符号链接,真正的可执行文件只有一个busybox。你把这个目录打包成根文件系统镜像,烧到板子上就能用了。

3.3 BusyBox的配置选项

配置选项是BusyBox最灵活的地方。进入make menuconfig后,你会看到一个类似内核配置的界面。我挑几个关键部分说说。

配置项 说明 我的建议
Settings → Build Options 选择静态编译还是动态编译 新手建议静态编译,省去动态库依赖的麻烦
Settings → Cross Compiler prefix 指定交叉编译器前缀 也可以在环境变量中设置,二选一即可
Linux Module Utilities 模块加载相关工具(insmod、rmmod等) 如果内核需要动态加载模块,必须开启
Networking Utilities 网络工具(ifconfig、ping、telnet等) 按需选择,不需要的关掉可以省空间
Init Utilities init进程相关配置 如果使用BusyBox init,必须开启

小技巧: 配置完成后,可以用make defconfig生成默认配置,然后在此基础上修改。这样不容易漏掉关键选项。我每次新项目都是这么干的。

还有一个很重要的选项——静态编译。在Settings → Build static binary (no shared libs)里开启。为什么推荐静态编译?因为嵌入式系统的动态库环境可能不完整,你编译时链接的libc版本跟板子上的对不上,跑起来就会报错。静态编译直接把所有依赖打进二进制文件,省心。

当然,静态编译也有代价——文件体积会变大。但通常也就大个一两百KB,对于现在的Flash容量来说,完全可以接受。

3.4 BusyBox的启动流程

这是理解根文件系统如何“活起来”的关键。BusyBox的启动流程,说白了就是内核启动后,如何通过BusyBox完成系统初始化。

流程大致是这样的:

  1. 内核启动:内核完成硬件初始化后,挂载根文件系统
  2. 执行init:内核在根文件系统中寻找/sbin/init(或/bin/init等),这个init就是BusyBox的符号链接
  3. 读取inittab:BusyBox init读取/etc/inittab配置文件
  4. 执行启动脚本:根据inittab的配置,执行/etc/init.d/rcS等启动脚本
  5. 启动shell:最后启动一个getty或shell,等待用户登录

下面这张图可以帮你理清整个流程:

BusyBox 启动流程 BusyBox 启动流程 1. 内核启动 2. 挂载根文件系统 3. 执行 /sbin/init (BusyBox) 4. 读取 /etc/inittab 5. 执行 /etc/init.d/rcS 说明:inittab 中通常配置了 sysinit 动作,指向 /etc/init.d/rcS rcS 脚本负责挂载文件系统、配置网络、启动服务等初始化工作

这里有个关键文件——/etc/inittab。它的格式很简单,每行一个条目:

# 格式:id:runlevel:action:process
::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty -L ttyS0 115200 vt100
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a

各字段含义:

  • id:终端设备名,可以为空
  • runlevel:运行级别,BusyBox中通常忽略
  • action:动作类型,如sysinit、respawn、askfirst等
  • process:要执行的程序

常见的action类型:

action 含义
sysinit 系统初始化时执行一次,通常用于启动脚本
respawn 进程退出后自动重启,常用于getty
askfirst 类似respawn,但启动前会先询问用户
once 只执行一次,不等待结束
ctrlaltdel 按下Ctrl+Alt+Del时执行
shutdown 系统关机时执行

避坑指南: 我曾经在一个项目里忘了创建/etc/inittab文件,结果BusyBox init启动后直接报错,系统卡在“Kernel panic - not syncing: Attempted to kill init!” 后来查文档才发现,如果没有inittab,BusyBox会使用默认行为——直接启动一个shell。但前提是你要在编译时开启了相应的配置选项。

启动脚本/etc/init.d/rcS是系统初始化的核心。它通常负责:

  • 挂载/proc、/sys、/dev等虚拟文件系统
  • 配置网络接口
  • 启动系统服务(如syslogd、telnetd等)
  • 设置主机名、时区等

一个简单的rcS脚本示例:

#!/bin/sh

# 挂载虚拟文件系统
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp

# 创建必要的设备节点
mdev -s

# 配置网络
ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up

# 启动系统日志
syslogd

# 启动telnet服务(方便调试)
telnetd -l /bin/sh

嗯,这里要注意:rcS脚本必须有可执行权限(chmod +x),否则init会报错。我刚开始做的时候经常忘记加执行权限,然后一脸懵逼地看日志。

最后总结一下BusyBox启动的核心逻辑:内核 → init → inittab → rcS → shell。这个链条上的每个环节都缺一不可。你只要把inittab和rcS写对了,系统就能正常启动起来。