资讯动态

完善 BusyBox 根文件系统:No init found 与 NFS 调试

发布时间:2026/10/20 8:54:09 来源:尧图企业网站定制
这期继续写 Linux 系统移植系列里的一个硬骨头——完善 BusyBox 构建的根文件系统开发环境是 Ubuntu20.04。很多人做到用 BusyBox 生成 rootfs这一步就停住了因为把_install目录里的文件原样打包烧到板子上之后结果往往是串口上出现一行刺眼的Kernel panic - not syncing: No init found随后是无休止的登录不上、网络不通、modprobe 找不到模块。这一期不打算从零讲怎么裁剪内核、怎么编译 BusyBox而是聚焦在完善两个字上当你已经有一份基础 rootfs 之后怎样把它从能解压变成能用、能开发、能交付这就是本期全部内容的价值所在。1. 基础 rootfs 离能跑还差多远目录骨架与编译路线的选择1.1 从_install目录到完整根目录先搭骨架BusyBox 编译完成后默认的make install只会生成bin、sbin、usr三个目录里面是各种命令的符号链接和busybox本体。如果你直接把这三个目录打包当 rootfs 用板子大概率起不来因为内核在挂载 rootfs 之后要找到一个名为/init或/sbin/init的可执行文件同时还要访问/dev/console这样的设备节点。基础 rootfs 里缺的就是这些系统运行所依赖的基础设施。所以第一步就是把这些目录手工补上。以我平时整理的 rootfs 为准你的板子至少要准备好下面这些目录缺哪个后面就会在某一步踩雷/dev设备节点所在目录。没有它内核和用户空间的交互就没有入口/dev/console、/dev/null这类基本节点必须在启动早期可用。/proc、/sys内核虚拟文件系统挂载点。ps、top、free这些命令都依赖/proc设备模型信息依赖/sys。/etc配置文件目录inittab、rcS、profile、passwd全在这里。/lib动态链接库所在目录尤其当你选择动态编译 BusyBox 时这是重中之重。/tmp临时文件目录很多程序运行时会往这里写数据。/var运行期数据目录比如var/log、var/run如果程序想写锁文件找不到目录就报错。/rootroot 用户的家目录登录 shell 启动时会切到这里。创建目录本身没什么技术含量一条命令就能搞定。真正容易被忽略的是权限/tmp建议给 1777/var/run给 755 或者 777 取决于你的脚本目录权限不对会导致某些服务启动时报Permission denied。这里还要解释一个关键点为什么不能偷懒不做/dev下的节点。很多人以为mdev -s能自动生成所有设备节点于是光建一个空目录就完事了结果启动早期还没执行到 rcS 脚本之前内核控制台输出都找不到设备节点。稳妥的做法是先用mknod手工创建/dev/console和/dev/null两个节点后续其他设备再交给mdev动态管理。mknod的用法后面会提现在先记住这两个节点是整个系统最基础的生命线。1.2 静态编译还是动态编译完善工作量完全不同BusyBox 的编译方式直接决定了后期完善的复杂度。make menuconfig里Settings - Build BusyBox as a static binary勾上就是静态编译不勾就是动态编译。两种方式各有利弊我用一张表直观说明对比项静态编译动态编译是否依赖 glibc不依赖自包含依赖 libc.so、ld-linux 等运行库rootfs 体积较大单个 busybox 可能几 MB较小busybox 几百 KB库文件可裁剪部署步骤简单拷过去就能跑需要搬运运行库并且路径要正确后期扩展加一个第三方程序就得重新编译直接丢编译好的 .so 和可执行文件即可调试难度相对麻烦不好 gdb 动态库方便能看到库函数维度的问题作为实际产品我个人会优先走动态编译。体积小是显性收益隐性收益是扩展性嵌入式系统后期难免要加openssl、c-ares、业务后台程序这些第三方库动态链接方式直接拷贝文件和 so 就能用静态方式就得把每个程序都重新编一遍。这一期的完善流程也围绕动态编译展开。提醒一句如果你之前编译 BusyBox 时用的是静态方式本期讲的库文件搬运步骤可以直接跳过但inittab、rcS、网络配置这些内容一个都不会少。两种路线都躲不开系统编排。2. No init found背后的真实原因动态链接器与 init 查找顺序2.1 内核找 init 的逻辑以及报错时到底发生了什么开发中最常见的启动失败就是Kernel panic - not syncing: No init found。很多人第一反应是我明明把 BusyBox 放 rootfs 里了怎么会找不到 init问题往往不出在文件是否在而出在 init 能不能被执行。内核解压完、挂载完 rootfs 之后会按照固定顺序尝试执行 init先找/sbin/init再找/etc/init然后是/bin/init最后是/bin/sh。只要有一个能成功执行init 阶段就算启动。BusyBox 的安装过程会用符号链接把/sbin/init指到busybox可执行文件所以路径本身通常没问题。但动态编译的 BusyBox 是靠动态链接器启动的。它真正运行前需要内核先加载/lib/ld-linux-armhf.so.3具体文件名以你 readelf 看到的为准再由这个动态链接器把libc.so.6等依赖库加载进来。如果你的 rootfs 里压根没有这些 so 文件内核执行/sbin/init时会收到一个无法读取可执行文件的错误它以为没有 init于是继续尝试下一个路径全失败后就只能 panic。所以看到No init found时先别急着怀疑文件缺失。它大概率是三种情况之一rootfs 里真的没有/sbin/initBusyBox 是动态编译但/lib下没有动态链接器动态链接器路径和 BusyBox 编译时记录的路径不一致。把排查顺序理清楚比在那里反复烧固件有效率得多。2.2 在 Ubuntu20.04 上确认 BusyBox 依赖的准确方法要确认问题属于哪一种不需要把板子搬出来直接在 Ubuntu20.04 宿主机上分析这个二进制文件就能知道答案。文件本身就携带了足够多的信息。先看文件格式和链接方式file busybox如果是静态编译输出里会有statically linked如果是动态编译会显示dynamically linked并且能看到目标架构比如ARM、EABI这些标识。这一行信息能帮你立刻排除一半的问题。接着看它依赖哪些动态库readelf -d busybox | grep NEEDED输出会列出类似libc.so.6、libm.so.6这样的条目这就是你必须在 rootfs 的/lib里准备好的文件。还要看它要求的动态链接器路径readelf -l busybox | grep INTERP动态编译的 ARM 程序会在 INTERP 段里写死一个路径常见的是/lib/ld-linux-armhf.so.3。内核加载这个程序时会先到这个路径去找解释器。你稍后向 rootfs 拷贝库文件时必须保证这个路径真实存在一个字节都不能改否则程序就是起不来。这里有个新手很容易踩的坑不要在 x86 的 Ubuntu20.04 宿主机上直接对 ARM 版 BusyBox 执行ldd因为本机动态链接器不识别 ARM 格式大概率会给你报一个无意义的结果。用readelf才是干净可靠的判断方式而且不依赖目标硬件。2.3 库文件从哪来交叉工具链 sysroot 的正确搬运姿势确认了依赖项接下来就是找对文件来源。很多人图省事直接从 Ubuntu20.04 宿主机的/lib/x86_64-linux-gnu目录里拷贝同名 so 文件到 rootfs这样百分之百起不来因为宿主机的是 x86_64 架构的库ARM 芯片根本执行不了。正确来源是交叉编译工具链自带的 sysroot 目录。以 Ubuntu20.04 上最常用的gcc-arm-linux-gnueabihf工具链为例安装后它的运行库通常位于/usr/arm-linux-gnueabihf/lib或/usr/arm-linux-gnueabihf/libc/lib。其他自编译的工具链一般也遵循类似结构目录名里带arm-linux-gnueabihf或目标三元组的那一层就是 sysroot。我的做法是先把整个库目录整体复制过来再做裁剪因为逐个挑选太容易漏文件ROOTFS/path/to/your/rootfs sudo cp -a /usr/arm-linux-gnueabihf/lib/. $ROOTFS/lib/复制完别急着打包。先用find把静态库*.a、开发用的头文件等没用的东西清掉再用交叉工具链自带的strip工具压缩一下体积sudo find $ROOTFS/lib -name *.so* -exec arm-linux-gnueabihf-strip --strip-unneeded {} \;最后回到 2.2 那一步检查关键依赖库是否都在特别是ld-linux-armhf.so.3这个动态链接器。只要它在No init found这类问题就能从根上被消灭掉。3. 串口登录从无到有inittab、rcS 与用户系统的搭建顺序3.1 inittab 逐行拆解一行配置决定一张串口终端库文件就位、init 能跑起来之后下一个标志性节点是串口出现登录提示符。这个行为完全由/etc/inittab控制。BusyBox 的 inittab 格式比传统 SysV init 简化了不少但核心字段仍然是四段id:runlevels:action:process其中 runlevels 段在 BusyBox 里可以直接留空。我用的开发板配置一般是这样的::sysinit:/etc/init.d/rcS console::respawn:/sbin/getty -L console 115200 vt100 ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r ::restart:/sbin/init逐行解释一下。::sysinit:/etc/init.d/rcS表示内核 init 最先运行系统初始化脚本这是整个系统从内核态切到用户态的枢纽。console::respawn:/sbin/getty -L console 115200 vt100表示在名为 console 的终端设备上启动一个登录进程如果用户退出respawn会让它自动重新拉起这个终端和你内核启动参数里的consolettyS0,115200必须对应上否则串口上什么提示都看不到。ctrlaltdel和shutdown两行是行为约定前者管三键重启后者管关机前的卸载文件系统。如果你希望在启动后不做密码验证、直接得到一个 root shell可以把respawn那一行改成console::askfirst:-/bin/shaskfirst的意思是终端空闲时显示 Please press Enter to activate this console按下回车就直接给 shell而不问用户名密码。开发调试阶段我经常这么干省时省心交付和正式环境再换回 getty 登录。这里有一个非常容易被绕晕的点inittab 的 id 段填的是设备名不是/dev/下的完整路径。如果你内核里用的是consolettyS0这里通常填ttyS0::respawn:...如果用的是consolettyAMA0就填ttyAMA0::respawn:...。填错后登录进程会一直 fork 失败现象就是系统活着但串口完全无法交互。3.2 rcS 脚本系统初始化编排的正确顺序inittab 只是入口真正的初始化动作都写在/etc/init.d/rcS里。这个脚本的执行顺序决定了后续很多怪问题的根因。我整理了一份最小可用的 rcS 示例#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mkdir -p /dev/pts /tmp /var/run mount -t devpts none /dev/pts mdev -s hostname myboard ifconfig lo 127.0.0.1 up ifconfig eth0 192.168.1.20 netmask 255.255.255.0 route add default gw 192.168.1.1顺序不是随便写的。mdev -s必须放在sysfs挂载之后因为它要从 sysfs 里扫描内核注册过的设备再据此在/dev下生成节点如果顺序颠倒mdev -s扫不到任何东西设备节点一个都建不出来。/proc、/sys又必须在ps、top这类工具之前挂好否则这些命令的输会场场都是空的。写 rcS 时有个细节极容易翻车脚本写完忘加可执行权限。chmod x /etc/init.d/rcS这步被跳过之后系统会卡在 sysinit 阶段但没有任何报错提示你只会觉得启动好像卡了很久。别问我怎么知道的建议你把权限检查写进每次收尾清单里。如果项目里习惯用/etc/fstab统一管理挂载也可以在 rcS 里只写一行mount -a由 fstab 描述挂载点和类型。这种方式更适合文件系统结构复杂的产品但初期调试用显式的 mount 命令更直观哪一行失败一眼就能定位。3.3 profile、passwd、shadow登录之后的世界登录能通之后shell 环境好不好用取决于/etc/profile和用户数据库文件。/etc/profile只需要解决几个问题PATH 够不够全、提示符观感、家目录对不对。嵌入式系统我一般这么写#!/bin/sh export PATH/sbin:/usr/sbin:/bin:/usr/bin export HOME/root export PS1[\u\h:\w]# 这里不需要像桌面 Linux 那样堆一堆系统级配置嵌入式环境越简单越不容易出问题。想设置系统语言或用其他时区再按需往 profile 里追加export即可。用户数据库包括/etc/passwd、/etc/shadow、/etc/group三个文件。开发板上的最小化配置可以这样写root:x:0:0:root:/root:/bin/sh在/etc/passwd里密码字段填x表示真正的密码放在 shadow 文件里。接着创建对应的 shadow 条目并设置密码比较省事的办法是把自己板子上的 root 密码 hash 直接写进去或者登录系统后用passwd命令现场设置。/etc/group至少要有 root 组root:x:0:这段配置的顺序有个讲究先有/etc/passwd再设置/etc/shadow最后写/etc/group。若顺序颠倒你可能会遇到 login 已经验证通过但 shell 却进不去的怪现象因为/bin/sh启动时要读用户的家目录和属主信息信息不完整就直接退出。4. 网络与域名解析在嵌入式 rootfs 中的配置细节4.1 网络初始化静态地址还是 DHCP板子跑起来之后第一件正经事通常是联网。系统能不能联网rootfs 侧要做的事情并不复杂但都是纯细节。开发阶段强烈建议把网络配置直接写进 rcS。静态环境可以用最简单的三条命令ifconfig eth0 192.168.1.20 netmask 255.255.255.0 route add default gw 192.168.1.1 echo nameserver 192.168.1.1 /etc/resolv.conf如果需要动态获取地址用 BusyBox 自带的udhcpc/sbin/udhcpc -i eth0这里有一个几乎所有人都会踩的坑udhcpc依赖外部脚本/usr/share/udhcpc/default.script。如果你编译/配置 BusyBox 之后没有从源码的examples/udhcp/simple.script拷贝一份过去执行 udhcpc 会直接报udhcpc: no /usr/share/udhcpc/default.script然后退出。这个问题和内核、驱动都没有任何关系纯粹是用户态补丁缺失。4.2 DNS 解析失败与 resolv.conf为什么能 ping 通 IP 却通不了域名嵌入式 rootfs 里最常见的网络现象就是ping 192.168.1.1通ping www.baidu.com报bad address或unknown host。这不是网卡坏了也不是内核配置丢了99% 的情况下是/etc/resolv.conf缺失或为空。DNS 解析是一个典型的用户态行为libc里的解析函数会先读取/etc/resolv.conf获取 nameserver 地址再去发起 DNS 查询。文件不存在时glibc 会尝试读取本机配置的默认值如果默认值指向一个不可达地址解析必然失败。最小可用配置只需要一行nameserver 8.8.8.8但要注意公司内网或实验环境里不一定允许向公网 DNS 出查询更稳妥的做法是把 nameserver 指向自己的网关或者局域网 DNS 服务器。实际调试时我先用nslookup或ping一个局域网内已知域名来测试如果还不行就抓包看 UDP 53 端口有没有响应这样能快速把问题定位在 DNS 配置还是网络路径上。4.3 profile 中容易漏掉的路由与 DNS 相关文件和桌面系统不同嵌入式 rootfs 里/etc/hosts、/etc/nsswitch.conf这两个文件通常不会自动生成但它们对 glibc 的解析流程是有影响的。/etc/hosts至少要写上一行主机名和回环地址127.0.0.1 localhost myboard如果没有这条记录很多程序解析本机主机名时会尝试发起一次 DNS 查询白白增加延迟有些极端情况下还会导致服务启动变慢。/etc/nsswitch.conf在最小 rootfs 里可以不创建glibc 有默认行为但如果你发现某些解析行为诡异可以补一个最简文件hosts: files dns这表示先查本地/etc/hosts查不到再走 DNS。这个顺序也是我实际项目里一直沿用的能让本机主机名解析完全走本地文件彻底绕开网络因素。5. 从能启动到可开发内核模块、NFS 调试与镜像打包5.1 内核模块该放哪目录结构与 depmod 依赖很多教程做内核模块测试时都是把.ko文件直接丢到 rootfs 的任意目录然后insmod xxx.ko。这么做在验证阶段没问题但一旦模块多了依赖关系根本理不清。正确的做法是把模块放进内核约定的目录结构里让modprobe帮你处理一切。目录结构是/lib/modules/$(uname -r)/ ├── kernel/ │ ├── drivers/ │ └── ... ├── modules.dep └── modules.alias$(uname -r)就是内核版本号字符串比如5.4.0-arm。如果你在 Ubuntu20.04 宿主机上从内核源码树安装模块最简单的方式是make ARCHarm INSTALL_MOD_PATH/path/to/rootfs modules_install内核构建系统会自动在 rootfs 的/lib/modules/版本下生成对应目录结构并把modules.dep、modules.alias一并生成好。这样你在板子上执行modprobe xxx时工具会读取modules.dep推断依赖、按序加载比insmod一个文件一个文件手工指定省心太多。5.2 开发期网络文件系统调试告别反复烧写每次都把 rootfs 打包烧写到 SD 卡或 flash改一行脚本就要重新烧一遍这种效率在开发后期完全不能接受。我一般会用网络文件系统模式让板子直接挂载宿主机 Ubuntu20.04 上的某个目录作为 rootfs改完文件立即生效。宿主机侧先安装并配置 NFS 服务sudo apt install nfs-kernel-server然后在/etc/exports里加一行/home/developer/rootfs 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)重启 NFS 服务后给 U-Boot 设置如下内核参数root/dev/nfs nfsroot192.168.1.100:/home/developer/rootfs,prototcp,rw ipdhcp其中192.168.1.100是宿主机的 IP。板子启动时内核会在早期挂载目录阶段直接把这个 NFS 目录当作根文件系统使用。注意这种方式对内核配置有要求文件系统相关选项里必须开启网络文件系统支持否则内核会报Root-NFS: no NFS support。开发期用 NFS 还有一个附带好处你可以在宿主机的 rootfs 目录里直接编辑脚本然后通过串口在板子上重新执行双屏效率非常高。唯一需要适应的是启动速度会稍微慢一些毕竟走的是网络而不是本地 flash。5.3 从调试态回到交付态打一个 ext4 镜像NFS 模式跑通功能之后最终还是要做成可以离线烧写的镜像。以嵌入式里最常见的 ext4 镜像为例制作流程在 Ubuntu20.04 上一气呵成dd if/dev/zero ofrootfs.ext4 bs1M count256 mkfs.ext4 -F rootfs.ext4 mkdir -p /mnt/tmp sudo mount -o loop rootfs.ext4 /mnt/tmp sudo cp -a /path/to/rootfs/* /mnt/tmp/ sudo umount /mnt/tmpcount256表示 256MB实际大小要根据你的 rootfs 内容量级调整库文件全、带精简 gcc 工具链的 rootfs 会大一些纯运行时的 rootfs 可能 64MB 就绰绰有余。在镜像制作上我习惯预留 20%~30% 的余量因为交付后还会遇到在线加包、写日志的需求。这里有一个容易忽略的权限问题cp -a会保留文件属主和权限但如果你在 sudo 状态下操作rootfs 里的普通文件、设备节点模式不会变化不过如果 rootfs 里之前是 NFS 挂载状态有些临时文件可能脏了制作镜像前最好先做一次clean把/tmp、/var/run下的临时内容清理干净再打包。6. 实测全流程从串口日志里定位完善过程的全部隐患6.1 一份健康启动日志的五个关键节点完善完 rootfs 之后不要急着合眼睡觉先把串口日志从头到尾看一遍。一份正常启动日志在根文件系统层面应当包含几个标志性的节点。第一个节点是 U-Boot 引导内核你会看到内核镜像和 DTB 的加载信息。第二个节点是内核解压、初始化驱动最终日志里出现VFS: Mounted root (ext4 filesystem) on device ...看到这行说明 rootfs 挂载成功。第三个节点紧接着是init进程启动BusyBox 会打印一行自己的版本信息。第四个节点是 rcS 脚本执行后出现的网络或者服务相关输出。最后一个节点是串口出现登录提示符或 shell 提示符。如果你在哪两个节点之间发现了断层那就是排查的起点。比如挂载 rootfs 成功但 init 没起来问题在 init 可执行性和动态库rcS 跑了一半卡死问题通常在脚本自身逻辑init 启动了但没有登录提示问题在 inittab 和终端设备名。6.2 高频故障快速对表为了方便你在现场快速定位我把完善过程中最高频的故障现象、可能原因和检查手段整理成一张表强烈建议把它记在笔记里随时翻现象可能原因优先检查项No init foundpanicrootfs 里动态链接器缺失或 init 符号链接不存在readelf -l busybox | grep INTERP确认 lib 目录挂载 rootfs 失败内核没开对应文件系统类型或分区信息不对检查内核.config中的 ext4/ramfs/squashfs 支持init 启动后无串口输出inittab 设备名与内核 console 不一致确认 console 参数与 inittab id 字段rcS 执行到一半退出脚本缺可执行权限或挂载类型不支持ls -l rcS内核配置检查 proc/sysfs登录后弹回 login 提示passwd/shadow/group 文件信息不完整/etc/passwd、/etc/shadow、/etc/group逐项核对能 ping IP 不能 ping 域名resolv.conf 缺失或 nameserver 不对检查/etc/resolv.conf内容modprobe 找不到模块模块目录或 modules.dep 缺失modules_install是否正常执行排查时有一个非常高效的救援模式技巧在内核启动参数里追加init/bin/sh让内核跳过初始化脚本、直接给你一个可交互 shell。这样即使 inittab 或 rcS 已经把系统搞到卡死你也能进入 shell 手工修复文件。完成修复后再把内核参数改回来重新正常启动。6.3 完善 rootfs 应当养成的两个习惯做系统移植做到这一期我想额外聊两句长期价值的东西不是教你某一招而是教你少走弯路。第一宿主机的 chroot 快速校验法值得成为固定动作。在 Ubuntu20.04 上装好qemu-user-static之后把qemu-arm-static复制到 rootfs 的/usr/bin然后执行sudo chroot /path/to/rootfs /bin/sh你可以直接在 x86 宿主机里模拟 ARM 环境执行 rootfs 里的命令提前发现动态库缺失、脚本语法错误这类问题完全不用碰板子。这个手段比每次烧写后再看日志高效一个量级。第二养成rootfs 三副本的习惯。一份最小调试版一份完整开发版一份准备交付的精简版。每份都用 git 管理关键脚本和配置文件改动留痕。我在实际项目里不止一次因为改坏了某个 profile 而被迫回到板子前折腾半小时后来有了版本管理一条git diff就能定位问题。这一期从 init 启动失败开始一直做到库文件搬运、登录系统、网络初始化、NFS 调试和镜像打包整个过程里最核心的思维方式是系统移植不只是把文件摆对更重要的是把系统启动的每个阶段拆开理解。每个阶段之间都有明确的签名和日志你把签名认全了问题基本就解决一半了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