☰
BusyBox实战:手工搭建嵌入式Linux根文件系统
2026/11/5 5:59:05 网站建设 项目流程

我从一个实际项目说起吧。早些年我帮客户做一款工业网关,用的主控是ARM Cortex-A7,内存只有64MB,Flash 256MB。系统跑起来总共占用不到15MB的空间,启动时间控制在5秒以内,这就是BusyBox的功劳。当时团队里有个刚从桌面Linux转过来的同事,第一反应是想在板子上跑Ubuntu的core版本,我直接劝退了。倒不是说Ubuntu不好,而是对这个场景来说,一套完整的GNU工具链和systemd体系带来的收益远小于开销。而BusyBox这一个程序,几百个命令,占空间小、行为可预期、定制灵活,很多时候它才是嵌入式Linux里最合适的那个“操作系统外壳”。

这篇博文我会把BusyBox的原理拆开讲清楚,再带大家从零到一,用它手工搭一套根文件系统。内容包括:BusyBox的applet工作方式、编译配置的关键选项、根文件系统的目录骨架、init启动流程、集成Dropbear实现SSH登录、用NFS挂载根文件系统调试,以及我在实际项目中踩过的各种坑。

1. 从“跑不动的Linux”说起:BusyBox到底解决什么问题

1.1 一套完整的GNU工具链在嵌入式板子上有多贵

你在PC上敲ls、cp、grep的时候,背后对应的是coreutils、procps、grep这些独立软件包里的程序。每个程序是独立的ELF可执行文件,自己带着一堆动态库依赖和配置文件。在x86服务器上这无所谓,硬盘按TB算,内存按GB算。但放到嵌入式板子上,情况就完全不同了。

拿我那块64MB内存的板子来说。一个完整的coreutils包装上去,大概需要几十MB的空间,而grep、sed、awk这些再来一圈,Flash空间就快见底了。更重要的是,这些程序在启动阶段逐个加载,每个都要经过动态链接器解析、映射、初始化,消耗的时间和内存积少成多,对启动速度的影响非常明显。

BusyBox的做法完全不同。它把几百个命令的代码逻辑塞进了一个可执行文件里,在系统里你只看到一个/bin/busybox,ls、cp、cat这些命令都是指向它的符号链接。当你在终端敲ls的时候,内核加载的是同一个busybox程序,只是启动参数不同而已。

1.2 BusyBox能干什么:一张命令清单看清边界

BusyBox能提供的命令数量与配置选项有关,完整的配置打开后大约有400多个applet。我这里整理了一份常用的命令清单,方便新手对照理解它的能力边界。

分类常用命令
文件操作ls, cp, mv, rm, mkdir, touch, cat, tail, head, find, tar, gzip
文本处理grep, sed, awk, cut, sort, uniq, wc, tr, vi
进程管理ps, top, kill, killall, nice, renice, nohup, pgrep
网络工具ifconfig, route, ping, wget, telnetd, netstat, nc, udhcpc, httpd
系统管理mount, umount, dmesg, free, df, du, sync, mdev, init, reboot, poweroff
shell环境ash, sh, msh
其他实用crond, ntpd, syslogd, klogd, watchdog, fdisk, mkfs.vfat, hwclock

这份清单覆盖了嵌入式Linux日常开发和运维的绝大部分需求。有一些命令我是建议额外使用完整版工具的,比如dd、parted这些涉及数据恢复和分区操作的,BusyBox的实现虽然能用,但容错性比GNU原版弱一些。我在实际项目中如果设备上空间允许,通常会单独放一个完整的e2fsprogs的e2fsck进去,用于文件系统修复。

1.3 不是只有小系统才用BusyBox

很多人的印象是BusyBox只给单片机级别的设备用,其实不然。Docker容器里的大量精简镜像,底层就是BusyBox。安卓系统的调试模式里也有它的身影。工业路由器、智能汽车的车机系统、甚至一些桌面Linux的initramfs阶段,都会用BusyBox作为过渡工具。

