嵌入式常用的只读文件系统,我们先从squashfs说起,如何设计一个专为嵌入式系统和 Live CD 设计的高压缩只读文件系统,它的文件系统快结构,压缩办法,访问办法以及随机访问速度
SquashFS 本质上是一个压缩的、只读的 Linux 文件系统,但它的用途远不止于此。它还可以作为一个灵活、通用的压缩存档格式使用。
它的核心设计目标有三个:
因为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)。
所以片段块的含义是
关键设计点:如果某个块压缩后的尺寸反而比原始数据还大,SquashFS 会直接存储原始未压缩数据。这确保了块在磁盘上的大小永远不会超过输入块大小——一个务实且必要的选择。
与数据块不同,元数据(inode、目录列表等)被当作一个连续的流来处理,每 8KB 切割成一个元数据块并独立压缩。输入大小固定为 8KB,与数据块大小无关。
元数据块有一个特殊前缀:一个 16 位无符号整数,存储该块的磁盘大小,最高位表示是否未压缩。读取时需要先读这个 16 位头,解压后的大小永远不会超过 8KB。
更值得注意的是,元数据条目可以跨越块边界。一个 inode 可能位于元数据块的末尾,部分内容延伸到下一个块的开头。读取时必须将两个块都读入并一起解压——这解释了为什么 SquashFS 实现中元数据引用使用 64 位整数:低 16 位是块内偏移,高 48 位是块的磁盘位置。
SquashFS 的查找表(如 fragment table、export table、ID table)存储方式是一个经典的空间换时间设计:
计算公式如下:
block_count = ceil(table_count * entry_size / 8192) meta_index = floor(index * entry_size / 8192) offset = index * entry_size % 8192
访问一个表项最多需要一次元数据块读取(最多 8194 字节)。对于 NFS 导出这类需要频繁 inode 查找的场景,这个设计是值得的。
超级块是 SquashFS 归档的第一个部分,大小固定为 96 字节,包含整个归档的关键信息:
| 字段 | 类型 | 说明 |
|---|---|---|
| magic | u32 | 魔数 0x73717368("hsqs") |
| inode count | u32 | 归档中的 inode 数量 |
| mod time | u32 | 最后修改时间(UTC 秒数,不计闰秒) |
| block size | u32 | 数据块大小(4096~1048576,2 的幂次) |
| frag count | u32 | fragment 表的条目数 |
| compressor | u16 | 压缩算法 ID(1=GZIP, 2=LZMA, 3=LZO, 4=XZ, 5=LZ4, 6=ZSTD) |
| block log | u16 | block size 的 log₂ 值,用于校验 |
| flags | u16 | 标志位(位 OR) |
| id count | u16 | ID 查找表的条目数 |
| version major | u16 | 主版本号(固定为 4) |
| version minor | u16 | 次版本号(固定为 0) |
| root inode | u64 | 根目录 inode 的引用 |
| bytes used | u64 | 归档使用的字节数 |
| ID table | u64 | ID 表的字节偏移 |
| Xattr table | u64 | 扩展属性表的字节偏移 |
| Inode table | u64 | inode 表的字节偏移 |
| Dir. table | u64 | 目录表的字节偏移 |
| Frag table | u64 | fragment 表的字节偏移 |
| Export table | u64 | export 表的字节偏移 |
这里有一个值得玩味的细节:mod time 字段是无符号的,这意味着它会在 2106 年过期(而不是 2038 年的有符号 32 位时间)。这是一个有意的设计选择。
flags 字段有十几个标志位,从 inode 未压缩、数据块未压缩、fragment 使用策略,到 NFS 导出支持、扩展属性等。但唯一被 Linux 内核实际检查的标志是"压缩选项存在"位(0x0400)。其余标志基本只用于告知编辑工具原始打包设置。
为什么这样设计?因为 Linux 内核的首要任务是正确读取归档,而不是关心它是怎么被创建出来的。
当 flags 中的 0x0400 位置被设置时,超级块后紧跟一个未压缩的元数据块,内容为压缩选项:
这里有个特殊规则:LZ4 压缩选项必须存在,即使使用默认值也不例外。这与其他压缩器的可选选项形成鲜明对比。
SquashFS 是一个有意思的设计例子:它只允许使用单一压缩器,同时应用于数据块和元数据块。不支持数据用 zstd、元数据用 gzip 这样的混合策略。
各算法的现状:
有一点值得注意: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
我们逐步看看这些信息的拆解
我们知道这些表都是8K的大小,所以拿到上面的table的offset,直接查找8K的内容即可
hexdump -C -s $INODE_START -n 8192 filesystem.squashfs
这里INODE_START就是表的offset值
0x73717368("hsqs")、主版本号 4、次版本号 0 是 SquashFS 格式的硬编码常量