EROFS - 增强型只读文件系统¶
概述¶
EROFS 文件系统代表增强型只读文件系统。 它的目标是为各种只读用例形成通用的只读文件系统解决方案,而不是仅仅关注节省存储空间而不考虑运行时性能的任何副作用。
它的设计旨在满足灵活性、功能可扩展性和用户负载友好性等需求。 除此之外,它仍然是一个简单的随机访问友好的高性能文件系统,可以摆脱与类似方法相比不必要的 I/O 放大和内存驻留开销。
它被实现为以下场景的更好选择
只读存储介质或
完全受信任的只读解决方案的一部分,这意味着由于安全或其他考虑因素,它需要是不可变的并且与官方黄金镜像逐位相符并且
希望通过使用紧凑的布局、透明的文件压缩和直接访问来最大限度地减少额外的存储空间,同时保证端到端性能,特别是对于那些具有有限内存的嵌入式设备和具有大量容器的高密度主机。
以下是 EROFS 的主要特性
小端磁盘设计;
支持基于块的分布和基于文件的 fscache 分布;
支持多个设备引用外部 blobs,可用于容器镜像;
每个设备 32 位块地址,因此目前 4KiB 块大小最多支持 16TiB 地址空间;
两种 inode 布局,适用于不同的需求
Inode 元数据大小
32 字节
64 字节
最大文件大小
4 GiB
16 EiB(也受最大卷大小限制)
最大 uids/gids
65536
4294967296
每个 inode 的时间戳
否
是 (64 + 32 位时间戳)
最大硬链接数
65536
4294967296
保留的元数据
8 字节
18 字节
支持扩展属性作为选项;
支持 bloom 过滤器,加快负扩展属性查找;
通过使用扩展属性支持 POSIX.1e ACL;
支持透明数据压缩作为选项:可以在每个文件的基础上使用 LZ4、MicroLZMA 和 DEFLATE 算法; 此外,还支持原位解压缩,以避免反弹压缩缓冲区和不必要的页面缓存抖动。
支持基于块的数据去重和滚动哈希压缩数据去重;
与字节寻址的未对齐元数据或较小的块大小替代方案相比,支持 tailpacking 内联;
支持将尾端数据合并到特殊 inode 中作为片段。
支持大 folios 以利用 THPs(透明大页);
支持对未压缩文件进行直接 I/O,以避免循环设备的双重缓存;
支持未压缩镜像上的 FSDAX,用于安全容器和 ramdisk,以避免不必要的页面缓存。
支持通过 Fscache 基础设施进行基于文件的按需加载。
以下 git 树提供了正在开发的文件系统用户空间工具,例如格式化工具 (mkfs.erofs)、磁盘一致性和兼容性检查工具 (fsck.erofs) 和调试工具 (dump.erofs)
git://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git
有关更多信息,请参阅文档站点
欢迎提交错误和补丁,请帮助我们并发送到以下 linux-erofs 邮件列表
linux-erofs 邮件列表 <linux-erofs@lists.ozlabs.org>
挂载选项¶
(no)user_xattr |
设置扩展用户属性。 注意:如果选择了 CONFIG_EROFS_FS_XATTR,则默认启用 xattr。 |
||||||
(no)acl |
设置 POSIX 访问控制列表。 注意:如果选择了 CONFIG_EROFS_FS_POSIX_ACL,则默认启用 acl。 |
||||||
cache_strategy=%s |
从现在开始选择缓存解压缩的策略
|
||||||
dax={always,never} |
使用直接访问(无页面缓存)。 请参阅 文件的直接访问。 |
||||||
dax |
一个旧选项,是 |
||||||
device=%s |
指定要一起使用的额外设备的路径。 |
||||||
fsid=%s |
为 Fscache 后端指定文件系统镜像 ID。 |
||||||
domain_id=%s |
在 fscache 模式下指定域 ID,以便在给定域 ID 下具有相同 blobs 的不同镜像可以共享存储。 |
||||||
fsoffset=%llu |
为主要设备指定块对齐的文件系统偏移量。 |
Sysfs 条目¶
有关已挂载 erofs 文件系统的信息可以在 /sys/fs/erofs 中找到。 每个已挂载的文件系统将在 /sys/fs/erofs 中都有一个基于其设备名称的目录(即,/sys/fs/erofs/sda)。 (另请参阅 ABI 文件测试/sysfs-fs-erofs)
磁盘细节¶
摘要¶
与其他只读文件系统不同,EROFS 卷的设计尽可能简单
|-> aligned with the block size
____________________________________________________________
| |SB| | ... | Metadata | ... | Data | Metadata | ... | Data |
|_|__|_|_____|__________|_____|______|__________|_____|______|
0 +1K
所有数据区域应与块大小对齐,但元数据区域可能不对齐。 现在可以在两个不同的空间(视图)中观察到所有元数据
Inode 元数据空间
每个有效的 inode 应与一个 inode 插槽对齐,这是一个固定值(32 字节),旨在与紧凑的 inode 大小保持一致。
- 可以使用以下公式直接找到每个 inode
inode 偏移量 = meta_blkaddr * block_size + 32 * nid
|-> aligned with 8B |-> followed closely + meta_blkaddr blocks |-> another slot _____________________________________________________________________ | ... | inode | xattrs | extents | data inline | ... | inode ... |________|_______|(optional)|(optional)|__(optional)_|_____|__________ |-> aligned with the inode slot size . . . . . . . . . . . . .____________________________________________________|-> aligned with 4B | xattr_ibody_header | shared xattrs | inline xattrs | |____________________|_______________|_______________| |-> 12 bytes <-|->x * 4 bytes<-| . . . . . . . . . . ._______________________________.______________________. | id | id | id | id | ... | id | ent | ... | ent| ... | |____|____|____|____|______|____|_____|_____|____|_____| |-> aligned with 4B |-> aligned with 4BInode 可以是 32 或 64 字节,可以从所有 inode 版本都有的公共字段 -- i_format 中区分
__________________ __________________ | i_format | | i_format | |__________________| |__________________| | ... | | ... | | | | | |__________________| 32 bytes | | | | |__________________| 64 bytesXattrs、extents、数据内联放置在相应 inode 之后,并进行适当的对齐,并且对于不同的数据映射,它们可以是可选的。 _目前_ 支持总共 5 种数据布局
0
没有数据内联的平面文件数据(没有 extent);
1
固定大小输出数据压缩(具有非紧凑索引);
2
具有尾部打包数据内联的平面文件数据(没有 extent);
3
固定大小输出数据压缩(具有紧凑索引,v5.3+);
4
基于块的文件 (v5.15+)。
可选 xattrs 的大小由 inode 标头中的 i_xattr_count 指示。 大型 xattrs 或许多不同文件共享的 xattrs 可以存储在共享 xattrs 元数据中,而不是直接内联在 inode 之后。
共享 xattrs 元数据空间
共享 xattrs 空间与上述 inode 空间类似,从由 xattr_blkaddr 指示的特定块开始,一个接一个地组织,并进行适当的对齐。
- 也可以通过以下公式直接找到每个共享 xattr
xattr 偏移量 = xattr_blkaddr * block_size + 4 * xattr_id
|-> aligned by 4 bytes
+ xattr_blkaddr blocks |-> aligned with 4 bytes
_________________________________________________________________________
| ... | xattr_entry | xattr data | ... | xattr_entry | xattr data ...
|________|_____________|_____________|_____|______________|_______________
目录¶
现在所有目录都以紧凑的磁盘格式组织。 请注意,每个目录块都分为索引区和名称区,以便支持随机文件查找,并且所有目录条目都 _严格_ 按字母顺序记录,以便支持改进的前缀二进制搜索算法(可以参考相关的源代码)。
___________________________
/ |
/ ______________|________________
/ / | nameoff1 | nameoffN-1
____________.______________._______________v________________v__________
| dirent | dirent | ... | dirent | filename | filename | ... | filename |
|___.0___|____1___|_____|___N-1__|____0_____|____1_____|_____|___N-1____|
\ ^
\ | * could have
\ | trailing '\0'
\________________________| nameoff0
Directory block
请注意,除了第一个文件名的偏移量之外,nameoff0 还指示此块中目录条目的总数,因为根本不需要引入另一个磁盘字段。
基于块的文件¶
为了支持基于块的数据去重,自 Linux v5.15 以来,支持一种新的 inode 数据布局:文件被分割成大小相等的数据块,inode 元数据的 extents
区域指示如何获取块数据:这些可以简单地是一个 4 字节的块地址数组,也可以是 8 字节的块索引形式(有关更多详细信息,请参阅 erofs_fs.h 中的 struct erofs_inode_chunk_index。)
顺便说一句,基于块的文件目前都是未压缩的。
长扩展属性名称前缀¶
在某些用例中,具有不同值的扩展属性只能有几个常见的前缀(例如 overlayfs xattrs)。 在这种情况下,预定义的前缀在镜像大小和运行时性能方面都效率低下。
引入了长 xattr 名称前缀特性来解决这个问题。 总体思路是,除了现有的预定义前缀之外,xattr 条目还可以引用用户指定的长 xattr 名称前缀,例如“trusted.overlay.”。
当引用长 xattr 名称前缀时,erofs_xattr_entry.e_name_index 的最高位(bit 7)被设置,而较低位(bit 0-6)作为一个整体表示引用的长名称前缀在所有长名称前缀中的索引。 因此,只有名称的尾部(除了长 xattr 名称前缀)存储在 erofs_xattr_entry.e_name 中,如果完整的 xattr 名称与其长 xattr 名称前缀完全匹配,则该尾部可能为空。
只要打包 inode 有效,或者在元 inode 中,所有长 xattr 前缀都一个接一个地存储在打包 inode 中。 xattr_prefix_count(在磁盘超级块中)指示长 xattr 名称前缀的总数,而 (xattr_prefix_start * 4) 指示打包/元 inode 中长名称前缀的起始偏移量。 请注意,如果 xattr_prefix_count 为 0,则禁用长扩展属性名称前缀。
每个长名称前缀都以格式存储:ALIGN({__le16 len, data}, 4),其中 len 表示数据部分的总大小。 数据部分实际上由 ‘struct erofs_xattr_long_prefix’ 表示,其中 base_index 表示预定义的 xattr 名称前缀的索引,例如,“trusted.overlay.” 长名称前缀的 EROFS_XATTR_INDEX_TRUSTED,而 infix 字符串保存剥离短前缀后的字符串,例如,上面示例中的“overlay.”。
数据压缩¶
EROFS 实现了固定大小输出压缩,与现有的其他固定大小输入解决方案相比,它可以从可变大小的输入生成固定大小的压缩数据块。 由于当今流行的数据压缩算法大多基于 LZ77,并且这种固定大小输出方法可以从历史字典(又名滑动窗口)中受益,因此使用固定大小输出压缩可以获得相对更高的压缩率。
详细地说,原始(未压缩)数据被转换为多个可变大小的 extents,同时压缩为物理集群(pclusters)。 为了记录每个可变大小的 extent,引入了逻辑集群(lclusters)作为压缩索引的基本单位,以指示是否在范围内生成了一个新的 extent (HEAD) 或没有 (NONHEAD)。 现在 lclusters 的大小是固定的,如下图所示
|<- variable-sized extent ->|<- VLE ->|
clusterofs clusterofs clusterofs
| | |
_________v_________________________________v_______________________v________
... | . | | . | | . ...
____|____._________|______________|________.___ _|______________|__.________
|-> lcluster <-|-> lcluster <-|-> lcluster <-|-> lcluster <-|
(HEAD) (NONHEAD) (HEAD) (NONHEAD) .
. CBLKCNT . .
. . .
. . .
_______._____________________________.______________._________________
... | | | | ...
_______|______________|______________|______________|_________________
|-> big pcluster <-|-> pcluster <-|
一个物理集群可以看作是一个物理压缩块的容器,其中包含压缩数据。 以前,只支持 lcluster 大小(4KB)的 pclusters。 自引入大 pcluster 特性(自 Linux v5.13 起可用)后,pcluster 可以是 lcluster 大小的倍数。
对于每个 HEAD lcluster,记录 clusterofs 以指示新 extent 的开始位置,并使用 blkaddr 查找压缩数据。 对于每个 NONHEAD lcluster,可以使用 delta0 和 delta1 代替 blkaddr 来指示到其 HEAD lcluster 和下一个 HEAD lcluster 的距离。 PLAIN lcluster 也是 HEAD lcluster,除了它的数据未压缩。 有关更多详细信息,请参阅 erofs_fs.h 中 “struct z_erofs_vle_decompressed_index” 周围的注释。
如果启用了 big pcluster,则还需要记录 lclusters 中 pcluster 的大小。 让第一个 NONHEAD lcluster 的 delta0 存储压缩块计数,并带有一个特殊标志,作为一种新调用的 CBLKCNT NONHEAD lcluster。 很容易理解它的 delta0 始终为 1,如下图所示
__________________________________________________________
| HEAD | NONHEAD | NONHEAD | ... | NONHEAD | HEAD | HEAD |
|__:___|_(CBLKCNT)_|_________|_____|_________|__:___|____:_|
|<----- a big pcluster (with CBLKCNT) ------>|<-- -->|
a lcluster-sized pcluster (without CBLKCNT) ^
如果另一个 HEAD 跟随一个 HEAD lcluster,则没有空间来记录 CBLKCNT,但是很容易知道这种 pcluster 的大小也为 1 lcluster。
自 Linux v6.1 以来,每个 pcluster 都可以用于多个可变大小的 extents,因此它可以用于压缩数据去重。