EROFS - 增强型只读文件系统

概述

EROFS 文件系统代表增强型只读文件系统。它的目标是为各种只读用例形成通用的只读文件系统解决方案,而不是仅仅关注节省存储空间而不考虑运行时性能的任何副作用。

它的设计旨在满足灵活性、特性可扩展性和用户有效载荷友好性等需求。除此之外,它仍然是一个简单的、随机访问友好的高性能文件系统,与类似方法相比,可以摆脱不必要的 I/O 放大和常驻内存开销。

它被实现为以下场景的更好选择

  • 只读存储介质或

  • 完全可信的只读解决方案的一部分,这意味着它需要是不可变的,并且对于其发布的官方黄金镜像来说是逐位相同的,因为安全或其他考虑因素和

  • 希望通过使用紧凑的布局、透明的文件压缩和直接访问来最大限度地减少额外的存储空间,并保证端到端的性能,特别是对于那些内存有限的嵌入式设备和具有大量容器的高密度主机。

以下是 EROFS 的主要特性

  • 小端字节序磁盘设计;

  • 支持基于块的分布和基于文件的 fscache 分布;

  • 支持多个设备引用外部 blob,可用于容器镜像;

  • 每个设备使用 32 位块地址,因此目前使用 4KiB 块大小,最大地址空间为 16TiB;

  • 两种 inode 布局,满足不同需求

    Inode 元数据大小

    32 字节

    64 字节

    最大文件大小

    4 GiB

    16 EiB(也受最大卷大小限制)

    最大 uids/gids

    65536

    4294967296

    每个 inode 时间戳

    是 (64 + 32 位时间戳)

    最大硬链接数

    65536

    4294967296

    保留的元数据

    8 字节

    18 字节

  • 支持扩展属性作为一种选项;

  • 支持加速负扩展属性查找的布隆过滤器;

  • 通过使用扩展属性支持 POSIX.1e ACL;

  • 支持透明数据压缩作为一种选项:LZ4、MicroLZMA 和 DEFLATE 算法可以按文件使用;此外,还支持就地解压缩,以避免反弹压缩缓冲区和不必要的页面缓存抖动。

  • 支持基于块的数据去重和滚动哈希压缩数据去重;

  • 与字节寻址的非对齐元数据或更小的块大小替代方案相比,支持尾部填充内联;

  • 支持将尾部数据合并到特殊 inode 作为片段。

  • 支持大型页,以利用 THP(透明大页);

  • 支持未压缩文件的直接 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 邮件列表

挂载选项

(no)user_xattr

设置扩展用户属性。注意:如果选择了 CONFIG_EROFS_FS_XATTR,则默认启用 xattr。

(no)acl

设置 POSIX 访问控制列表。注意:如果选择了 CONFIG_EROFS_FS_POSIX_ACL,则默认启用 acl。

cache_strategy=%s

从现在开始,选择缓存解压缩的策略

disabled

仅就地 I/O 解压缩;

readahead

缓存最后一个不完整的压缩物理簇以供进一步读取。它仍然对剩余的压缩物理簇执行就地 I/O 解压缩;

readaround

缓存不完整压缩物理簇的两端以供进一步读取。它仍然对剩余的压缩物理簇执行就地 I/O 解压缩。

dax={always,never}

使用直接访问(无页面缓存)。请参阅 文件的直接访问

dax

一个遗留选项,是 dax=always 的别名。

device=%s

指定要一起使用的额外设备的路径。

fsid=%s

为 Fscache 后端指定文件系统镜像 ID。

domain_id=%s

在 fscache 模式下指定域 ID,以便给定域 ID 下具有相同 blob 的不同镜像可以共享存储。

Sysfs 条目

有关挂载的 erofs 文件系统的信息可以在 /sys/fs/erofs 中找到。每个挂载的文件系统在 /sys/fs/erofs 中都有一个目录,其名称基于其设备名称(即,/sys/fs/erofs/sda)。(另请参阅 Documentation/ABI/testing/sysfs-fs-erofs)

磁盘细节

摘要

与其他只读文件系统不同,EROFS 卷的设计尽可能简单

                              |-> aligned with the block size
 ____________________________________________________________
| |SB| | ... | Metadata | ... | Data | Metadata | ... | Data |
|_|__|_|_____|__________|_____|______|__________|_____|______|
0 +1K