原因很简单,initramfs要在极小的内存环境里完成挂载真正根文件系统的任务,它需要一套完整的、依赖极少的基本工具。BusyBox静态编译之后,对系统几乎零依赖,扔进去就能用。这种“一个二进制打天下”的特性,让它在很多对体积和依赖敏感的场合成了首选。

2. 瘦身成功的秘密:applet机制与编译选型的底层逻辑

2.1 从main函数到符号链接:BusyBox的设计哲学

BusyBox的源码根目录下有个文件叫libbb/appletlib.c,里面藏着一个非常精巧的分发机制。当内核执行busybox的时候,main函数第一件事就是检查argv[0],也就是调用这个程序时使用的名字。如果是ls,就走ls的applet_main;如果是cp,就走cp的applet_main。这就是符号链接能工作的根本原因。

源码中每个applet都有一个类似applet_main的入口点,它们被统一注册在applet表里。编译的时候通过CONFIG_FEATURE_SH_STANDALONE这类选项,还可以让BusyBox里的shell在找不到外部命令时回退到内置applet,实现“无外部依赖”运行。

有个细节值得注意:BusyBox并不只靠符号链接来触发。当argv[0]显示是busybox本身的时候,它会把第一个参数当作要执行的applet名,比如busybox ls -l的效果和ls -l一样。这在一些特殊情况很好用——比如某个脚本里硬编码了/bin/busybox cat /etc/passwd,即使cat的符号链接坏了,命令依然能执行。

2.2 动态链接还是静态链接:一个影响一生的决定

编译BusyBox时,最重要的一个选择是CONFIG_STATIC是否打开。这个选项直接决定了最终生成的二进制文件是否依赖动态库。

对比项动态链接(默认)静态链接
二进制体积约800KB-1MB约1.5MB-2.5MB
运行依赖需要glibc/musl等C库无任何外部依赖
调试便利性可用gdb正常调试需注意strip后符号丢失
适用场景Flash充足、需要节省内存initramfs、极小系统、chroot环境

我个人的建议是,如果不是空间特别紧张,尽量使用静态链接。原因很实在:嵌入式系统里动态库版本一旦升级,很容易出现busybox与新libc兼容性出问题的情况。静态链接的busybox像一块砖头一样扎实,扔到哪个板子上都能跑,少了很多“幽灵式”的依赖困扰。

有一个坑必须提醒:如果你的系统里所有用户程序都依赖glibc,而busybox用了musl静态编译,那么busybox调用外部程序时,glibc的程序依然会正常执行,这不冲突。但反过来,如果你用busybox的shell写脚本调用外部工具,外部工具的依赖项得自己保证。所以静态编译busybox本质上不解决整个系统的依赖问题,它只解决busybox自身的依赖问题。

2.3 要不要开启的隐藏配置项

打开make menuconfig之后,密密麻麻的配置项容易让人眼花缭乱。我梳理了几个实际项目中最容易出问题的选项,做一个说明。

  • CONFIG_FEATURE_TOP_SMP_CPU:top命令显示多核CPU使用率。默认不开启时,多核设备上的CPU使用率显示可能不准。
  • CONFIG_FEATURE_MOUNT_HELPERS:挂载时启用辅助工具。对NFS、FUSE这类文件系统挂载有影响,建议开启。
  • CONFIG_FEATURE_VI_REGEX_SEARCH:vi编辑器支持正则搜索。不开启的话,vi的搜索功能会弱很多。对在板子上临时改文件来说,这个功能还挺重要的。
  • CONFIG_ASH_JOB_CONTROL:ash shell的工作控制。关闭后,你在终端里按Ctrl+C可能会失灵,任务前后台切换也有问题。这个选项默认打开,建议保持。
  • CONFIG_FEATURE_EDITING_SAVEHISTORY:保存shell历史记录。在调试时很实用,能减少重复输入命令的烦恼。

还有个值得一提的坑:有些开发板厂商会给BusyBox打一堆自己的补丁,然后在配置里开一些奇奇怪怪的选项。交叉编译环境的CONFIG_CROSS_COMPILER_PREFIX要填对,不然后面链接阶段会莫名其妙报错。这个错误信息通常还不直观,我第一次遇到的时候查了半天,最后发现是工具链前缀写错了。

