尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

BusyBox原理与嵌入式Linux根文件系统手工构建实战

BusyBox原理与嵌入式Linux根文件系统手工构建实战 BusyBox这东西做嵌入式Linux的人绕不开。内核编译完、设备树改好结果板子上电卡在Kernel panic - not syncing: VFS: Unable to mount root fs十有八九是根文件系统没弄好。而根文件系统里最核心的那一个可执行文件就是BusyBox。我在这个坑里爬过很多次后来发现只要把BusyBox的原理和根文件系统的组织逻辑吃透整个嵌入式Linux的启动链路就顺了。这篇就把我从原理到实战的完整经验写出来给正在折腾板子和系统镜像的同学一个可以直接照做的参考。我先说结论BusyBox不是一个普通工具它是一个一个二进制文件干完所有基础命令的特殊程序。它既可以是init也可以是ls、sh、mount甚至可以是telnetd。这种设计在桌面Linux看起来匪夷所思但在嵌入式环境里恰恰是救命稻草。下面我从原理开始拆然后一步步用手工方式构建一份能启动的根文件系统再补上NFS调试和U盘烧录这些实战姿势。1. BusyBox到底是什么嵌入式Linux的生存底座1.1 一百条命令封装进一个可执行文件你可以把BusyBox理解成压缩包版的Linux命令集。普通Linux发行版里ls是一个程序cp是一个程序grep是另一个程序每个都有独立的可执行文件和动态库依赖。而BusyBox把这些常用命令的实现全部收进了一个程序里运行时根据被执行的名字来决定自己扮演谁。比如在嵌入式板子的/bin目录下你看到的不是一堆真实的可执行文件而是指向/bin/busybox的符号链接lrwxrwxrwx root root ls - /bin/busybox lrwxrwxrwx root root cp - /bin/busybox lrwxrwxrwx root root sh - /bin/busybox lrwxrwxrwx root root mount - /bin/busybox当你敲下ls内核执行的是/bin/busybox只是argv[0]是lsBusyBox拿到这个名字就去走ls的分支逻辑。这就是它体积小的核心原因所有命令共享一套运行库、一套内存管理、一套字符串处理代码不需要每个命令都链接一份libc。打开BusyBox的源码目录每个命令对应一个子目录比如coreutils里是ls、cp、mvshell里是ash、hushnetworking里是telnetd、wget、httpd。整套下来几百个应用编译出来的二进制在几MB到一MB多之间动态链接还能压到几百KB。1.2 为什么嵌入式Linux离不开它根文件系统不只是存文件的目录它必须包含Linux启动后用户态的第一个程序——init以及后续维护和调试用的全套基础命令。没有根文件系统内核起来后无事可做最后只能panic。但嵌入式设备存储空间有限Flash可能只有16MB、32MBRAM也只有128MB甚至更小塞下一套完整的coreutils加bash和一堆库显然不现实。BusyBox就是为了解决空间不够但基础功能必须齐这个矛盾。它一个程序同时兼任init、shell、常用工具于是根文件系统的核心部分可以压缩到很小。更重要的是它不挑运行环境静态编译完搬到哪个架构的板子上都能跑配合内核够稳、够省所以几乎所有嵌入式发行版、路由器固件、开发板出厂系统底层都是BusyBox。1.3 一个容易混淆的点BusyBox和glibc、buildroot的关系先说glibc。BusyBox是应用层程序glibc是C标准库。BusyBox自己不带printf、malloc这些函数用的是库里的实现。所以BusyBox有两种编译姿势动态链接到目标板的glibc或musl或者静态链接把库打进一个文件。后面实战会详细讲这里先有个概念。再说buildroot。BusyBox是buildroot里的一个软件包buildroot是一键生成完整嵌入式Linux系统的工具。你可以手工下载BusyBox源码、自己交叉编译、自己组装根文件系统这条路适合理解原理也可以配置buildroot让它自动把内核、BusyBox、libc、各种工具打包成镜像那条路适合工程交付。本文走手工路线因为只有亲手搭一遍遇到问题才知道根因在哪里。2. 解剖BusyBox一个二进制分身上百个命令的秘密2.1 从内核exec到init进程BusyBox的第一个身份内核启动的最后阶段会尝试挂载根文件系统然后执行/init。这个/init从哪里来有两种常见情况如果内核参数指定了init/linuxrc或者根文件系统根目录有/linuxrc内核会先去执行它。BusyBox安装时会在根目录生成一个linuxrc链接指向/bin/busybox。所以内核启动早期执行的init实际就是BusyBox。BusyBox发现argv[0]是linuxrc或init之后进入init逻辑读取/etc/inittab按里面的配置启动shell、挂载文件系统、运行rcS脚本。这个机制是整个根文件系统能跑起来的起点。init在嵌入式里不能做得像systemd那么庞大BusyBox的init只做几件事解析inittab、建立控制台、执行系统初始化脚本、按需重启或进入紧急shell。轻量、稳定、行为可预期这对嵌入式来说比功能多更重要。2.2 命令行分派一个main如何变成一百个命令写BusyBox源码的人把每个命令包装成一个applet所有applet按名字存在一张表里。每个applet有两个入口一个main函数一个help信息。BusyBox统一的入口main()拿到argv[0]后先在applet表里二分查找匹配的名字找到就跳转到对应实现找不到就打印applet not found。现代BusyBox为了兼容各种调用方式还支持busybox ls、busybox --install这类显式写法。前者的逻辑是当argv[0]是busybox时取argv[1]作为命令名后者则用于批量创建符号链接。这套设计让BusyBox既能当单命令用也能当命令集合用调试时非常方便。2.3 符号链接与--install的安装逻辑既然一个二进制能干所有事那系统的/bin/ls、/usr/bin/top等路径是怎么来的答案是符号链接。在执行完make install后BusyBox会把生成的busybox文件拷贝到目标目录然后根据配置创建一批/bin、/sbin下的软链接。你还可以在板子的shell里主动执行busybox --install -s /bin-s表示用软链接方式在指定目录生成所有配置过的applet链接。这么做的好处是命令的数量由配置决定不需要的applet可以不建链接进一步减少目录污染。2.4 静态链接还是动态链接体积与兼容性的权衡这是个老生常谈但又必须说的选择。动态链接的BusyBox只有几百KB但运行时要依赖目标板的动态库比如libc.so.6、libm.so.6、ld-linux-armhf.so.3如果拷贝不全会报error while loading shared libraries。静态链接的BusyBox把所有依赖打进一个文件体积多一到两MB但几乎可以在任何同架构环境下直接运行不必处理库依赖。我的建议是做调试用根文件系统时直接上静态链接省心做产品化系统体积敏感、想共享库才考虑动态链接。后者需要把工具链的sysroot里的库拷贝进rootfs一个都不能少后面会给出具体清单。3. 实战第一步交叉编译出一份可用的BusyBox3.1 版本选择和交叉工具链准备版本别追太新稳定优先。我这边用的是BusyBox 1.36.x功能完整社区活跃坑也少。直接去官网下载tar包或者在GitHub上拉tag都一样。交叉工具链你手头有哪个用哪个arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc都行。我用的是ARM 32位的arm-linux-gnueabihf工具链做例子。先把工具链路径加进环境变量export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- export PATH/opt/toolchains/arm-linux-gnueabihf/bin:$PATH然后解压BusyBox源码进入配置菜单tar xjf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 make menuconfig3.2 menuconfig里必须注意的配置项配置界面和内核的menuconfig同源操作方式一样。以下是我每次必检查的项Settings - Build static binary (no shared libs): 选上静态编译避免库依赖问题。Settings - Destination path for make install: 填_staging或者rootfs目录比如../rootfs后面安装命令直接往这里装。Settings - BusyBox installation prefix: 默认是./_install改成../rootfs也行。Settings - vi-style line editing commands: 看个人习惯我喜欢选上。Init utilities - init: 必须选上作为系统init进程。Shells - ash: 选上这是嵌入式最常用的shell。Coreutils - 常用命令: 按需选ls、cp、mv、cat这些默认就有不建议全选有些命令会增大体积。Linux System Utilities - mdev: 选上做设备节点热插拔。Networking utilities - telnetd: 如果调试需要远程登录选上。Networking utilities - httpd: 如果需要嵌入式web管理页面再选。Development - 不需要: 开发工具没必要进根文件系统裁剪掉。配置完保存退出然后编译make -j$(nproc) make install如果配置了安装到../rootfs这一步结束你会在rootfs目录下看到bin/、sbin/、usr/三个目录里铺满了符号链接以及一个真正的bin/busybox文件。3.3 安装目录的约定和后续操作make install不会帮你创建整个根文件系统的目录结构它只铺命令。所以装完以后你自己还需要补上etc、dev、proc、sys、tmp、var、root这些目录。我习惯的目录规划是mkdir -p rootfs/{bin,sbin,usr,lib,etc/init.d,dev,proc,sys,tmp,var,mnt,root}顺便设置权限tmp和var要可写root的权限设为700比较稳。检查一下安装结果file rootfs/bin/busybox # 输出应该是ELF 32-bit LSB executable, ARM, statically linked看到statically linked说明静态编译成功这个文件拷到同架构板子上就能跑。3.4 第一步实测验证命令真的能跑在PC上装qemu-user的话可以直接用qemu验证这份BusyBox能不能执行qemu-arm rootfs/bin/busybox qemu-arm rootfs/bin/busybox ls如果打印出BusyBox的信息和使用帮助说明程序本身是好的。这一步提前验证能省不少板子调试时间。没装qemu的也没关系进到后面的根文件系统启动环节在板子上验证一样。4. 手搓根文件系统目录、设备节点、init与启动脚本4.1 标准目录骨架哪些不能少哪些可以精简根文件系统的目录结构是有约定的内核和用户态都会按约定找路径。以下目录是必须的/bin、/sbin、/usr/bin、/usr/sbin: 存放命令和符号链接BusyBox安装时会建好。/etc: 配置文件所在地init、fstab、inittab都在这里。/dev: 设备节点目录最少要有console和null否则内核输出没地方去。/proc、/sys: 内核虚拟文件系统的挂载点很多工具依赖。/tmp、/var: 运行时临时文件脚本和进程要写。/lib: 动态库目录即使静态链接也建议保留方便以后放库。/root: root用户的home可选但建议留。/mnt: 手动挂载外部存储的挂载点。/init或/linuxrc: 内核启动后第一个执行的程序入口。没有硬性要求非要有/home或/opt嵌入式精简系统可以省。4.2 设备节点的两种处理方式内核启动后控制台信息输出依赖/dev/console如果这个节点不存在init启动和shell交互都会出问题。手工创建设备节点用mknodcd rootfs sudo mknod -m 622 dev/console c 5 1 sudo mknod -m 666 dev/null c 1 3设备号不是随便填的console的主设备号是5、次设备号1null的主设备号1、次设备号3这些是内核约定俗成的标准。早期手工构建系统这一步必须做否则系统起不来。在真实产品里更常见的方案是让内核通过devtmpfs自动挂载设备或者在系统启动后用mdev来动态维护设备节点。使用devtmpfs的做法很简单编译内核时选上CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT内核会自动挂载/dev并生成节点。这时候只要在rcS脚本里加一句挂载devtmpfs到/dev或者让内核自动挂载就不用手工建一堆节点了。但console和null我还是建议保留因为内核在devtmpfs挂载前就要打印日志。4.3/etc/inittab与init进程的启动流程BusyBox的init进程读取的配置文件是/etc/inittab。这份文件的语法比System V的inittab简单很多每一行是一个条目格式是id:runlevel:action:process嵌入式环境一般把所有字段简写runlevel位往往是空。参考写法::sysinit:/etc/init.d/rcS ::askfirst:-/bin/sh ::restart:/sbin/init ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r解释一下sysinit: 内核态init完成后先执行/etc/init.d/rcS做系统初始化。askfirst: 在console上询问并启动交互shell按回车才进入避免串口日志被输入干扰。restart: 当init收到重启信号时执行。shutdown: 关机时卸载所有挂载的文件系统。ctrlaltdel: 处理组合键重启。/etc/init.d/rcS是一个普通shell脚本里面写设备挂载、网络配置、mdev启动等逻辑。一个基础的rcS长这样#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev mkdir -p /dev/pts mount -t devpts none /dev/pts echo /sbin/mdev /proc/sys/kernel/hotplug mdev -s ifconfig eth0 192.168.1.50 netmask 255.255.255.0 up route add default gw 192.168.1.1这里有个细节config/net的初始化如果用的是ifconfig依赖BusyBox编译时选上ifconfig和route工具通常默认都有。挂载devpts是为了支持pty设备sshd和串口登录都依赖。rcS脚本一定要有执行权限否则init执行时资源受限chmod x rootfs/etc/init.d/rcS4.4 把挂载和网络初始化串起来的几个坑/etc/fstab其实不是必须的但有了它可以让mount -a自动挂载脚本里省事。参考写法proc /proc proc defaults 0 0 sysfs /sys sysfs defaults 0 0 devpts /dev/pts devpts defaults 0 0在rcS里执行mount -a就会把所有条目挂上。建议fstab里别写none作为设备名虽然能挂但看起来不规范阅读代码的人会疑惑。网络这块如果你用DHCP命令不同取决于BusyBox编译了udhcpc还是dhcpcdudhcpc -i eth0如果板子一直拿不到IP检查有线物理链路时间ifconfig eth0 up以后再跑udhcpc有些驱动需要一点时间建链。rcS里可以加个sleep 1。4.5 动态库依赖的拷贝与strip瘦身如果你当时选了动态链接一定会踩找不到libc.so.6的坑。排查方法是在PC上用交叉工具链的readelf查看BusyBox依赖了哪些库arm-linux-gnueabihf-readelf -d rootfs/bin/busybox输出里会列出NEEDED项一般包含libc.so.6和ld-linux-armhf.so.3。然后去交叉工具链的sysroot目录下找到这些库拷贝到rootfs对应的/lib下。注意保留符号链接版本比如libm.so.6 - libm-2.31.so。少了链动态链接器找不全。为了减小体积可以给拷进来的库做striparm-linux-gnueabihf-strip rootfs/lib/*.so但注意别strip BusyBox本身除非确定不再需要调试信息。strip之后有些调试会不好做我一般是最后打包前再strip一次。5. 让板子真正启动起来内核参数、NFS根文件系统与U盘烧录5.1 U-Boot的bootargs怎么给定内核起来以后挂哪个根文件系统、用什么方式挂全看内核命令行的root参数。U-Boot里通过环境变量bootargs传递常见组合setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rw rootfstypeext4 init/sbin/init这里的consolettyS0,115200和你的串口驱动对应很多SoC用的是ttyS0或ttymxc0查SoC手册。root指定根设备rootfstype可以省略由内核自动探测rw表示根文件系统可写嵌入式调试时建议给上不然后面改配置不方便。init/sbin/init手动指定init路径防止内核找不到。手工构建的rootfs里/sbin/init也存在是BusyBox安装时生成的软链接。5.2 NFS调试每次改完不用反复烧录的姿势开发和调试阶段NFS挂载根文件系统是最省心的方案。启动时内核通过网络从开发机拉取根文件系统改板子上的文件只需要改开发机的目录重开shell或重启进程即可不用重新烧录Flash也不用拔SD卡。开发机侧需要安装nfs-kernel-server然后在/etc/exports里声明rootfs目录允许板子访问/opt/rootfs 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)配置好以后重启NFS服务然后在内核启动参数里指定setenv bootargs consolettyS0,115200 root/dev/nfs nfsroot192.168.1.100:/opt/rootfs,v3,tcp rw ip192.168.1.50:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off说明一下root/dev/nfs告诉内核用NFS作为根文件系统nfsroot是开发机IP和路径ip参数依次是板子IP、开发机IP、网关、掩码。下面的冒号字段可以省略部分内容但建议写全避免U-Boot乱填。这种方法依赖内核编译时选上网络文件系统支持CONFIG_ROOT_NFS等相关选项如果内核对NFS支持不全会在启动时直接报VFS: Unable to mount root fs via NFS。所以编译内核时记得开NFS client和root over NFS选项。NFS挂载还有个隐蔽问题板子和开发机之间的网线如果没插好、IP不在同一网段内核会卡在等NFS server的步骤上。排查时先在U-Boot里ping一下开发机IP能通再谈挂载。5.3 U盘/SD卡方案与块设备参数调试完成后最终要落地到Flash、SD卡或U盘。SD卡一般分区第一个分区放内核和设备树第二个分区放rootfs。U-Boot引导时把zImage和设备树拷到内存再指定rootfs在第二个分区setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rw rootwaitrootwait很重要因为SD卡初始化比内核尝试挂载根文件系统慢加了这个参数内核会一直等设备就绪。U盘启动则改root设备为/dev/sda1。这类块设备的驱动在内核里别漏了比如CONFIG_MMC、CONFIG_USB_STORAGE。把rootfs拷贝到分区后要注意文件属主和权限直接sudo tar解压到分区时尤其容易漏了特殊权限位。我习惯打包前先cd rootfs tar czf ../rootfs.tar.gz *在目标板上挂载SD第二个分区然后解包并顺手检查init显式权限。5.4 启动失败的典型排查链路做嵌入式调启动最忌讳瞎试。我稳定复现的排查顺序是串口没输出先查U-Boot启动参数console是否匹配波特率对不对串口线是否交叉。内核起但挂不上根文件系统报VFS: Unable to mount root fs先看是找不到设备还是格式不对。找不到设备说明驱动或root有问题格式不对说明文件系统类型或内核配置缺了件。rootfs挂上但init起不来看日志是否出现/sbin/init: No such file or directory这个提示很坑它可能真的是文件不存在也可能是解释器不存在——如果BusyBox是动态链接的库里少文件也会报No such file or directory。init起来了但shell不响应多半是/dev/console没有或者inittab里askfirst配置不对。一步步排除先保证到哪一步、什么现象再动手改。6. 进阶玩法SSH、热插拔与从手工到buildroot6.1 dropbear给这个迷你系统加上SSH能力BusyBox自带telnetd调试内网足够但为了安全或者做产品还是建议上dropbear一个为嵌入式环境设计的轻量SSH服务端。思路很简单先交叉编译zlib和dropbear然后把可执行文件、库、密钥生成脚本放进rootfs里在rcS里启动dropbear或者从inetd拉起。dropbear需要配置host key首次启动时它会自动生成但要确保rootfs的/etc/dropbear目录可写。ssh登录默认是root需要在rootfs里创建/etc/passwd、/etc/shadow给root设个密码或禁密码。加dropbear会让rootfs体积增加几百KB到几MB比起完整openssh小太多了。如果做IoT设备dropbear基本是标配。6.2 mdev用热插拔机制管理设备节点devtmpfs能解决大部分节点生成问题但遇到USB设备、SD卡插入这类热插拔事件需要mdev来协同。BusyBox里的mdev就是简化版udev根据内核发送的hotplug事件和/etc/mdev.conf规则来创建设备节点、设置权限、执行脚本。rcS里已经写了echo /sbin/mdev /proc/sys/kernel/hotplug mdev -smdev -s会在启动阶段扫描当前系统的sysfs把已有设备节点补齐。echo /sbin/mdev hotplug设置内核把热插拔事件交给mdev处理。这样一来插上U盘会自动创建/dev/sda等节点脚本里再配合自动挂载体验就完整了。6.3 从手工搭建到buildroot值得不值得手工搭这套流程的价值在于理解原理但工程化交付时我会直接用buildroot。buildroot做的就是把你刚才手工做的所有事情自动化下载内核、BusyBox、第三方库统一交叉编译一次生成rootfs、内核镜像、bootloader镜像。用buildroot的好处有三个可重复性同一个配置在不同机器上产出一致镜像、包管理新增软件包只需要make menuconfig勾选、依赖完整省去手工拷贝库的烦恼。坏处也有就是出了交叉编译问题时你要懂它内部那一层层的makefile和模板不懂原理的人出了问题根本无从排起。所以我的路线是先手工搭一遍再用buildroot做工程化。两条路都走过了你对rootfs的理解才算真的到位。6.4 能跑只是开始裁剪、安全和体积控制当你的系统能正常启动、能ssh进去、能跑业务程序之后真正的产品化才刚刚开始。体积方面可以用du和busybox du查哪些目录占空间去掉文档、调试符号、不必要的busybox applet安全方面建议关闭telnetd仅保留dropbear对root密码做过期策略web服务开启认证稳定性方面把rcS脚本加上set -e任何一步失败直接进入紧急shell防止半初始化状态。如果你想在这个基础上加图形界面AWTK这类轻量GUI框架也可以直接用BusyBox提供底层shell和文件系统能力上层GUI只是多放几个动态库和可执行文件的事情。这也是嵌入式Linux有意思的地方底层扎实了上面想搭什么都是顺势而为。在我自己的项目中BusyBox这根瑞士军刀从头到尾一直握着从最开始的串口调试到最终产品发布它就这么安静地躺在rootfs里承担着init、shell、文件操作、网络工具这些最不起眼却最关键的工作。每次看到它在只有几百KB的Flash上跑得飞快我就觉得嵌入式Linux的浪漫从来不在于堆功能而在于用最合适的工具解决最实际的问题。这份手工搭建rootfs的经验后续我做任何板卡都能复用也希望这篇文章能让你少走几个我当年走过的弯路。
返回列表