所有数据区域都应与块大小对齐,但元数据区域可能不对齐。现在可以在两个不同的空间(视图)中观察到所有元数据

  1. 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 4B
    

    Inode 可以是 32 或 64 字节,可以从所有 inode 版本都具有的公共字段 i_format 中区分出来

     __________________               __________________
    |     i_format     |             |     i_format     |
    |__________________|             |__________________|
    |        ...       |             |        ...       |
    |                  |             |                  |
    |__________________| 32 bytes    |                  |
                                     |                  |
                                     |__________________| 64 bytes
    

    Xattrs、盘区、数据内联放置在相应的 inode 之后,并进行适当的对齐,对于不同的数据映射,它们可能是可选的。 _当前_ 支持总共 5 种数据布局

    0

    无数据内联的平面文件数据(无盘区);

    1

    固定大小输出数据压缩(带有非压缩索引);

    2

    具有尾部填充数据内联的平面文件数据(无盘区);

    3

    固定大小输出数据压缩(带有压缩索引,v5.3+);

    4

    基于块的文件 (v5.15+)。

    可选 xattrs 的大小由 inode 标头中的 i_xattr_count 指示。大的 xattrs 或许多不同文件共享的 xattrs 可以存储在共享 xattrs 元数据中,而不是内联在 inode 之后。

  2. 共享 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 元数据的 盘区 区域指示如何获取块数据:这些可以简单地作为 4 字节的块地址数组或 8 字节的块索引形式(有关更多详细信息,请参阅 erofs_fs.h 中的 struct erofs_inode_chunk_index。)

顺便说一下,基于块的文件目前都是未压缩的。

长扩展属性名称前缀

在某些用例中,具有不同值的扩展属性可能只有几个常见的前缀(例如 overlayfs xattrs)。在这种情况下,预定义的前缀在镜像大小和运行时性能方面都效率低下。

引入了长 xattr 名称前缀功能来解决此问题。总体思路是,除了现有的预定义前缀之外,xattr 条目还可以引用用户指定的长 xattr 名称前缀,例如“trusted.overlay.”。

当引用长 xattr 名称前缀时,会设置 erofs_xattr_entry.e_name_index 的最高位(位 7),而较低位(位 0-6)作为一个整体表示所有长名称前缀中被引用的长名称前缀的索引。因此,只有长 xattr 名称前缀之外的名称的尾部存储在 erofs_xattr_entry.e_name 中,如果完整 xattr 名称与它的长 xattr 名称前缀完全匹配,则该尾部可能为空。

只要打包的 inode 有效,所有长 xattr 前缀都一个接一个地存储在打包的 inode 中,否则存储在元 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,并且这种固定大小输出方法可以从历史字典(又名滑动窗口)中获益,因此可以通过使用固定大小输出压缩获得相对较高的压缩率。

具体来说,原始(未压缩)数据被转换为多个大小可变的范围(extent),同时被压缩成物理簇(pcluster)。为了记录每个大小可变的范围,引入了逻辑簇(lcluster)作为压缩索引的基本单元,以指示是否在范围内生成了新的范围(HEAD)或者没有(NONHEAD)。现在,lcluster 的大小是固定的块大小,如下所示。

         |<-    variable-sized extent    ->|<-       VLE         ->|
       clusterofs                        clusterofs              clusterofs
         |                                 |                       |
_________v_________________________________v_______________________v________
... |    .         |              |        .     |              |  .   ...
____|____._________|______________|________.___ _|______________|__.________
    |-> lcluster <-|-> lcluster <-|-> lcluster <-|-> lcluster <-|
         (HEAD)        (NONHEAD)       (HEAD)        (NONHEAD)    .
          .             CBLKCNT            .                    .
           .                               .                  .
            .                              .                .
      _______._____________________________.______________._________________
         ... |              |              |              | ...
      _______|______________|______________|______________|_________________
             |->      big pcluster       <-|-> pcluster <-|

一个物理簇可以看作是物理压缩块的容器,其中包含压缩数据。之前,只支持 lcluster 大小 (4KB) 的 pcluster。自从引入大 pcluster 特性(从 Linux v5.13 开始可用)后,pcluster 可以是 lcluster 大小的倍数。

对于每个 HEAD lcluster,会记录 clusterofs 以指示新范围的起始位置,而 blkaddr 用于查找压缩数据。对于每个 NONHEAD lcluster,使用 delta0 和 delta1 代替 blkaddr 来指示其到 HEAD lcluster 和下一个 HEAD lcluster 的距离。一个 PLAIN lcluster 也是一个 HEAD lcluster,只是它的数据是未压缩的。有关更多详细信息,请参阅 erofs_fs.h 中 “struct z_erofs_vle_decompressed_index” 周围的注释。

如果启用了大 pcluster,则还需要记录 pcluster 在 lcluster 中的大小。让第一个 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 lcluster 之后紧跟着另一个 HEAD lcluster,则没有空间来记录 CBLKCNT,但是很容易知道这样的 pcluster 的大小也是 1 lcluster。

自从 Linux v6.1 起,每个 pcluster 可以用于多个大小可变的范围,因此它可以用于压缩数据去重。