3. 交叉编译BusyBox:一套能用的配置和避坑记录

3.1 用Buildroot还是纯手动:两条路线怎么选

搭建BusyBox环境有两条常用路线。如果你还在项目初期,对工具链、内核、根文件系统的版本搭配没有把握,建议直接上Buildroot。它把工具链、内核、BusyBox、整个用户空间全打包管理,版本之间的兼容性被严格验证过,出问题概率低很多。

如果你是想深入理解BusyBox本身,或者你的工具链是芯片厂商定制的、无法用Buildroot统一管理,那就手动来。手动编译的流程也不复杂,基本步骤就是交叉编译工具链准备好,解压BusyBox源码,配置,编译,安装到一个临时目录,再把临时目录里的内容整合进根文件系统。

我个人的经验是:第一次玩BusyBox,不建议直接上Buildroot,最好是手动编译一遍。原因在于Buildroot帮你屏蔽了太多细节,你最后发现自己只是敲了个make,整个过程黑盒化非常严重。而手动编译一次,你能清晰感知到交叉编译器、sysroot、链接过程分别做了什么,这对接下来的根文件系统调试非常有帮助。

3.2 手动编译的完整流程

以下命令基于x86主机,目标平台是ARM Cortex-A7,使用通用的arm-linux-gnueabihf-工具链。实际操作时,工具链前缀换成你板子对应的名字即可。

# 解压源码 tar -xvf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 # 先生成默认配置,再在menuconfig中调整 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

进入menuconfig之后,我通常会检查这几处:

  • Settings -> Build static binary (no shared libs):选中,除非你有特殊理由用动态链接。
  • Settings -> Cross compiler prefix:填arm-linux-gnueabihf-。
  • Settings -> Destination path for 'make install':先空着或者填一个临时目录,之后手动拷贝。
  • Busybox Settings -> Busybox Library Tuning:按需微调。
  • Networking Utilities:勾选你需要的网络工具,比如udhcpc。
# 编译并安装到临时目录 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install CONFIG_PREFIX=/tmp/rootfs # 确认生成的关键文件 ls -l /tmp/rootfs/bin/busybox file /tmp/rootfs/bin/busybox

file命令的输出里应该能看到statically linked,说明静态编译生效了。与此同时,/tmp/rootfs下会有bin、sbin、usr等目录,里面全部是指向busybox的符号链接。

3.3 编译阶段最容易迷惑的三类报错

第一类是找不到头文件,比如stdio.h: No such file or directory。这是交叉编译器没有安装到系统路径,或者CONFIG_CROSS_COMPILER_PREFIX填错,导致编译器直接退化成宿主编译。第二类是链接阶段报cannot find -lc,说明你的工具链缺少libc库文件,安装完整工具链或者配置好sysroot路径就可以了。第三类是编译过程中突然出现与时间相关的警告,clock_gettime相关符号找不到,这多发生在musl工具链和某些老版本BusyBox搭配的时候,升级BusyBox版本或工具链版本基本能解决。

如果你用芯片厂商提供的工具链,可能还带有一个独立的sysroot路径。在Makefile或者环境变量里加上CFLAGS指向sysroot,具体方法各家不一样。我的习惯是写进Makefile的EXTRA_CFLAGS里,避免每次make clean后重配。

4. 用BusyBox手工搭建根文件系统:从目录骨架到启动成功

4.1 根文件系统的目录骨架到底怎么搭

有一句话要刻在脑子里:根文件系统不是为busybox准备的,是为整个Linux系统准备的。busybox只是其中的一部分。最终设备上完整的根文件系统至少要包含这些目录:

目录用途说明
/bin基本用户命令
/sbin系统管理命令
/usr/bin扩展的用户命令
/usr/sbin扩展的系统命令
/etc配置文件
/lib动态库和内核模块
/dev设备节点
/procproc虚拟文件系统挂载点
/syssysfs虚拟文件系统挂载点
/tmp临时文件
/var运行时数据
/rootroot用户的home目录
/home普通用户home目录
/mnt手动挂载点
/opt可选的附加软件目录

