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 虚拟文件系统架构 用户空间(User Space) 应用程序:open() / read() / write() 系统调用层(Syscall Layer) VFS 核心层(Virtual File System) 超级块 super_block 索引节点 inode 目录项 dentry 文件对象 file 提供统一抽象接口,屏蔽底层差异 具体文件系统层(FS Drivers) ext4 ubifs jffs2 squashfs 存储介质层 NAND / NOR / eMMC / SD

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 如何选择?我的建议

说了这么多,到底该怎么选?我根据经验总结了一个简单的决策流程:

  1. 先看存储介质:如果是eMMC或SD卡,优先考虑ext4。如果是NAND Flash,优先考虑ubifs。如果是NOR Flash且容量很小,jffs2还能凑合用。
  2. 再看读写需求:如果根文件系统不需要写,果断用squashfs。如果数据分区需要频繁写,ubifs或ext4都行,但要注意磨损均衡。
  3. 最后看资源限制:如果内存非常紧张(比如小于32MB),jffs2可能是唯一选择。如果内存还算充裕(64MB以上),ubifs的体验会好很多。