编辑
2026-06-05
记录知识
0

目录

什么是 SquashFS
整体架构:九个部件的精密协作
文件数据的打包策略
数据块:分而治之
元数据块:8KB 的固定帧
查找表:常数时间的代价
超级块:96 字节的枢纽
标志位:看似很多,实则只用一处
压缩选项:格式各异
支持的压缩算法
示例程序
总结

嵌入式常用的只读文件系统,我们先从squashfs说起,如何设计一个专为嵌入式系统和 Live CD 设计的高压缩只读文件系统,它的文件系统快结构,压缩办法,访问办法以及随机访问速度

什么是 SquashFS

SquashFS 本质上是一个压缩的、只读的 Linux 文件系统,但它的用途远不止于此。它还可以作为一个灵活、通用的压缩存档格式使用。

它的核心设计目标有三个:

  1. 快速随机访问:压缩文件被分割成固定大小的块,每个块独立压缩存储
  2. 完整的 Unix 权限支持:支持文件权限、稀疏文件和扩展属性
  3. 多压缩算法支持:可选用 zlib、lz4、lzo、lzma、xz 或 zstd 进行压缩

因为squashfs-tools和 squashfs-tools-ng 的默认制作参数是 128K,所以我们可以看到大部分squashfs的默认块大小为 128KB(可通过 mksquashfs 设置 4KB 到 1MB 之间的任意 2 的幂次值)。 为什么squashfs-tools-ng要选 128KB?因为这是压缩率和随机访问性能的平衡点——块太小压缩率低,块太大则随机读取时要解压很多无关数据。

整体架构:九个部件的精密协作

SquashFS总是以小端序格式存储整数。 构成 SquashFS 归档的数据块是字节对齐的(没有以4K这种效果对齐)

SquashFS 归档文件由最多九个部分有序组成:

_______________ | Superblock | 归档的核心,包含其他所有部件的位置信息 |_______________| | Compression | 可选,存储非默认压缩选项 | options | |_______________| | Data blocks | 文件内容,分割为独立压缩的块 | & fragments | |_______________| | Inode table | 文件的元数据(所有权、权限等) |_______________| | Directory | 目录列表,包含文件名和 inode 引用 | table | |_______________| | Fragment | 描述片段块在数据区中的位置 | table | |_______________| | Export | inode 号到磁盘位置的映射,支持 NFS 导出 | table | |_______________| | UID/GID | 唯一的 UID/GID 列表,inode 用索引节省空间 | lookup table| |_______________| | Xattr | 扩展属性表 | table | |_______________|

文件数据的打包策略

数据块:分而治之

文件被分割成固定大小的块(128K),每个块独立压缩并按顺序存储。如果一个文件不是块大小的整数倍,剩余的"尾部"可以单独处理,也可以与其他文件的尾部打包成一个片段块(fragment block)

所以片段块的含义是

  1. 如果一个文件超过块大小的取余部分
  2. 其他小文件的组合(小于128K文件)

关键设计点:如果某个块压缩后的尺寸反而比原始数据还大,SquashFS 会直接存储原始未压缩数据。这确保了块在磁盘上的大小永远不会超过输入块大小——一个务实且必要的选择。

元数据块:8KB 的固定帧

与数据块不同,元数据(inode、目录列表等)被当作一个连续的流来处理,每 8KB 切割成一个元数据块并独立压缩。输入大小固定为 8KB,与数据块大小无关。

元数据块有一个特殊前缀:一个 16 位无符号整数,存储该块的磁盘大小,最高位表示是否未压缩。读取时需要先读这个 16 位头,解压后的大小永远不会超过 8KB。

更值得注意的是,元数据条目可以跨越块边界。一个 inode 可能位于元数据块的末尾,部分内容延伸到下一个块的开头。读取时必须将两个块都读入并一起解压——这解释了为什么 SquashFS 实现中元数据引用使用 64 位整数:低 16 位是块内偏移,高 48 位是块的磁盘位置。

查找表:常数时间的代价