有一个细节我强烈建议新手注意:在做成镜像之前,先把这些目录在PC上建好,再逐一核对。很多根文件系统起不来的问题,不是配置错了,而是缺了某个目录。比如/var下缺了/var/log,syslogd会启动失败;/tmp没有可写权限,很多临时文件操作会报错。

ROOTFS=/tmp/rootfs mkdir -p $ROOTFS/{bin,sbin,etc,lib,usr,proc,sys,tmp,var,root,home,mnt,opt,dev} chmod 1777 $ROOTFS/tmp

/tmp设置1777权限属于强烈的推荐操作。不设的话,普通用户进程往/tmp写文件时会以Permission denied告终。

4.2 busybox --install与符号链接:手动建还是自动建

前面make install已经生成了一套符号链接。但如果你拿到的是一个现成的busybox二进制而没有那套链接,在设备上可以直接运行:

/bin/busybox --install -s

-s参数表示用符号链接方式安装,不消耗额外Flash空间。不带-s的话,busybox会把每个applet的硬链接创建出来,或者直接复制一份完整busybox二进制——这显然更浪费空间。

这个命令一般在最早期的手工搭阶段非常有用。你甚至可以在设备启动后,在rootfs挂载阶段调用它重建全套命令。但要注意,在chroot或系统尚未完全启动时,--install的目标目录必须存在,并且busybox自身要能被找到。有一种鸡生蛋蛋生鸡的场景:你想用ls查看/bin目录,但/bin/ls这个符号链接还没建——这时候直接/bin/busybox ls /bin就够了。

4.3 设备节点:最容易被“失踪”的基础设施

设备节点的处理,是老一代嵌入式工程师和新手之间一个很重要的认知分水岭。如果你习惯桌面Linux,你几乎感知不到设备节点的存在,因为udev自动帮你创建了。但在一个精简的嵌入式系统里,udev是一个比较重的组件,很多时候我们用mdev或者干脆手动创建关键节点。

至少这四个设备节点是内核启动后立即需要的:

mknod -m 600 $ROOTFS/dev/console c 5 1 mknod -m 666 $ROOTFS/dev/null c 1 3 mknod -m 666 $ROOTFS/dev/ttyS0 c 4 64 mknod -m 666 $ROOTFS/dev/zero c 1 5

/dev/console是内核打印信息的输出目标,权限设置成600是常见做法。/dev/null和/dev/zero的作用不用多说,很多命令和脚本依赖它们。/dev/ttyS0是串口控制台设备节点,如果你的板子串口对应的是ttyS0就用这个。

如果你还需要访问SD卡、USB设备,可以提前放好mmcblk0、sda等节点,也可以依赖后面的mdev自动创建。不过从调试的角度看,我建议在根文件系统里提前把SD卡节点建好,因为mdev需要内核事件触发,如果开机阶段还没有uevent,设备节点不会凭空出现。

4.4 内核启动参数的传递:从uImage到root=的完整链路

在引导加载U-Boot的阶段,内核启动参数一般由环境变量bootargs传递。典型的bootargs长这样:

console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw

console=ttyS0,115200告诉内核串口控制台用哪个设备、什么波特率。root=/dev/mmcblk0p2表示根文件系统在SD卡的第二分区。rootwait表示等待设备节点出现,如果省掉这个参数,早期启动阶段内核可能因为找不到根设备直接kernel panic。rw表示以读写方式挂载根文件系统,这在调试时非常有用。

我见过不少人卡在“内核启动到一半panic”的状态,最后发现是bootargs里的console参数和实际板子的串口编号对不上。比如高通的板子可能是ttyMSM0,全志的板子可能是ttyS0,这个不能一概而论,得看内核里的串口驱动注册名。如果内核本来就支持多个串口,可以先用earlyprintk把早期日志打到所有串口上,一次就能定位到底该用哪个别名。

5. 启动流程的完整解剖:inittab、rcS与控制台的那点事

5.1 BusyBox init和SysVinit的异同

