4. 根文件系统构建:从零到NFS挂载
根文件系统,说白了就是Linux启动后挂载的第一个文件系统。它里面装着init程序、各种库、配置文件,还有你常用的命令工具。没有它,内核启动完就只能panic——我早年第一次做系统移植时就栽在这个坑里,内核起来了,结果找不到init,直接崩溃。
这一章我们聚焦四个核心问题:怎么用Busybox搭一个最小的根文件系统、glibc和uClibc怎么选、init进程和inittab怎么配、以及怎么用NFS挂载根文件系统来加速开发。
4.1 Busybox:最小根文件系统的瑞士军刀
Busybox是一个把几百个常用Linux命令打包成一个可执行文件的工具。它特别适合嵌入式环境——你想想看,一个ls命令就要几十KB,但Busybox整个才几百KB,还包含了sh、cp、mv、mount这些核心工具。
编译Busybox的步骤其实不复杂,但有几个关键点要注意:
- 下载源码:我习惯用1.36.x版本,比较稳定
- 配置:
make menuconfig,选上你需要的命令 - 交叉编译:设置CROSS_COMPILE环境变量
- 安装:
make install CONFIG_PREFIX=/path/to/rootfs
关键配置项:在menuconfig里,记得打开Build static binary (no shared libs)。这样编译出来的Busybox不依赖动态库,省去很多麻烦。不过代价是体积会大一些——我一般在调试阶段用静态编译,产品阶段再切回动态。
# 交叉编译Busybox示例
export CROSS_COMPILE=arm-linux-gnueabihf-
make defconfig
make menuconfig # 按需裁剪命令
make -j4
make install CONFIG_PREFIX=./_install
安装完成后,_install目录下会出现bin/、sbin/、usr/这些目录,以及一个关键的linuxrc软链接——它其实就是Busybox本身。
4.2 glibc vs uClibc:C库的抉择
C库的选择直接影响根文件系统的大小和兼容性。我见过不少新手在这上面纠结很久,其实没那么复杂。
| 对比项 | glibc | uClibc |
|---|---|---|
| 体积 | 较大(约2-5MB) | 较小(约200-500KB) |
| 兼容性 | 完全POSIX兼容 | 大部分兼容,偶有差异 |
| 性能 | 经过充分优化 | 轻量,但部分函数较慢 |
| 调试支持 | 完善(gdb、valgrind等) | 有限 |
| 适用场景 | 产品级、功能复杂的系统 | 资源极度受限的设备 |
我的建议:如果你的Flash有8MB以上,直接用glibc。别为了省那几百KB去折腾uClibc的兼容性问题——我在一个项目中吃过这个亏,一个用到了getaddrinfo的网络库在uClibc下行为异常,排查了两天才发现是C库差异。
如果你决定用glibc,需要把交叉编译器对应的库文件拷贝到根文件系统里:
# 拷贝glibc库到根文件系统
cp -a /path/to/toolchain/arm-linux-gnueabihf/libc/lib/* ./_install/lib/
# 注意:只需要拷贝.so文件和ld-linux,不需要.a静态库
4.3 init进程与inittab配置
内核启动完成后,会尝试执行根文件系统里的init程序。Busybox的init会读取/etc/inittab文件,决定启动哪些进程。
一个典型的inittab长这样:
# /etc/inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
每个字段的含义:
- id:终端设备名(可以留空)
- runlevels:运行级别(Busybox忽略此字段)
- action:动作类型——sysinit、respawn、askfirst、once、shutdown等
- process:要执行的程序
注意:respawn和askfirst的区别很大。respawn会在进程退出后自动重启,而askfirst会在启动前先打印"Press Enter to activate this console"。我建议调试阶段用askfirst,这样你可以选择是否进入shell,避免系统反复重启。
另外,/etc/init.d/rcS是系统初始化脚本,里面通常做这些事情:
#!/bin/sh
# /etc/init.d/rcS
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
mdev -s # 自动创建设备节点
ifconfig lo 127.0.0.1
# 其他初始化操作...
4.4 NFS挂载根文件系统
开发阶段,每次修改根文件系统都要重新烧写Flash,太慢了。NFS挂载可以让你在主机上直接修改文件,目标板通过网络加载根文件系统——这是我做驱动开发时最常用的方式。
配置步骤分两边:
主机端(Ubuntu为例)
- 安装NFS服务器:
sudo apt install nfs-kernel-server - 编辑
/etc/exports,添加共享目录:
/path/to/rootfs *(rw,sync,no_subtree_check,no_root_squash)
- 重启NFS服务:
sudo systemctl restart nfs-kernel-server
目标板端
内核启动参数需要添加NFS相关的配置。以U-Boot为例:
setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/path/to/rootfs ip=192.168.1.10:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off'
bootm 0x60003000
这里nfsroot指定了NFS服务器IP和共享路径,ip参数配置了目标板的网络信息。
避坑指南:我曾经在NFS挂载上卡了一整天,最后发现是no_root_squash没加。如果不加这个选项,NFS服务器会把root用户映射成nobody,导致目标板上的进程没有权限访问设备节点。另外,记得检查防火墙——Ubuntu的ufw默认可能阻挡NFS端口。
4.5 完整的根文件系统目录结构
把上面所有东西拼起来,一个可用的最小根文件系统应该包含这些:
rootfs/
├── bin/ # Busybox命令(通过软链接指向/bin/busybox)
├── sbin/ # 系统管理命令
├── usr/ # 用户程序
│ ├── bin/
│ └── sbin/
├── lib/ # C库和动态链接器
│ ├── libc.so.6
│ ├── ld-linux-armhf.so.3
│ └── ...
├── etc/ # 配置文件
│ ├── inittab
│ └── init.d/
│ └── rcS
├── dev/ # 设备节点(至少需要console和null)
│ ├── console
│ ├── null
│ └── ...
├── proc/ # 挂载点(空目录)
├── sys/ # 挂载点(空目录)
├── tmp/ # 临时文件(空目录)
└── mnt/ # 挂载点(空目录)
创建设备节点可以用mknod命令:
sudo mknod -m 622 dev/console c 5 1
sudo mknod -m 666 dev/null c 1 3
小技巧:如果你用mdev自动创建设备节点,dev目录下只需要放console和null就够了。mdev会在启动时根据内核上报的uevent事件自动创建其他节点。我习惯在rcS脚本里加上echo /sbin/mdev > /proc/sys/kernel/hotplug,这样热插拔设备也能自动处理。
嗯,到这里,一个完整的根文件系统就构建好了。你可以先试试用NFS挂载验证功能,确认没问题后再考虑烧写到Flash里。调试阶段用NFS,产品阶段用只读文件系统——这是嵌入式开发的常规套路。