SquashFS 的查找表(如 fragment table、export table、ID table)存储方式是一个经典的空间换时间设计:

  1. 把表分成 8KB 的元数据块,分别压缩存储
  2. 额外存储一个 64 位位置列表,记录每个元数据块在磁盘上的位置(不压缩)
  3. 通过二分查找定位到正确的元数据块,再在块内计算偏移

计算公式如下:

block_count = ceil(table_count * entry_size / 8192) meta_index = floor(index * entry_size / 8192) offset = index * entry_size % 8192

访问一个表项最多需要一次元数据块读取(最多 8194 字节)。对于 NFS 导出这类需要频繁 inode 查找的场景,这个设计是值得的。

超级块:96 字节的枢纽

超级块是 SquashFS 归档的第一个部分,大小固定为 96 字节,包含整个归档的关键信息:

字段类型说明
magicu32魔数 0x73717368("hsqs")
inode countu32归档中的 inode 数量
mod timeu32最后修改时间(UTC 秒数,不计闰秒)
block sizeu32数据块大小(4096~1048576,2 的幂次)
frag countu32fragment 表的条目数
compressoru16压缩算法 ID(1=GZIP, 2=LZMA, 3=LZO, 4=XZ, 5=LZ4, 6=ZSTD)
block logu16block size 的 log₂ 值,用于校验
flagsu16标志位(位 OR)
id countu16ID 查找表的条目数
version majoru16主版本号(固定为 4)
version minoru16次版本号(固定为 0)
root inodeu64根目录 inode 的引用
bytes usedu64归档使用的字节数
ID tableu64ID 表的字节偏移
Xattr tableu64扩展属性表的字节偏移
Inode tableu64inode 表的字节偏移
Dir. tableu64目录表的字节偏移
Frag tableu64fragment 表的字节偏移
Export tableu64export 表的字节偏移

这里有一个值得玩味的细节:mod time 字段是无符号的,这意味着它会在 2106 年过期(而不是 2038 年的有符号 32 位时间)。这是一个有意的设计选择。

标志位:看似很多,实则只用一处

flags 字段有十几个标志位,从 inode 未压缩、数据块未压缩、fragment 使用策略,到 NFS 导出支持、扩展属性等。但唯一被 Linux 内核实际检查的标志是"压缩选项存在"位(0x0400)。其余标志基本只用于告知编辑工具原始打包设置。

为什么这样设计?因为 Linux 内核的首要任务是正确读取归档,而不是关心它是怎么被创建出来的。

压缩选项:格式各异

当 flags 中的 0x0400 位置被设置时,超级块后紧跟一个未压缩的元数据块,内容为压缩选项:

  • GZIP:压缩级别(1-9)、窗口大小、启用策略
  • XZ:字典大小、CPU 特定过滤器
  • LZ4:版本号和 HC 模式标志
  • ZSTD:压缩级别
  • LZO:算法变体和压缩级别
  • LZMA v1:不支持压缩选项,此部分必须不存在

这里有个特殊规则:LZ4 压缩选项必须存在,即使使用默认值也不例外。这与其他压缩器的可选选项形成鲜明对比。

支持的压缩算法

SquashFS 是一个有意思的设计例子:它只允许使用单一压缩器,同时应用于数据块和元数据块。不支持数据用 zstd、元数据用 gzip 这样的混合策略。

各算法的现状:

  • gzip (zlib):最通用,raw zlib 流(不是 gzip 格式)
  • lzo:速度优先
  • lzma 1 (LZMA):已废弃
  • xz (LZMA v2):高压缩率,但只能使用 CRC32 校验
  • lz4:平衡方案,选项必须有
  • zstd:最先进但需要权衡

有一点值得注意:XZ 压缩器在设置高压缩级别时,会大幅增加解码器的内存消耗。如果 writer 端调高了级别,decoder 端无感知——这在资源受限的嵌入式场景中是一个潜在问题。

示例程序

现在通过命令来初步了解什么是squashfs文件系统,unsquashfs自带一个解析命令如下