当内核启动并挂载好根文件系统后,它会执行/sbin/init。在BusyBox体系里,init就是busybox的符号链接之一。它完成三件主要的事情:读取/etc/inittab、执行系统初始化脚本、为每个控制台启动登录shell。

BusyBox init的语义是SysVinit的一个子集,但有很多细节差异。最典型的是对/etc/inittab的解析方式。SysVinit里可以配置各种复杂的runlevel,BusyBox init简化了很多,重点支持以下几种action:

action触发时机
sysinit系统启动时最先执行
respawn进程结束后自动重启
askfirst终端上第一次按键后再启动进程
wait启动后等待进程结束
once启动一次,进程结束后不重启
ctrlaltdelCtrl+Alt+Delete触发
shutdown系统关机时执行

5.2 一份可以直接抄的inittab

我们项目里用的一份最简inittab长这样:

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

解释一下每一行的意思。第一行,启动时执行/etc/init.d/rcS脚本,这是整个用户空间初始化的起点。第二行,在console这个终端上等待用户按一次键,然后启动一个login shell。-前缀表示login shell,它会执行完profile相关的初始化再交给你交互提示符。第三行是init进程自己重启时的入口。第四行和第五行分别是热键重启和关机时卸载文件系统。

有个非常常见的坑:如果你在inittab的第一行写的是console::sysinit:/etc/init.d/rcS,而你的板子的确存在/dev/console节点,那么rcS执行的stdin/stdout会指向console,shell的输出确实能看到。但如果你不指定设备或者指定了不存在的设备,rcS里的所有输出会淹没在/var/log/messages里,你盯着串口终端可能什么都不显示,还以为系统卡死了。

“can't access tty; job control turned off”这句警告,为什么会出现?因为它试图为当前shell分配一个控制终端,却找不到可用的tty。于是shell只能以“无工作控制”模式运行,Ctrl+C和前后台任务切换都会失灵。在开发早期,如果板子只有一个串口,简单粗暴的做法就是inittab里把console那个条目配上并保证/dev/console存在,基本就不会有这个警告了。

5.3 rcS脚本里到底该放什么

/etc/init.d/rcS是整个用户空间初始化的入口。它通常是一个shell脚本,按顺序做几件事:挂载虚拟文件系统、配置主机名、启动mdev、初始化网络。

#!/bin/sh # 挂载基础虚拟文件系统 mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t tmpfs tmpfs /tmp # 设置主机名 echo "mytestboard" > /etc/hostname hostname -F /etc/hostname # 初始化设备管理 echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s # 配置网络 ifconfig lo 127.0.0.1 up ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up

注意mount -t tmpfs tmpfs /tmp,这个挂在前面搭建骨架时提过。tmpfs是基于内存的文件系统,数据重启即失,但对嵌入式系统来说,把临时文件放内存里效率高而且不伤Flash寿命,是常规操作。

这里有一个我要重点提醒的坑:mdev -s必须在/sys挂载成功之后再跑,否则-s扫描不到sysfs里的设备信息,设备节点就创建不全。有些板子内核里没有CONFIG_UEVENT_HELPER,那么写/proc/sys/kernel/hotplug这一步可能没有效果,这种时候mdev -s就是创建设备节点的主力,即使热插拔事件辅助失效,冷插拔设备的节点也能靠-s补上。

5.4 一个真实的启动debug案例

我记得有次调试一块板子,内核启动完到用户空间之后,串口上只有init started之后就彻底静默了。当时的直觉是rcS卡住了,于是我改了bootargs,在内核启动参数里加init=/bin/sh,直接绕过init跳过inittab进shell。进去之后手动挂载/proc、/sys,一步步排查,最终发现是rcS里有个mount NFS的语句,服务器没响应,mount命令卡在TCP重传上。

这个经验很直接:当用户空间启动卡住时,第一件事情不是猜,而是用init=/bin/sh绕过整个init体系,手工逐条执行脚本命令。这让你能在最小环境里快速定位是哪一步在阻塞。

6. 给BusyBox配上SSH:集成Dropbear的关键细节

6.1 为什么是Dropbear而不是OpenSSH

