3、SquashFS深度解析:只读压缩文件系统原理、mkfs.squashfs参数调优、块大小对齐策略

3.1 为什么嵌入式系统偏爱只读文件系统?

做嵌入式存储优化这些年,我接触过不少文件系统。但要说哪个最适合只读场景,SquashFS绝对排第一。

你想想看,嵌入式设备里很多数据是不需要改写的——固件、内核模块、字体库、配置文件模板。这些东西一旦烧录进去,基本就定型了。用可读写文件系统去存它们,不仅浪费空间,还得考虑掉电保护、日志刷写这些麻烦事。

说白了,SquashFS就是为这种场景量身定做的。它把文件压缩后打包成一个只读镜像,挂载后可以直接访问。我在一个IoT网关项目里,用SquashFS替换了原来的JFFS2根文件系统,镜像体积直接缩小了60%。嗯,这个数字我记得很清楚,因为当时客户对Flash容量卡得很死。

核心优势:

  • 高压缩比:支持gzip、lz4、lzo、xz等多种算法
  • 随机读取友好:按块索引,无需解压整个文件
  • 元数据压缩:目录结构、inode信息都压缩
  • 端到端校验:支持可选的校验和,防止静默数据损坏

3.2 SquashFS的底层原理

要调优参数,得先理解它的内部结构。SquashFS把文件系统分成几个层次:

数据块(Data Block):每个文件被切成固定大小的块,独立压缩。块大小由你指定,默认是128KB。每个块压缩后单独存储,读取时按需解压。

元数据块(Metadata Block):目录项、inode、符号链接这些信息,也按块压缩。但元数据块大小固定为8KB,这个你改不了。

索引表(Fragment Table & Block Pointer):每个数据块在镜像中的偏移量和压缩后大小,都记录在索引表里。挂载时,内核通过索引表快速定位数据。

为什么会设计成这样?我个人的理解是——为了平衡压缩率和随机访问性能。如果整个文件压成一个块,读取一个小文件也得解压整个大块,太浪费。如果块太小,压缩率又上不去。128KB是个不错的折中点。

我的经验: 如果你主要存小文件(比如配置文件、脚本),把块大小降到4KB或8KB,随机读取性能会明显提升。但压缩率会下降10%-15%。

3.3 mkfs.squashfs参数调优实战

mkfs.squashfs是制作SquashFS镜像的工具。参数很多,但真正关键的没几个。我挑几个最常用的说说。

3.3.1 压缩算法选择

算法 压缩比 解压速度 适用场景
gzip 中等 通用场景,兼容性最好
lz4 较低 极快 启动时间敏感的设备
lzo 较低 老内核(2.6.x)兼容
xz 极高 存储极度受限,不关心启动速度

我个人习惯用gzip。它在压缩率和速度之间平衡得最好。xz虽然压缩率最高,但解压速度慢得让人抓狂——我曾经在一个ARM Cortex-A7平台上测试,xz解压速度只有gzip的1/5,启动时间多了3秒。客户直接说不行。

# 使用gzip压缩,块大小128KB
mkfs.squashfs rootfs/ rootfs.squashfs -comp gzip -b 128K

# 使用xz压缩,追求极致压缩率
mkfs.squashfs rootfs/ rootfs.squashfs -comp xz -b 256K -Xdict-size 100%

# 使用lz4,追求最快解压
mkfs.squashfs rootfs/ rootfs.squashfs -comp lz4 -b 64K

3.3.2 块大小对齐策略

块大小对齐,这是个容易被忽略但很重要的点。为什么?因为Flash的擦除块大小通常是128KB或256KB。如果你的SquashFS块大小和Flash擦除块大小不对齐,更新固件时就会多出很多碎片操作。

我曾经在一个项目里踩过这个坑。当时用的NAND Flash,擦除块是128KB。我图省事,把SquashFS块大小设成了64KB。结果每次OTA升级,擦除和写入操作比预期多了30%。后来改成128KB对齐,问题就解决了。

