干嵌入式Linux这行,BusyBox就是个绕不开的东西。打开你手边任何一个路由器、机顶盒、工控板,或者随便一台跑Linux的物联网设备,进到shell里敲几条命令,ls、ps、ifconfig、mount,十有八九都是BusyBox干的活儿。它被叫“嵌入式Linux的瑞士军刀”不是没道理——一个几兆大小的二进制文件,把几十上百个常用命令都装了进去,这在寸土寸金的嵌入式设备上,几乎是从零构建根文件系统时绕不开的起点。
这篇文章我想把自己在项目中实际用过的流程和踩过的坑完整整理一遍,从BusyBox的编译配置、静态动态链接的选择,到根文件系统目录搭建、init进程脚本、mdev设备管理,再到NFS挂载调试和镜像打包。内容适合两类人:一类是刚入坑嵌入式Linux、对“怎么把一个系统跑起来”还比较模糊的初学者;另一类是已经能编译内核、但一碰根文件系统就各种报错的老哥。看完你至少能自己从零做出一套能启动的根文件系统,也大概明白每一步为什么要这么做。
1. 为什么嵌入式Linux离不开BusyBox这个“瑞士军刀”
1.1 BusyBox的本质:一个二进制文件,装下一整套工具集
先看一个很实际的问题:一个嵌入式设备上,Flash通常也就8MB、16MB,内存可能只有64MB甚至更小。你不可能像桌面Linux那样把GNU coreutils、util-linux、procps那一整套工具全装上,光是/bin/ls、/bin/bash、/usr/bin/top这些加起来就好几十MB,挑几个常用的一装,Flash就爆了。
BusyBox解决这个问题的方式很聪明。它采用“单二进制多applet”的架构:所有工具都被编进同一个可执行文件,运行的时候靠argv[0](或者busybox xxx这种显式调用方式)来决定派发到哪个子命令上。传统Linux里是/bin/ls、/bin/cp、/bin/mkdir各自独立,BusyBox里则是busybox ls、busybox cp,通过在bin目录下建立指向同一个busybox二进制的软链接来模拟传统命令调用方式。所以你看到设备上ls、cp、mv都是一个inode,ls -l会显示它们都指向/bin/busybox。
这种架构带来的好处不仅是一份代码库统一维护,功能裁剪也极其方便。你不需要一个个去找依赖,只要在编译配置里勾选某个applet,它就进二进制了。反过来说,你不勾,它连代码都不会编进去。
从源码角度看,每个applet都有个统一入口,编译时会根据配置生成一个applet_tables,里面是命令名和对应处理函数的映射表。启动时BusyBox拿argv[0]去查这张表,找到就执行对应函数,找不到就报错。这套设计让BusyBox非常紧凑,同时也决定了它跟“完整版”Linux命令肯定有差异——选项少、实现精简、对POSIX兼容是“能用但不保证全部实现”。做嵌入式开发的一定要习惯这个“够用就好”的前提,不要一上来就指望BusyBox完全等价于桌面版工具。
1.2 从BusyBox看嵌入式根文件系统的特殊约束
根文件系统这个词,初学者容易跟“根分区”“内核”搞混。内核负责初始化硬件、调度任务,但真正的用户空间环境——/bin、/sbin、/etc、/home这些目录,以及各种系统服务、配置文件——都在根文件系统里。内核启动后要挂载根文件系统,然后执行里面的init程序作为1号进程,系统才算真正“活”过来。
这里有个关键约束:嵌入式设备的根文件系统需要照顾Flash寿命、启动速度和体积,不能像PC那样直接原封不动地把一个大rootfs扔进去。所以嵌入式开发里,根文件系统常常被格式化成squashfs(只读压缩)、jffs2/ubifs(针对NAND优化的日志文件系统),或者干脆做成initramfs,由内核启动时一次性解压进内存。而这些不同的格式和启动方式,都要在构建rootfs的阶段提前规划好。BusyBox因为体积小、依赖少,天然是往里放的第一批内容。
我个人的体会是:BusyBox只是根文件系统的一部分,但它是系统的地基。地基打好了,后面加应用、加库、加服务才不闹心。而且版本选择上有讲究——老项目还在用BusyBox 1.22.1这种上古版本,新项目已经跟进到1.36.x了。版本之间inittab语法、mdev配置方式有些细微变化,看文档时要先确认版本,不然容易照着网上老教程踩坑。
2. 编译BusyBox之前,先想清楚三个关键选择
2.1 选哪套交叉编译工具链
BusyBox编译本身不复杂,但第一步交叉编译工具链的选择会影响后面整个流程。常见选择有几种:芯片原厂提供的工具链、Buildroot自己编出来的工具链、Linaro的预编译工具链,以及根据发行版算号还是源代码从零构建的crosstool-NG。
实际项目里,如果用的是STM32MP1、i.MX、AM335x这类平台,我建议直接用官方SDK里的工具链,或者用Buildroot生成一套。因为内核、uboot、用户空间程序都会用同一套编译器和C库,保持统一最省事。只是练手的话,用Linaro出品的预编译arm-linux-gnueabihf工具链也没问题,网上教程多,踩坑容易搜。
选工具链时要特别注意两个地方:C库是glibc还是musl还是uClibc-ng,以及是32位还是64位。这直接关系到你编译出的BusyBox是动态链接还是能顺利静态链接,也关系到NFS挂载调试时用户空间的库依赖。比如选了glibc的armhf工具链,动态方案下你的rootfs里必须有对应的ld-linux-armhf.so.3和libc.so.6等库文件。
2.2 静态编译还是动态编译:从Flash体积和调试效率权衡
这是很多人第一次编译BusyBox时都会纠结的问题。简单说,静态编译的意思是把C库代码直接编进busybox二进制,运行时不再依赖外部.so库;动态编译则是编出来的程序依赖根文件系统里的C库文件。
静态编译最大的优势是省心。你把一个静态busybox扔到rootfs里,只要内核能挂载根文件系统,它就能跑,不用管库的事,特别适合容器镜像、initramfs、急救盘这种环境。而且NFS调试时不用每次同步库文件,bin/bash不用配,直接静态搞定。缺点是Flash占用偏大,因为一份C库代码被塞进了每一个静态程序里;另外用glibc做静态编译时DNS解析、nss相关功能容易出问题,虽然BusyBox不太涉及。
动态编译则能明显压缩体积,而且如果你后面还要放自己的应用,反正都得带C库,大家一起动态共用,总体Flash占用更小。坏处是rootfs结构里必须准备好/lib下的C库和动态链接器,版本还得匹配,稍有不慎启动时就报“can't load library 'libc.so.6'”,排查起来有点烦。
我的建议是:开发调试阶段优先静态编译,先把系统跑通,少一个变量;等进入稳定阶段、需要控制体积时再切到动态,并且明确锁定C库版本。也有折中方案,用musl库做静态编译,体积比glibc小不少。最近几个新项目我都用的musl,体积和部署便利都挺满意。
操作上,menuconfig里找到Settings -> Build static binary (no shared libs),勾上是静态,不勾是动态。用musl还得到工具链级别去选,这不是BusyBox能单独决定的。
2.3 配置裁剪:从defconfig起步,再按需调整
BusyBox配置有几种起点。最省事的是make defconfig,它会生成一份默认配置,几乎所有applet都编译进去,说白了“大而全”。初学者建议先这样,跑通了再裁剪。生产环境则更多是从一份已知可工作的配置开始改,或者用make allnoconfig这种“几乎全关”的基线,然后一个一个勾自己需要的。
这里重点说下我实际用的裁剪思路。核心是先从启动必需的命令开始:init、sh、mount、umount、mdev、ls、cat、echo、mkdir、rm、cp、mv、ps、ifconfig、ping、route、udhcpc这些肯定要;然后是调试辅助的:vi(或者sed、awk)、tail、grep、find、tar、lsmod、insmod、rmmod、dmesg、kill、killall、free、df、du、top;再根据应用层需求补网络相关的:wget、nc、telnetd、dropbear(如果你不打算用整个openssh的话,BusyBox里不带dropbear,但可以集成进来,后面细说)。
记住一个原则:不要为了追求“小”盲目裁剪。嵌入式调试本来就够痛苦了,连个grep、vi都没有,改个配置还得来回编译烧写,那个时间成本比多占几百KB Flash高得多。我一般在开发版配置里保留常用调试命令,等到量产版本再专门做一版裁剪配置。两个配置用不同的config文件名分开保存,需要哪个就用哪个。
3. 手把手:用BusyBox从零搭建一套可用的根文件系统
3.1 环境准备与BusyBox编译实战
假设你已经准备了一台装有Linux的Ubuntu 22.04机器,我们实际操作一遍。先把源码下下来,当前最新的稳定版本大概在1.36.x。可以用官网下载,也可以用git仓库clone,我习惯clone官方git,方便切换版本看历史。
git clone https://git.busybox.net/busybox cd busybox git checkout 1_36_stable接着配置交叉编译环境。如果工具链在PATH里,那就很简单:
export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make defconfig make menuconfig然后进menuconfig做几个关键设置。第一,Settings -> Build static binary (no shared libs),开发阶段我勾上;第二,Settings -> Destination path for 'make install',改成你自己的输出目录,比如/home/user/rootfs,或者直接编译后用make install CONFIG_PREFIX=/home/user/rootfs指定。第三,如果你想用musl,那是在工具链层面选的,BusyBox这边不用改。
配置保存退出后,直接编译:
make -j$(nproc) make install如果一切正常,你会在CONFIG_PREFIX指定的目录下看到bin、sbin、usr三个目录,里面是各种指向busybox的符号链接,外加一个bin/busybox实体文件。用file命令看一下:
file bin/busybox输出里能看到“statically linked”就说明静态编译成功,这时候你就可以把它丢到rootfs里了。
这里插一句版本相关的坑。有些发行版会自带打了补丁的BusyBox,比如网上搜到“busybox v1.22.1(kylin1:1.22.0)”这种,就是基于1.22.0加了发行版定制补丁。自己编译倒是没这个顾虑,但如果跟别的工具链协作,注意尽量统一版本,避免inittab、mdev的行为差异导致莫名的奇怪问题。
3.2 搭建rootfs目录骨架:除了busybox还需要什么
很多人以为把busybox放进目录就算根文件系统完成了,这其实差得远。内核挂载根文件系统后,会去找init程序(默认/sbin/init),如果你没有正确放置init,内核会直接打印“Attempted to kill init!”或者panic。一个能启动的最小rootfs至少包含这么几块:BusyBox本身、必需的C库(动态方案时)、/etc下的配置文件、/dev下的设备节点(或配合mdev自动创建)、/proc和/sys挂载点,以及init进程要执行脚本。
手动建一遍目录:
sudo mkdir -p rootfs/{bin,sbin,usr,etc,dev,proc,sys,root,tmp,var,mnt,lib} sudo chmod 1777 rootfs/tmp sudo chown -R root:root rootfs设备节点这里,静态方法是用mknod手动创建关键的几个:console、null、zero、tty。但更推荐的做法是内核用devtmpfs(CONFIG_DEVTMPFS=y)自动管理/dev,配合BusyBox的mdev在用户空间做补充动态管理。如果你内核开了devtmpfs,/dev下的设备节点会在系统启动时自动生成,那么rootfs里只需要保证/dev目录存在即可,甚至mknod那一步都可以省掉。这里我提醒一下:如果你用的老内核没有devtmpfs,仍然需要手动mknod,或者挂载tmpfs后靠mdev -s扫描sysfs生成。所以开机脚本的顺序是:先挂proc、sysfs,再挂/dev为tmpfs,再执行mdev -s,这一套下来设备节点才会完整。
接着把之前编译好的BusyBox相关文件拷贝进rootfs:
cp -a /home/user/rootfs_staging/* rootfs/注意,这里的CONFIG_PREFIX目录只是BusyBox的bin/sbin/usr,你还得再拷C库(如果用动态),或者什么都不用拷(如果用静态)。
3.3 init与inittab:系统启动后第一个用户进程怎么工作
内核启动完会尝试执行根文件系统里的init程序。BusyBox提供的init在用/sbin/init软链接启动时会读取/etc/inittab,按照里面定义的action依次执行各项任务。最小可用的inittab大概这样:
::sysinit:/etc/init.d/rcS ::askfirst:-/bin/sh ::restart:/sbin/init ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r每一行格式是id:runlevel:action:process。嵌入式里一般id留空,runlevel也留空(BusyBox不严格区分runlevel)。重点说几个action:
sysinit:内核启动后第一个要执行的动作,一般用来挂载proc/sysfs、初始化设备节点、配置网络等,对应的脚本通常叫/etc/init.d/rcS。askfirst:在指定终端上启动一个shell,并且在启动前打印“Please press Enter to activate this console”,按回车才继续。串口调试时这个非常有用,能避免程序输出把控制台刷得乱七八糟。restart:init重启时执行的动作,通常直接指回/sbin/init。ctrlaltdel、shutdown:按Ctrl+Alt+Del和关机时的处理动作。
再说那个rcS脚本,它才是真正干活的。一个典型的最小rcS:
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev mkdir -p /dev/pts mount -t devpts none /dev/pts echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s ifconfig lo 127.0.0.1 up这里有个顺序问题我反复强调:mdev -s必须在/sys挂载之后执行,因为它是扫描/sys里的设备信息来生成/dev节点的。如果你把顺序搞反了,就会出现“dev目录空空如也”“串口无法登录”这种怪问题。另外,如果内核配置里有CONFIG_DEVTMPFS,其实/dev已经被内核自动挂载了,这时候再挂tmpfs反而会覆盖。我建议一行mount -t devtmpfs none /dev,或者直接不挂靠内核自动挂载,稳妥一点。
脚本写完记得加可执行权限:
chmod +x rootfs/etc/init.d/rcS很多初学者第一次启动就卡在这里:脚本没加执行权限,或者第一行没有写#!/bin/sh,导致sysinit“静默失败”,然后系统看起来像是没有任何输出或者直接panic。排查优先级很高。
再补充一下登录方式。如果只做串口调试,用askfirst直接给shell就够了,不需要登录用户名密码。如果需要登录提示,可以启用BusyBox的loginapplet,并在inittab里写::respawn:/sbin/getty -L ttyS0 115200 vt100,然后配合/etc/passwd、/etc/shadow设置账号。实际项目里,产品级设备通常需要登录验证,开发板则怎么方便怎么来。
3.4 设备节点与mdev机制:让/dev目录自动生成
设备节点本质上是用户空间与内核驱动的接口。传统Linux里,/dev下的节点是静态创建的,每加一个驱动就手动mknod一次,这在嵌入式里不现实。mdev是BusyBox提供的一个轻量级udev替代品,它利用内核的hotplug机制,在设备插入或加载驱动时执行/sbin/mdev,动态生成或删除对应的设备节点。
要正常工作,系统需要满足几个条件:
- 内核编译时打开
CONFIG_HOTPLUG和CONFIG_NET相关选项(hotplug默认通过netlink机制通知用户空间)。 /proc/sys/kernel/hotplug要设置成/sbin/mdev,这样设备事件发生时内核会调用mdev。/etc/mdev.conf按需配置设备节点的权限和属主。
一个简单的mdev.conf示例:
# 格式:设备正则 属主:属组 权限 sd[a-z][0-9]* root:disk 0660 tty[A-Za-z0-9]* root:tty 0660 null root:root 0666实际上如果只是要“设备节点能自动生成”,mdev.conf可以不用配,mdev -s会按sysfs里的信息自动生成。mdev.conf主要是用来定制权限、属主,以及屏蔽某些设备节点生成。
如果设备需要挂载U盘,你可能还要在rcS脚本里加一行echo /sbin/mdev > /proc/sys/kernel/hotplug来确保热插拔事件被处理。关于U盘测速这种需求,那是另外一篇文章了,但设备节点这关过不了,后面全是白搭。
3.5 网络配置、主机名与系统时间等常规项目
rootfs里还有一些“起眼但重要”的配置值得花时间检查。基础的比如/etc/hostname(决定shell提示符里的主机名),/etc/profile(默认的PATH、PS1、环境变量),/etc/fstab(静态挂载列表)。别看这些不起眼,缺少了它们,很多程序会在启动时报错或者行为异常。
/etc/profile一个常见写法:
export PATH=/bin:/sbin:/usr/bin:/usr/sbin export PS1='[\u@\h:\w]\$ '/etc/fstab示例:
# device mountpoint type options dump pass proc /proc proc defaults 0 0 sysfs /sys sysfs defaults 0 0 tmpfs /tmp tmpfs defaults 0 0如果开发板想自动获取IP,可以写一个脚本调用udhcpc:
udhcpc -i eth0 -s /etc/udhcpc/default.scriptBusyBox源码的examples/udhcpc/default.script就是现成模板,拷到rootfs里再加执行权限就行。
另外,嵌入式设备经常出现“时间总是不对”的问题。如果没有硬件RTC电池,每次开机都是从1970年1月1日开始。实际项目里可以在rcS里做NTP时间同步,或者在产品联网后定期同步。BusyBox自带ntpd,ntpd -p ntp.aliyun.com这种一行命令就能搞定,但得等网络起来以后执行,别在rcS太早的阶段跑。
3.6 启动调试:NFS挂载根文件系统是最灵活的调试方式
rootfs搭完之后,怎么让板子跑起来?最直接的方式是打包成镜像烧到Flash或者SD卡里,但每次改动都重新烧写太痛苦了。开发初期强烈推荐用NFS方式:内核通过命令行参数root=/dev/nfs nfsroot=192.168.1.10:/home/user/rootfs直接挂载开发机上的rootfs目录,板子上的每一次文件改动都是热生效的,调试效率高几个量级。
NFS调试在uboot里设置bootargs时这样写:
setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.10:/srv/nfs/rootfs ip=dhcp rw' saveenv boot开发机上要装并配置NFS服务,Ubuntu上大致:
sudo apt install nfs-kernel-server sudo vim /etc/exports # 添加下面一行,加上自己的网络段 /srv/nfs/rootfs 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash) sudo exportfs -ra sudo systemctl restart nfs-kernel-server注意no_root_squash,没有这个选项的话板子上root用户对rootfs文件只有nobody权限,改文件会Permission denied。这个坑我踩过。
NFS虽然方便,但有个前提:内核网络驱动必须已经能正常工作,也就是说你的内核在启动时要能起来eth0,并且IP配置正确。如果网络都起不来,那就只能回到SD卡或者flash方案。
3.7 制作可烧写的根文件系统镜像
等调试得差不多,要固化到板子上,就得把rootfs目录制作成镜像。常见格式有几个:
- ext4:最简单通用,适合SD卡和eMMC,直接
mkfs.ext4 -d rootfs rootfs.ext4即可。 - squashfs:只读压缩,适合存到Flash后挂载为根文件系统,优点是省空间,缺点是启动后不能直接写根目录,需要把/var、/tmp等可写目录单独挂到tmpfs。
- ubifs/jffs2:针对NAND Flash的日志文件系统,需要ubinize等工具配合,不同NAND page size、eraseblock size都要在制作时指定。
以ext4为例:
dd if=/dev/zero of=rootfs.ext4 bs=1M count=64 mkfs.ext4 -d rootfs rootfs.ext4然后把镜像写到SD卡或者通过fastboot/uboot烧进eMMC。如果你打算做initramfs,那更简单,只要把rootfs目录打包成cpio.gz,内核对这个包解压到内存后用tmpfs当根文件系统,不需要Flash上建立rootfs分区:
cd rootfs find . | cpio -H newc -o | gzip > ../initramfs.gz系统起来后根文件系统就在内存里,所有改动都是临时的,适合急救系统、Live CD、以及一些内存运行的应用场景。如果你对“从零构建完整嵌入式Linux系统镜像”这个概念比较感兴趣,initramfs + 静态BusyBox是最容易上手的组合,因为对Flash布局要求最低。
4. 实战高频踩坑与排查技巧实录
4.1 内核panic:No init found
这是最经典的问题。内核启动到准备执行init程序时,在根文件系统里找不到/sbin/init或者/bin/sh之类的程序,就打印“Kernel panic - not syncing: No init found”之类的信息。排查思路分几步:先确认rootfs目录里/init或/sbin/init软链接是否真的存在,是否指向busybox;再确认busybox是否编译成功、文件是否损坏;接着确认内核启动参数里root设备对不对,rootfs是不是真的挂载上了;最后如果用了initramfs,确认打包时目录结构是否正确。
一个常被忽略的点:如果busybox是动态链接的,而rootfs里没有/lib下的C库,那么即使init软链接存在,也根本加载不起来,同样会panic。所以新手排查时先用file确认是静态还是动态。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| Kernel panic: No init found | rootfs里缺少init或busybox库依赖缺失 | 检查/sbin/init、/bin/busybox是否存在,用file确认链接方式 |
| Kernel panic: Attempted to kill init | init启动后立即退出 | 检查inittab、rcS脚本和执行权限 |
| 启动卡在“Freeing unused kernel memory” | rootfs挂载失败或init执行失败 | 换NFS调试,加上init=/bin/sh看能否进shell |
4.2 “can't access tty; job control turned off”
串口启动后登录shell时,经常能看到这句提示,然后shell里Ctrl+C、Ctrl+Z这些作业控制全失效。原因很简单:当前会话没有关联一个可用的tty。BusyBox init默认是在一个虚拟控制台上跑shell的,而串口设备如果没有正确配置为ttyS0(或者其他实际端口),init就无法分配作业控制。
解决办法通常是检查inittab里askfirst那行的设备名是否和实际串口一致,以及内核启动参数console是否正确。比如nanoPi这类板子,串口设备可能是ttyAMA0,而你inittab里写的是ttyS0,那就会出现这个提示。把inittab改成实际端口名,或者直接去掉dev前缀让init自动选择当前console,也能解决:
::respawn:-/bin/sh如果内核cmdline里的console=指定了ttyS0,inittab里这样写通常就能分配到正确的tty。另外,直接把登录方式改成::respawn:/sbin/getty -L ttyS0 115200 vt100也是一种标准做法。
4.3 动态链接的busybox提示找不到libc.so.6
这个问题在NFS调试时特别常见。你用的交叉工具链是glibc版本A,系统里的libc版本是B,板子上拷过去的lib库跟busybox版本不匹配,就会报这个错。排查办法先看依赖关系:
arm-linux-gnueabihf-readelf -d bin/busybox | grep NEEDED arm-linux-gnueabihf-readelf -l bin/busybox | grep interpreter确认它需要的动态链接器路径,比如/lib/ld-linux-armhf.so.3,再去rootfs里看这个文件是否存在、版本是否一致。C库拷贝到rootfs时,结构也有讲究:glibc的.so通常是符号链接,指向带版本号的实际文件,你要把整个链接关系一起拷过去,只拷最底层那个会出问题。最稳的做法是把工具链里的arm-linux-gnueabihf/libc目录下所有内容按原目录结构拷到rootfs里。如果你用的是Buildroot生成的工具链,Buildroot编译完通常会自动把库精简好放到output/target里,直接拿那个目录当rootfs基底更方便。
4.4 串口命令行能进,但mdev没生效
设备节点一塌糊涂,串口能进shell但/dev下空空的。最常见的两个原因:一是rcS里mdev -s跑得太早,/sys还没有挂载,mdev扫了个寂寞;二是内核没开hotplug或者没把/proc/sys/kernel/hotplug设置成/sbin/mdev。
调试方法很简单,在shell里手动执行:
mount -t proc none /proc mount -t sysfs none /sys echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s如果手动执行完设备节点就出来了,说明rcS顺序问题或者脚本没执行。如果手动也不行,检查busybox配置里是否包含了mdev applet——deconfig默认应该有,但裁剪配置里可能被去掉而不自知。还有一点,mdev依赖sysfs,如果你的内核没挂载sysfs,mdev -s会直接报错,这个看dmesg输出就能发现。
4.5 NFS挂载rootfs时网络起不来或read-only
NFS调试的依赖链比较长,网络、NFS服务、内核配置、权限,任何一个环节出问题都起不来。先把问题切分清楚:板子上电后,uboot阶段能不能ping通开发机?内核起来后eth0有没有被识别?uboot的网络、内核的网络驱动、rootfs里用户空间的网络配置,这三个是完全独立的层面,别混在一起排查。
NFS rootfs如果以只读方式挂载,很多程序会行为异常(比如无法创建socket文件、tmp目录不可写)。出现这种情况检查bootargs里有没有加rw,以及开发机/etc/exports里的导出选项是否带了rw。另外no_root_squash尽量加上,否则root用户也会被当作nobody。
还有一个很多人不知道的坑:NFS版本。新内核和开发机的NFS服务都默认用NFSv4,而一些老工具链编出来的busybox mount命令可能只懂NFSv3。如果挂载时报错mount: unknown filesystem type 'nfs'或者mount.nfs: Protocol not supported,可以在nfsroot参数里强制指定nfsvers=3,或者在开发机/etc/exports里加fsid=0之类的参数。
5. BusyBox周边的常用扩展:dropbear、常用脚本与最小化配置模板
BusyBox本身不带SSH服务,但它的大小和风格跟dropbear很搭。dropbear是一套轻量级SSH实现,包含服务端dropbear和服务端密钥管理工具,二进制加起来一两百KB,特别适合嵌入式。编译方式跟BusyBox类似,交叉编译后把dropbear、dropbearkey放进/usr/sbin,配置好/ect/dropbear目录,在inittab加一行::respawn:/usr/sbin/dropbear -R,就能通过SSH远程登录开发板了。相比telnet,SSH的方便程度和安全等级都高一个等级,几乎是产品设备的标配。
再提一句总线BusyBox init脚本的写法。很多人习惯把一堆设置全塞在rcS里,结果调试时注释来注释去特别混乱。我建议把rcS做成一个按需source的框架:
#!/bin/sh for script in /etc/init.d/S*; do [ -x "$script" ] && "$script" done然后在/etc/init.d/下建S01mdev、S02network、S03dropbear等脚本,每个脚本只管一件事。这样增删服务只需要改文件名就行,清晰得多。这个习惯我从BusyBox官方示例里看到之后,就再也回不到“一个大脚本从头写到尾”的方式了。
另外,如果你在用Buildroot构建整个系统,其实BusyBox、dropbear、各种库和工具都已经帮你集成好了,直接menuconfig勾选即可。但从学习角度,我强烈推荐手动做一遍。手动做过一次,你才知道Buildroot每一步到底在干什么,以后遇到问题才知道去哪里查。
最后再分享一个小技巧:编译BusyBox时加上CONFIG_FEATURE_VERBOSE_CPRINTT或者保留全路径CONFIG_FEATURE_INSTALLER,能让make install时打印更多信息,方便你查软链接是否建立成功。还有,make help里有个busybox目标,可以直接生成一个名为busybox的文件而不需要完整install到目录,配合自定义符号链接脚本,有时比make install更灵活。