# unsquashfs -s filesystem.squashfs Found a valid SQUASHFS 4:0 superblock on filesystem.squashfs. Creation or last append time Fri Jun 5 08:36:30 2026 Filesystem size 532054402 bytes (519584.38 Kbytes / 507.41 Mbytes) Compression gzip Block size 131072 Filesystem is exportable via NFS Inodes are compressed Data is compressed Uids/Gids (Id table) are compressed Fragments are compressed Always-use-fragments option is not specified Xattrs are compressed Duplicates are removed Number of fragments 2 Number of inodes 199 Number of ids 1 Number of xattr ids 0

可以看到这里提取了文件系统的关键信息,我们再进一步细拆

# hexdump -C -n 96 filesystem.squashfs 00000000 68 73 71 73 c7 00 00 00 0e 1a 22 6a 00 00 02 00 |hsqs......"j....| 00000010 02 00 00 00 01 00 11 00 c0 00 01 00 04 00 00 00 |................| 00000020 48 1f 7d 23 00 00 00 00 82 81 b6 1f 00 00 00 00 |H.}#............| 00000030 7a 81 b6 1f 00 00 00 00 ff ff ff ff ff ff ff ff |z...............| 00000040 1e 3d b6 1f 00 00 00 00 63 72 b6 1f 00 00 00 00 |.=......cr......| 00000050 81 7f b6 1f 00 00 00 00 6c 81 b6 1f 00 00 00 00 |........l.......| 00000060

我们逐步看看这些信息的拆解

  1. Magic: 68 73 71 73: hsqs
  2. Inode: c7: 199
  3. 时间戳: 0e 1a 22 6a: 2026-06-05 08:36:30
  4. BlockSize: 00 00 02 00: 128KB
  5. Fragment: 02 00 00 00: 2 个
  6. 压缩算法: 01 00: 0x0001:GZIP just zlib streams (no gzip headers!)
  7. Block log: 11 00: 17
  8. Flags: c0 00: 去重 + 支持 NFS 导出
  9. ID count: 01 00: 1
  10. Version: 04 00 00 00: V4
  11. 根目录 inode 引用: 48 1f 7d 23 00 00 00 00: 高 48 位:元数据块的起始位置,低 16 位:该块内的偏移量
  12. 总大小:82 81 b6 1f 00 00 00 00: 532054402 字节
  13. ID table: 7a 81 b6 1f 00 00 00 00: UID/GID 查找表
  14. Xattr table: ff ff ff ff ff ff ff ff: mkfs的时候没设置
  15. Inode table: 1e 3d b6 1f 00 00 00 00: Inode 元数据表
  16. Directory table: 63 72 b6 1f 00 00 00 00: 目录表
  17. Fragment table: 81 7f b6 1f 00 00 00 00: 段表
  18. Export table: 6c 81 b6 1f 00 00 00 00: NFS Export 表

我们知道这些表都是8K的大小,所以拿到上面的table的offset,直接查找8K的内容即可

hexdump -C -s $INODE_START -n 8192 filesystem.squashfs

这里INODE_START就是表的offset值

总结

  1. SquashFS 是一个面向快速随机访问优化的高压缩只读文件系统,同时支持作为通用压缩存档格式使用
  2. 归档由最多九个部分有序组成,超级块(96 字节)是整个系统的枢纽
  3. 文件数据按固定块大小分割压缩,尾部可打包成 fragment block 以提高压缩率
  4. 元数据使用固定的 8KB 元数据块,条目允许跨块边界是 SquashFS 的独特设计
  5. 查找表通过位置列表加元数据块的方式实现常数时间访问
  6. 支持六种压缩算法(gzip、lzo、xz、lz4、zstd、废弃的 lzma v1),但同一归档只能使用一种
  7. 超级块的 flags 字段看似复杂,但 Linux 内核只检查"压缩选项存在"标志
  8. 魔数 0x73717368("hsqs")、主版本号 4、次版本号 0 是 SquashFS 格式的硬编码常量
  9. 时间戳使用无符号 32 位整数,过期时间为 2106 年而非 2038 年