4. 根文件系统构建:从零到NFS挂载

根文件系统,说白了就是Linux启动后挂载的第一个文件系统。它里面装着init程序、各种库、配置文件,还有你常用的命令工具。没有它,内核启动完就只能panic——我早年第一次做系统移植时就栽在这个坑里,内核起来了,结果找不到init,直接崩溃。

这一章我们聚焦四个核心问题:怎么用Busybox搭一个最小的根文件系统、glibc和uClibc怎么选、init进程和inittab怎么配、以及怎么用NFS挂载根文件系统来加速开发。

根文件系统构建核心流程 Busybox编译 配置→编译→安装 C库选择 glibc / uClibc 目录结构 bin/ sbin/ etc/ lib/ init与inittab 进程启动策略 设备节点 mknod / dev/ NFS挂载 开发阶段调试 可启动的根文件系统

4.1 Busybox:最小根文件系统的瑞士军刀

Busybox是一个把几百个常用Linux命令打包成一个可执行文件的工具。它特别适合嵌入式环境——你想想看,一个ls命令就要几十KB,但Busybox整个才几百KB,还包含了sh、cp、mv、mount这些核心工具。

编译Busybox的步骤其实不复杂,但有几个关键点要注意:

  1. 下载源码:我习惯用1.36.x版本,比较稳定
  2. 配置:make menuconfig,选上你需要的命令
  3. 交叉编译:设置CROSS_COMPILE环境变量
  4. 安装: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为例)

  1. 安装NFS服务器:sudo apt install nfs-kernel-server
  2. 编辑/etc/exports,添加共享目录:
/path/to/rootfs *(rw,sync,no_subtree_check,no_root_squash)
  1. 重启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,产品阶段用只读文件系统——这是嵌入式开发的常规套路。