注意: 块大小必须是2的幂次方,范围是4KB到1MB。建议和底层存储的擦除块大小保持一致。

3.3.3 碎片整理与重复数据去重

mkfs.squashfs有两个参数值得关注:

  • -no-fragments:禁用碎片整理。默认开启,会把小文件合并到同一个数据块里,减少空间浪费。
  • -no-duplicates:禁用重复数据检测。默认开启,会识别并合并内容完全相同的文件。

我建议保持默认。除非你明确知道自己在做什么。比如,你的文件系统里有很多大小刚好超过块边界的小文件,关闭碎片整理反而能提升读取性能——因为每个文件独立存储,不需要额外跳转。

# 开启碎片整理和重复数据去重(默认行为)
mkfs.squashfs rootfs/ rootfs.squashfs

# 显式关闭碎片整理
mkfs.squashfs rootfs/ rootfs.squashfs -no-fragments

# 显式关闭重复数据去重
mkfs.squashfs rootfs/ rootfs.squashfs -no-duplicates

3.4 实战:为Buildroot定制SquashFS根文件系统

在Buildroot里配置SquashFS很简单。运行make menuconfig,找到:

Filesystem images → 
  [*] squashfs filesystem
  Compression method (gzip)  → 选择你想要的算法
  (128) Block size (in KiB)  → 设置块大小

但这里有个细节:Buildroot默认的块大小单位是KiB,不是字节。128就是128KB。别填成128000,那会变成125MB的块——嗯,我见过有人这么干过。

另外,如果你需要更精细的控制,可以修改Buildroot的配置脚本,直接调用mkfs.squashfs并传入自定义参数。我一般会在board/目录下放一个后处理脚本:

#!/bin/bash
# 自定义SquashFS制作脚本
# 放在 board/mycompany/myboard/post-build.sh

mkfs.squashfs $TARGET_DIR \
  $BINARIES_DIR/rootfs.squashfs \
  -comp gzip \
  -b 128K \
  -noappend \
  -all-root \
  -processors 4

-all-root这个参数我特别提一下。它把镜像里所有文件的属主都设为root。在嵌入式系统里,这能避免一些权限问题。特别是当你的开发环境和目标环境用户ID不一致时。

3.5 性能测试与验证

做完镜像,怎么验证效果?我一般做三件事:

  1. 检查镜像大小ls -lh rootfs.squashfs,对比原始目录大小。
  2. 测试挂载和读取:在目标板上挂载,用time dd if=/dev/zero of=/mnt/test bs=1M count=10测顺序读性能。
  3. 随机读测试:用fio或者简单的hdparm -t

我记得有一次,客户反馈说系统启动后读取配置文件特别慢。我排查了半天,发现是块大小设成了1MB,而配置文件只有几百字节。每次读配置文件,内核都得解压整个1MB的块。改成4KB后,问题立刻解决。

小技巧:unsquashfs -ll rootfs.squashfs可以查看镜像里的文件列表和压缩信息。用unsquashfs -s rootfs.squashfs可以查看镜像的元数据,包括块大小、压缩算法等。

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 内核配置别忘了:SquashFS支持需要内核开启CONFIG_SQUASHFS和相关压缩算法。我见过有人镜像做好了,内核没开支持,挂载时报错unknown filesystem type
  • 大文件注意内存:解压大文件时,内核需要分配连续内存。如果块大小设得太大(比如1MB),内存碎片严重的系统可能分配失败。
  • 不要用SquashFS存日志:它是只读的。日志文件写不进去。我见过有人把/var/log放在SquashFS里,结果syslog启动失败。

嗯,关于SquashFS就聊这么多。下一章我们讲UBIFS,那是另一个方向——针对NAND Flash的可读写文件系统。到时候再对比着看,你会对嵌入式存储有更完整的认识。