嵌入式系统上做远程登录,Dropbear和OpenSSH都可以,但我实际项目里几乎全部用Dropbear。原因有四条。第一,Dropbear的二进制体积大约是OpenSSH的三分之一左右。第二,它会话建立速度更快,握手时间在嵌入式设备上感知明显。第三,配置项精简,适合在固定环境里使用。第四,它的授权协议是MIT,对商用产品比较友好。

Dropbear包含两部分:dropbear是服务端,dropbearkey是密钥生成工具。在嵌入式系统里,一套最简单的部署策略是:在主机上生成host key,然后拷贝到开发板的/etc/dropbear目录,之后启动dropbear进程即可。

6.2 静态编译与libc依赖问题

如果你在主机的交叉编译环境里编译Dropbear,最稳妥的方式还是静态链接。注意,Dropbear默认是链接动态库的,配置时加--enable-static可以对可执行文件启用静态链接。此外,还需要把--disable-zlib打开,因为zlib的依赖对嵌入式设备来说也是一种负担,而且Dropbear在无zlib情况下能正常使用。

一个比较阴间的坑是:如果你的整个系统都是glibc环境,而Dropbear用了musl静态编译,那么在上层应用通过PAM或者nsswitch查询用户信息的时候,可能会遇到读取不了shadow密码的问题。Dropbear默认不依赖PAM,默认用/etc/passwd和/etc/shadow验证,所以纯静态编译一般问题不大。但假如你启用了--enable-pam,就不得不考虑和系统的PAM模块以及动态库之间的互动,这时候静态编译反而不一定是好选择。

6.3 密钥、首次启动与开机自启

生成host key的命令:

dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key

ed25519的密钥比RSA小,生成速度也快,适合嵌入式。生成一次以后,这个密钥文件要保留好。设备重启后如果密钥变了,客户端的known_hosts会提示host key不匹配,影响使用体验。

在rcS中集成启动逻辑:

# 在rcS脚本中添加 mkdir -p /etc/dropbear if [ ! -f /etc/dropbear/dropbear_ed25519_host_key ]; then /usr/sbin/dropbearkey -t ed25519 -f /etc/dropbear/dropbear_ed25519_host_key fi /usr/sbin/dropbear -R

-R参数表示如果没有指定的host key,就自动生成一个随机的临时密钥。这个选项对调试很方便,因为省去了首次启动前手工生成密钥的操作。但生产环境建议还是用持久化的host key,不然每次重启客户端都要确认指纹变化。

6.4 用scp传文件的小细节

Dropbear提供了dbclient,但它和OpenSSH的scp不直接兼容。跨端传文件我一般用两种方案。方案一:在PC端装一个pscp(PuTTY工具集),或者直接用scp -O命令,但可能需要在PC端安装dropbear兼容工具。方案二:在板子上跑scp命令,如果BusyBox自带的scpapplet和另一端Dropbear服务端配合有问题,就改用busybox nc手动传。

我的实测结果是:PC端用OpenSSH的scp往Dropbear服务端传,偶尔能通偶尔卡住,跟Dropbear版本和算法协商有关。最省事的其实是直接在PC上执行dropbear的dbclient来连板子,或者用sftp的替代工具。过程有点绕,但胜在稳定。

7. NFS挂载根文件系统:调试效率翻倍的工作笔记

7.1 为什么NFS根文件系统是嵌入式开发者的加速器

嵌入式开发中反复烧写Flash是个非常折磨人的过程。每改一个小脚本,都要重新制作镜像、烧录、重启,一次循环下来十分钟没了。NFS根文件系统的思路是:板子通过以太网,把PC上某个目录当作根文件系统挂载起来。这样你在PC上改代码、改配置,板子重启后直接看到最新状态,整个过程省掉了烧写的等待。

我自己的项目里,一旦板子和PC的以太网都能通,调试阶段几乎都会把NFS根文件系统作为默认启动方式。只有到了发布阶段,才切回Flash或者SD卡上的正式文件系统。

7.2 服务器端配置的完整步骤

主机PC上安装NFS服务:

sudo apt install nfs-kernel-server sudo mkdir -p /srv/nfs/rootfs # 将制作好的根文件系统内容拷贝到该目录 sudo cp -a /tmp/rootfs/* /srv/nfs/rootfs/

修改/etc/exports:

/srv/nfs/rootfs 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)

关键参数解释一下:

  • rw:板子可读写挂载。
  • sync:同步写,保证每次写操作都直接落到主机磁盘,避免掉电丢数据。如果调试时不追求极限性能,还是开着sync安心。
  • no_root_squash:允许板子上的root用户以root权限访问NFS文件,否则默认会映射为nobody用户,板子上很多操作会报Permission denied。
sudo exportfs -ra sudo systemctl restart nfs-kernel-server

exportfs -ra的作用是重新导出共享目录,让配置立即生效。

有一种很常见的报错是mount.nfs: Operation not permitted,多半是no_root_squash没写对,或者NFS版本协商有问题。建议在exports里强制指定版本,比如加上vers=3,可以避开一些较新的NFSv4权限模型导致的困惑。

7.3 板子端的内核命令行配置

板子的bootargs需要指定根文件系统走NFS:

console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs/rootfs,v3,tcp rw ip=192.168.1.50:192.168.1.100::255.255.255.0::eth0:off

其中root=/dev/nfs是固定写法,告诉内核根文件系统由NFS提供。nfsroot后面跟的是服务器IP、共享目录、NFS版本和传输协议。ip=后面的格式是:板子IP:服务器IP:网关:子网掩码::网卡名:off。

关于ip=参数,有一个坑是格式里的::eth0:off,最后的off表示不自动配置IP。如果你省掉整个ip=参数,内核会尝试用DHCP自动获取IP,如果局域网里没有DHCP服务器,就会卡在“Waiting for root device”上。

7.4 sync、VFS和掉电安全的那点事

NFS根文件系统调试最频繁出现的问题之一就是:板子运行着的某些程序一直往根文件系统写数据,而此时如果板子掉电,数据可能丢失。

VFS(虚拟文件系统层)位于具体文件系统之上,负责把应用的write请求缓存到Page Cache里,然后再由pdflush/ writeback线程择机刷入实际存储介质。这种缓冲机制对性能很有帮助,但也意味着掉电瞬间数据不一定落盘。你调用sync命令能强制刷盘,但你的应用如果不主动sync,内核的数据要等一段时间才会真正写下去。

在NFS调试时,如果你发现板子上跑了一个应用,改完配置文件后立即断电,再上电后配置还是旧的,那多半是Page Cache里的数据没来得及刷到主机。这不一定非得改程序,你可以调整内核参数:

echo 1 > /proc/sys/vm/dirty_background_ratio echo 2 > /proc/sys/vm/dirty_ratio echo 100 > /proc/sys/vm/dirty_writeback_centisecs

dirty_writeback_centisecs单位是百分之一秒,100表示每1秒唤醒一次writeback线程。这个值越小,数据驻留内存的时间越短,掉电丢失的风险越低,但写性能会下降。实际情况中,嵌入式设备掉电频繁的场景,建议把dirty_writeback_centisecs调到50(0.5秒)。

7.5 NFS根文件系统的权限问题

NFS挂载后,板子上root用户的uid是0,PC端共享目录里的文件如果属于另一个用户,权限映射就可能会出现奇怪的现象。

例如,PC端某个目录的所有者是uid 1000,板子上root用户访问这个目录,由于no_root_squash的存在,它能读能写,但创建的新文件owner会变成uid 1000对应的用户。这在实际调试中很吓人——明明我是root,创建出来的文件却属于别人。解决办法是在PC端把目录所有者也改成root,或者调整共享参数加入all_squash加上anonuid、anongid的映射。

8. 从实战收尾:版本差异、Android对比与断舍离经验

8.1 老版本BusyBox在新时代的兼容性问题

项目中如果你拿到的是开发板厂商提供的旧源码包,BusyBox版本还停留在1.22甚至更老,这时候最好看一下里面的补丁历史。老版本的BusyBox,比如1.22,在某些新内核对mount的解析支持有问题,或者对新的文件系统类型如f2fs支持不完整。

