2. 根文件系统目录结构:FHS标准与关键目录详解
做嵌入式Linux开发,你迟早要面对一个问题:根文件系统里到底该放什么?
我刚开始接触这个领域时,以为随便把几个二进制文件扔进去就能跑。结果呢?系统要么启动到一半卡死,要么找不到命令,要么设备节点全乱套。后来我才明白,根文件系统的目录结构不是拍脑袋定的——它背后有一套标准,叫FHS。
2.1 FHS标准:谁规定了目录长这样?
FHS全称是Filesystem Hierarchy Standard,文件系统层次结构标准。说白了,它是一套约定:哪个目录放什么类型的文件,大家统一口径。
你可能会问:嵌入式系统那么小,有必要遵守这个吗?
我的经验是:尽量遵守,但可以裁剪。比如一个只有2MB Flash的IoT设备,你不可能把完整的FHS目录全塞进去。但核心的几个目录——/bin、/sbin、/etc、/dev、/proc、/sys——必须按规矩来。否则,你自己写的脚本、第三方库、甚至内核模块都会找不到路。
核心原则:FHS把文件系统分为两类——静态文件(不会随运行改变的,如可执行程序)和可变文件(日志、配置、临时数据)。静态文件通常放在只读分区,可变文件放在可写分区。这个思路在嵌入式里特别实用。
下面这张图是我自己整理的嵌入式根文件系统核心结构,你可以先有个整体印象:
2.2 /bin 和 /sbin:命令放哪里?
这两个目录放的都是可执行程序,但分工不同。
/bin 放的是普通用户和系统启动都需要的命令。比如 ls、cp、cat、echo、mount、sh。你想想看,系统启动时 init 进程要执行脚本,脚本里要用到这些命令——所以它们必须放在根文件系统里,不能放在 /usr 这种可能挂载在独立分区的目录下。
/sbin 放的是系统管理类的命令,通常只有 root 用。比如 init、reboot、halt、ifconfig、fdisk。我见过有人把 ifconfig 扔到 /bin 里,也能用,但不符合规范。更关键的是,有些系统脚本会硬编码路径查找 /sbin/ifconfig,你放错地方它就找不到了。
我的习惯:在构建最小根文件系统时,我会先用 busybox 生成一个单一体积的二进制,然后通过符号链接把 /bin/sh、/bin/ls、/sbin/init 都指向它。这样既节省空间,又保证所有基础命令都在。一个 busybox 搞定 /bin 和 /sbin,这在嵌入式里是常规操作。
2.3 /etc:系统的“神经中枢”
/etc 目录存放配置文件。没有它,系统就像一个人失去了记忆——不知道启动什么服务、不知道网络怎么配、不知道用户是谁。
常见的 /etc 内容:
| 文件/目录 | 作用 | 嵌入式常见吗? |
|---|---|---|
| /etc/inittab | init 进程的启动配置,定义运行级别和启动程序 | 几乎必选 |
| /etc/fstab | 文件系统挂载表,告诉系统启动时挂载哪些分区 | 常用 |
| /etc/init.d/ | 存放 SysV 风格的启动/停止脚本 | 看情况 |
| /etc/passwd | 用户账户信息 | 最小系统可省略 |
| /etc/profile | Shell 环境配置,设置 PATH、PS1 等 | 建议保留 |
| /etc/network/ | 网络配置,如 interfaces 文件 | 需要网络时必选 |
我曾经在一个项目里犯过傻:把 /etc/inittab 写错了,导致 init 找不到指定的启动脚本。结果系统反复重启,串口日志刷得飞快,我愣是盯了半天才发现是 inittab 里多了一个空格。嗯,配置文件这种东西,差一个字符都不行。
注意:在嵌入式系统里,/etc 通常是可写分区(比如放在 JFFS2 或 UBIFS 上),因为运行过程中可能需要修改配置。但如果你的系统是只读根文件系统(比如 squashfs),那 /etc 就得提前打包好,或者用 overlay 文件系统在运行时覆盖。
2.4 /dev:设备即文件
Linux 的设计哲学之一:一切皆文件。硬件设备在用户空间看来,就是 /dev 下的一个文件节点。
比如串口是 /dev/ttyS0,MMC 存储卡是 /dev/mmcblk0,控制台是 /dev/console。你往 /dev/ttyS0 写数据,数据就从串口发出去——就这么直接。
/dev 的两种管理方式:
- 静态设备节点:提前用 mknod 创建好所有需要的节点。适合设备固定、数量少的系统。我早期做 ARM9 项目时就这么干,因为那时候 udev 还没普及。
- 动态设备节点:使用 devtmpfs 或 udev/mdev,由内核或用户空间守护进程自动创建。现在的主流做法。内核启动时挂载 devtmpfs,设备驱动注册时自动在 /dev 下生成节点。
我个人强烈建议用 devtmpfs。你想想看,手动维护几十个设备节点,还要算主次设备号,多麻烦。而且一旦换了内核版本,设备号可能变,你的静态节点就废了。
2.5 /proc 和 /sys:内核的“窗户”
这两个目录是虚拟文件系统,不占用磁盘空间。它们的内容由内核动态生成,用来暴露内核状态和配置接口。
/proc 主要提供进程和系统信息。比如:
- /proc/cpuinfo — CPU 型号、频率、特性
- /proc/meminfo — 内存使用情况
- /proc/uptime — 系统运行时间
- /proc/<pid>/ — 每个进程一个目录,包含状态、文件描述符、内存映射等
/sys 提供设备模型和驱动信息,比 /proc 更结构化。比如:
- /sys/class/ — 按设备类别组织(如 net、gpio、led)
- /sys/bus/ — 按总线类型组织(如 platform、i2c、spi)
- /sys/devices/ — 所有设备的真实拓扑结构
我举个例子:你想控制一个 GPIO 输出高电平。在 /sys 里,你可以这样操作:
# 导出 GPIO 引脚
echo 17 > /sys/class/gpio/export
# 设置为输出方向
echo out > /sys/class/gpio/gpio17/direction
# 输出高电平
echo 1 > /sys/class/gpio/gpio17/value
看到了吗?操作硬件就像读写文件一样简单。这就是 /sys 的魅力。
重要区别:/proc 偏向进程和系统全局信息,/sys 偏向设备驱动和硬件拓扑。新内核(2.6+)建议优先使用 /sys 来操作硬件,/proc 逐渐只保留进程相关的内容。但 /proc 里的 cpuinfo、meminfo 这些经典接口,短期内不会消失。
2.6 其他关键目录
除了上面六个核心目录,还有几个你一定会遇到的:
- /usr:放用户程序和数据。在嵌入式里,/usr 经常和根文件系统合并,不单独分区。但 FHS 要求 /usr 下的 bin、lib、share 等子目录结构要清晰。
- /var:可变数据,比如日志、锁文件、临时数据库。嵌入式系统如果不需要持久化日志,可以用 tmpfs 挂载 /var,重启即清空。
- /tmp:临时文件。几乎总是用 tmpfs,掉电不保留。
- /lib:共享库和内核模块。没有它,/bin 里的程序跑不起来。ld-linux.so.* 就在这里面。
- /mnt 和 /media:挂载点。U盘、SD卡临时挂载的地方。
- /opt:可选软件包。嵌入式里用得少,但如果你要集成第三方闭源软件,放这里比较规范。
避坑指南:我曾经把一个第三方库的 .so 文件随手扔到了 /usr/lib 里,结果程序启动时报错“cannot open shared object file”。查了半天才发现,那个程序链接的是 /lib 下的路径,不是 /usr/lib。记住:启动时需要的库放 /lib,运行时才加载的库可以放 /usr/lib。这个区分在嵌入式里特别重要,因为根文件系统可能是只读的,而 /usr 可能挂载在另一个分区。
2.7 最小根文件系统长什么样?
说了这么多,我们来点实际的。一个能启动到 shell 的最小根文件系统,最少需要哪些目录和文件?
/
├── bin/
│ ├── busybox # 或者 sh, ls, mount 等独立二进制
│ └── sh -> busybox # 符号链接
├── sbin/
│ └── init -> ../bin/busybox # 符号链接
├── etc/
│ ├── inittab # init 配置
│ └── fstab # 挂载表(可选)
├── dev/
│ ├── console # 控制台设备
│ ├── null # 空设备
│ └── ttyS0 # 串口(根据硬件调整)
├── proc/ # 挂载点,启动后 mount -t proc
├── sys/ # 挂载点,启动后 mount -t sysfs
├── lib/
│ ├── ld-linux.so.* # 动态链接器
│ └── libc.so.* # C 库
└── tmp/ # 挂载点,通常用 tmpfs
你看,其实没多少东西。但每个目录都有它不可替代的作用。少了 /dev/console,内核启动到最后一步会卡住,因为无法打开控制台。少了 /lib/libc.so,busybox 直接报错退出。
这就是根文件系统的精妙之处:目录结构是骨架,文件是血肉,配置是灵魂。骨架搭对了,后面往里填东西就顺了。