1. 文件系统基础:嵌入式Linux文件系统概述、VFS虚拟文件系统架构、常见文件系统类型对比
各位好,我是老李。做嵌入式Linux系统优化这么多年,我始终觉得文件系统是整台设备的「地基」。地基不稳,上层应用跑得再欢也是白搭。今天咱们就从最基础的东西聊起——嵌入式Linux文件系统到底是什么,它怎么工作的,以及我们常用的那几种文件系统各自有什么脾气。
1.1 嵌入式Linux文件系统概述
文件系统,说白了就是一套管理数据怎么存、怎么读、怎么删的规则。在嵌入式设备上,它跟PC上的文件系统有很大不同。PC上你装个Windows,NTFS一挂就完事了。但在嵌入式环境里,你得考虑:
- 存储介质五花八门:NAND Flash、NOR Flash、eMMC、SD卡,甚至还有SPI Flash。每种介质的读写特性、擦除方式、寿命都不一样。
- 资源极度受限:CPU主频可能只有几百兆,内存可能只有几十兆。文件系统不能太「胖」,不能动不动就占几十MB内存。
- 掉电安全是刚需:嵌入式设备经常被直接拔电。文件系统必须能扛住突然掉电,不能丢数据、不能变砖。
- 只读与可写并存:很多嵌入式系统把根文件系统做成只读的(比如squashfs),防止被意外篡改。而数据分区则用可写的文件系统(比如ubifs)。
我记得刚入行那会儿,接手一个路由器项目。同事把根文件系统直接用了ext4,结果每次异常掉电重启,系统都要花好几分钟做fsck。用户投诉说「这路由器比猫还慢」……后来换成squashfs+ubifs的组合,问题才彻底解决。所以你看,选对文件系统,比调优参数更重要。
核心观点:嵌入式文件系统的选择,本质是在「性能」「可靠性」「空间效率」「写寿命」之间做权衡。没有银弹,只有最适合你场景的方案。
1.2 VFS虚拟文件系统架构
VFS(Virtual File System)是Linux内核里一个非常巧妙的设计。它把底层各种文件系统的差异给「抹平」了。你想想看,用户程序调用open()、read()、write()这些系统调用时,根本不需要关心底层是ext4还是ubifs。VFS就像一个翻译官,把统一的接口翻译成具体文件系统的操作。
VFS的架构可以分成三层:
- 系统调用层:用户程序看到的接口,比如
open、read、write、stat。 - VFS层:内核中的抽象层,维护着四个核心对象——超级块(super_block)、索引节点(inode)、目录项(dentry)、文件对象(file)。
- 具体文件系统层:ext4、ubifs、jffs2、squashfs等实际干活的文件系统驱动。
我画了一张图,帮你理解这个架构:
VFS的四个核心对象,我简单说一下:
- 超级块(super_block):每个挂载的文件系统都有一个超级块,记录着整个文件系统的元信息,比如块大小、总块数、inode总数等。它就像文件系统的「身份证」。
- 索引节点(inode):每个文件或目录都有一个inode,记录着文件的权限、大小、时间戳、数据块指针等。注意,文件名不在这里,文件名在dentry里。
- 目录项(dentry):负责把文件名和inode关联起来。VFS会缓存dentry,这样下次访问同一个路径时就不用重新解析了。这个缓存对性能影响很大,后面我们会专门讲。
- 文件对象(file):代表一个打开的文件实例。它记录了当前读写位置、打开模式(只读/读写)等。同一个文件可以被多次打开,每次打开都会生成一个新的file对象。
个人经验:我在调试一个NAND Flash设备时,发现系统启动后访问文件特别慢。用perf一查,发现大量时间花在dentry缓存未命中上。后来调整了dentry_cache的大小,启动速度提升了近30%。所以,VFS缓存参数是调优的重头戏,后面我们会专门开一章讲。
1.3 常见文件系统类型对比
嵌入式领域常用的文件系统就那么几种:ext4、ubifs、jffs2、squashfs。它们各有各的适用场景。我整理了一张对比表,方便你快速了解:
| 特性 | ext4 | ubifs | jffs2 | squashfs |
|---|---|---|---|---|
| 适用介质 | eMMC、SD卡、硬盘 | NAND Flash | NOR Flash | 任何只读介质 |
| 读写特性 | 读写均衡,适合大文件 | 读写均衡,适合大容量NAND | 读快写慢,适合小容量 | 只读,压缩率高 |
| 掉电安全 | 需要日志(jbd2),掉电恢复较慢 | 内建日志,掉电恢复快 | 内建日志,掉电恢复快 | 只读,无需考虑掉电 |
| 磨损均衡 | 不支持(依赖FTL) | 支持(UBI层实现) | 支持(简单磨损均衡) | 不涉及 |
| 压缩 | 支持(可选) | 支持(LZO/zlib) | 支持(zlib) | 支持(gzip/lzo/xz) |
| 挂载速度 | 较快 | 较快 | 慢(需要扫描整个介质) | 极快 |
| 典型用途 | eMMC根文件系统、数据分区 | NAND Flash根文件系统、数据分区 | 小容量NOR Flash启动分区 | 只读根文件系统、固件镜像 |
下面我逐个展开说说:
ext4
ext4是从PC领域「下放」到嵌入式领域的。它成熟、稳定、工具链完善。但它在嵌入式环境里有个硬伤——对NAND Flash支持不好。ext4假设底层存储是块设备,有FTL(Flash Translation Layer)帮你处理坏块和磨损均衡。但很多嵌入式设备用的是裸NAND,没有FTL。这时候用ext4,坏块管理、磨损均衡都得自己搞,非常麻烦。
我建议:如果你的设备用的是eMMC(它自带FTL),ext4是个不错的选择。但如果是裸NAND,还是老老实实用ubifs吧。
ubifs
ubifs是专门为NAND Flash设计的文件系统。它运行在UBI(Unsorted Block Images)层之上,UBI负责处理坏块管理、磨损均衡和逻辑块到物理块的映射。ubifs的优势很明显:
- 挂载速度快:不像jffs2那样需要扫描整个介质,ubifs通过索引树(B-tree)快速定位数据。
- 写性能好:支持写缓冲和延迟写入,吞吐量比jffs2高不少。
- 空间利用率高:支持在线压缩,可以节省30%-50%的存储空间。
不过ubifs也有缺点:它占用的内存比jffs2多,因为要维护索引树。如果你的设备内存只有16MB,用ubifs可能会有点吃力。
jffs2
jffs2是嵌入式Linux早期的「老将」了。它直接操作NAND/NOR Flash,不需要额外的FTL层。它的优点是实现简单、内存占用低。但缺点也很明显:
- 挂载极慢:挂载时需要扫描整个Flash介质,构建文件系统树。对于128MB的NAND,挂载时间可能长达几十秒。
- 写放大严重:每次写操作都可能触发垃圾回收,导致实际写入量远大于用户数据量。
- 不支持压缩:虽然可以启用zlib压缩,但性能损失较大。
我曾经在一个老项目里用过jffs2,每次系统升级都要等好几分钟。后来换成ubifs,升级时间缩短到30秒以内。所以,除非你的Flash容量很小(比如32MB以下),否则我不建议再用jffs2了。
squashfs
squashfs是一个只读的、压缩的文件系统。它的设计目标很明确:把一堆文件打包成一个压缩镜像,然后挂载使用。它的特点:
- 压缩率极高:用xz压缩算法,能把一个100MB的根文件系统压到30MB左右。
- 挂载极快:不需要扫描,直接读取超级块就能挂载。
- 内存占用低:数据按需解压,不会一次性加载到内存。
squashfs非常适合做只读根文件系统。很多路由器、机顶盒、工业控制器都用它。把根文件系统做成squashfs,既能节省存储空间,又能防止用户误删系统文件。
注意:squashfs是只读的,不能直接在上面写数据。所以通常的做法是:用squashfs做根文件系统,再挂载一个可写的分区(比如ubifs或ext4)做数据存储。这种「只读根+可写数据」的组合,是嵌入式系统里最经典的方案之一。
1.4 如何选择?我的建议
说了这么多,到底该怎么选?我根据经验总结了一个简单的决策流程:
- 先看存储介质:如果是eMMC或SD卡,优先考虑ext4。如果是NAND Flash,优先考虑ubifs。如果是NOR Flash且容量很小,jffs2还能凑合用。
- 再看读写需求:如果根文件系统不需要写,果断用squashfs。如果数据分区需要频繁写,ubifs或ext4都行,但要注意磨损均衡。
- 最后看资源限制:如果内存非常紧张(比如小于32MB),jffs2可能是唯一选择。如果内存还算充裕(64MB以上),ubifs的体验会好很多。