比较典型的是busybox v1.22.1在较新内核上,ps命令读取/proc信息时可能会显示不完整,某些内核版本下还会出现process information unavailable之类的输出。如果遇到这种情况,优先考虑把BusyBox单独升级到比较新的稳定版,比如1.36.x系列,而不是去修它。

旧版BusyBox另一个常见问题是对mdev的固件加载支持不完善。很多设备需要内核加载固件时,hotplug事件触发/sbin/mdev,但老版本mdev对firmware请求处理有bug,固件永远加载不进去,设备初始化失败。新版本在这方面修复很多。

8.2 Android的根文件系统为什么“不用”BusyBox了

顺着热词里提到Android的根文件系统,这里多说一句。Android早期确实使用过BusyBox的方案,但后来它构建了一个自己专门的操作系统用户空间层,底层启动文件系统由system/core里的init、toolbox按需构建。Android 10之后,toolbox逐渐被toybox取代。toybox的思路和BusyBox很像,但它的代码更专注于Android环境下的命令实现,且因为许可协议是BSD风格,对于Google的AOSP整合更友好。

对我们做嵌入式Linux的人有什么启发?我觉得最重要的是:BusyBox不是唯一的applet聚合工具,也不是永远的最佳选择。如果你的产品对许可合规有特别要求,或者你需要命令行为与Android生态高度一致,可以考虑toybox。但就通用嵌入式Linux开发调试来说,BusyBox依旧是最成熟、资料最多、社区经验最充沛的方案。

8.3 我踩过几次坑之后养成的使用习惯

构建根文件系统,我的固定流程是先跑make defconfig再手动开CONFIG_STATIC和几个必要的feature,而不是直接从别人的配置文件开始。原因是你不知道别人的配置里开了哪些私有patch,出了问题无从排查。从最保守配置逐步开,每开一项就做一次验证,有问题能准确定位到是哪个特性引起的。

编译阶段我的习惯是把CONFIG_PREFIX指到一个干净的临时目录,这一点已经强调过。注意顺序:make clean之后重新编译,一定要把目标目录清空,否则旧符号链接残留会导致奇怪的问题。我遇到过在目标目录里运行ls命令报“No such file or directory”,排查半天才发现是上次残留的符号链接指向了一个不存在的位置。

设备节点方面,我的习惯是手动创建console、null、zero、ttyS0四个节点,其余的交给mdev。但USB设备这种需要枚举才能确定的节点,光靠提前创建没用,还是要跑mdev -s或者依赖内核的uevent回调。很多人说“为什么我的U盘插上去没有/dev/sda”,十有八九是mdev的hotplug链路没有配置好。

还有一个小经验,在脚本调试阶段,把/etc/init.d/rcS里的命令逐条加上echo "=== running xxx ==="这类日志输出,能大大缩短定位时间。脚本一旦多了,没有进度提示的启动过程非常煎熬。这些日志最后在一两轮调试后可以删掉,但调试阶段它们价值巨大。

8.4 最后的建议:给新手的启动路径

如果你完全没接触过BusyBox,我给的建议路径是这样的:先在一台x86虚拟机上手动编译并运行一个基于BusyBox的最小系统,用qemu-system-x86_64跑起来。这一步没有硬件调试的干扰,能让你把Linux启动本质——内核加载、根文件系统挂载、init执行、shell交互——整条链路彻底理解清楚。

然后再上到ARM板子,这时候你已经知道哪些步骤属于通用Linux,哪些属于板子特有逻辑,问题定位会快非常多。一味拿开发板从头试错,很容易把“我网络没配好”和“我根文件系统有问题”混在一起,排查起来非常痛苦。

我个人在实际操作中最深的感受是:BusyBox这个工具,看起来只是一个“小型命令集合”,但只要深挖到它的init机制、applet实现、编译方式、和内核的配合关系,你几乎能把整个嵌入式Linux系统的启动链路完整串一遍。这只瑞士军刀,真正值钱的地方不是它有多锋利,而是你拿着它,能把一台“裸奔”的Linux设备从零“盘活”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询