2. 高层设计

ext4 文件系统被分割成一系列的块组。为了减少因碎片化导致的性能问题,块分配器会尽力将每个文件的块保存在同一个组内,从而减少寻道时间。块组的大小在 sb.s_blocks_per_group 块中指定,但也可以计算为 8 * block_size_in_bytes。使用默认的 4KiB 块大小,每个组将包含 32,768 个块,长度为 128MiB。块组的数量是设备大小除以块组的大小。

ext4 中的所有字段都以小端字节序写入磁盘。然而,jbd2(日志)中的所有字段都以大端字节序写入磁盘。

2.1.

ext4 以“块”为单位分配存储空间。“块”是 1KiB 到 64KiB 之间的一组扇区,扇区数必须是 2 的整数次幂。块又被分组为更大的单元,称为块组。块大小在 mkfs 时指定,通常为 4KiB。如果块大小大于页面大小(例如,在只有 4KiB 内存页的 i386 上使用 64KiB 块),可能会遇到挂载问题。默认情况下,一个文件系统可以包含 2^32 个块;如果启用“64bit”功能,则一个文件系统可以拥有 2^64 个块。结构的位置以结构所在的块号而不是磁盘上的绝对偏移量来存储。

对于 32 位文件系统,限制如下:

项目

1KiB

2KiB

4KiB

64KiB

块数

2^32

2^32

2^32

2^32

索引节点数

2^32

2^32

2^32

2^32

文件系统大小

4TiB

8TiB

16TiB

256TiB

每个块组的块数

8,192

16,384

32,768

524,288

每个块组的索引节点数

8,192

16,384

32,768

524,288

块组大小

8MiB

32MiB

128MiB

32GiB

每个文件的块数,范围

2^32

2^32

2^32

2^32

每个文件的块数,块映射

16,843,020

134,480,396

1,074,791,436

4,398,314,962,956(实际上是 2^32,由于字段大小的限制)

文件大小,范围

4TiB

8TiB

16TiB

256TiB

文件大小,块映射

16GiB

256GiB

4TiB

256TiB

对于 64 位文件系统,限制如下:

项目

1KiB

2KiB

4KiB

64KiB

块数

2^64

2^64

2^64

2^64

索引节点数

2^32

2^32

2^32

2^32

文件系统大小

16ZiB

32ZiB

64ZiB

1YiB

每个块组的块数

8,192

16,384

32,768

524,288

每个块组的索引节点数

8,192

16,384

32,768

524,288

块组大小

8MiB

32MiB

128MiB

32GiB

每个文件的块数,范围

2^32

2^32

2^32

2^32

每个文件的块数,块映射

16,843,020

134,480,396

1,074,791,436

4,398,314,962,956(实际上是 2^32,由于字段大小的限制)

文件大小,范围

4TiB

8TiB

16TiB

256TiB

文件大小,块映射

16GiB

256GiB

4TiB

256TiB

注意:不使用范围的文件(即使用块映射的文件)必须放置在文件系统的前 2^32 个块内。具有范围的文件必须放置在文件系统的前 2^48 个块内。目前尚不清楚更大的文件系统会发生什么。

2.2. 布局

标准块组的布局大致如下(以下各节将分别讨论这些字段)

组 0 填充

ext4 超级块

组描述符

保留 GDT 块

数据块位图

索引节点位图

索引节点表

数据块

1024 字节

1 个块

许多块

许多块

1 个块

1 个块

许多块

更多块

对于块组 0 的特殊情况,前 1024 个字节未使用,以便安装 x86 引导扇区和其他特殊用途。超级块将从偏移量 1024 字节处开始,无论该块是什么(通常是 0)。但是,如果由于某种原因块大小 = 1024,则块 0 被标记为已使用,并且超级块位于块 1 中。对于所有其他块组,没有填充。

ext4 驱动程序主要使用位于块组 0 中的超级块和组描述符。超级块和组描述符的冗余副本会写入磁盘上的一些块组中,以防磁盘开头损坏,但并非所有块组都必须托管冗余副本(有关更多详细信息,请参见下段)。如果该组没有冗余副本,则该块组以数据块位图开始。另请注意,当文件系统新格式化时,mkfs 会在块组描述符之后和块位图的开始之前分配“保留 GDT 块”空间,以允许将来扩展文件系统。默认情况下,允许文件系统的大小比原始文件系统的大小增加 1024 倍。

索引节点表的位置由 grp.bg_inode_table_* 给出。它是连续的块范围,足够大以包含 sb.s_inodes_per_group * sb.s_inode_size 字节。

至于块组中项目的排序,通常确定超级块和组描述符表(如果存在)将位于块组的开头。位图和索引节点表可以位于任何位置,位图很可能在索引节点表之后,或者两者都位于不同的组中 (flex_bg)。剩余空间用于文件数据块、间接块映射、范围树块和扩展属性。

2.3. 灵活块组

从 ext4 开始,有一个称为灵活块组 (flex_bg) 的新功能。在 flex_bg 中,几个块组被绑定在一起作为一个逻辑块组;flex_bg 的第一个块组中的位图空间和索引节点表空间被扩展为包含 flex_bg 中所有其他块组的位图和索引节点表。例如,如果 flex_bg 大小为 4,则组 0 将包含(按顺序)超级块、组描述符、组 0-3 的数据块位图、组 0-3 的索引节点位图、组 0-3 的索引节点表,并且组 0 中的剩余空间用于文件数据。这样做的效果是将块组元数据紧密地组合在一起以加快加载速度,并使大文件在磁盘上连续。超级块和组描述符的备份副本始终位于块组的开头,即使启用了 flex_bg。构成 flex_bg 的块组数由 2 ^ sb.s_log_groups_per_flex 给出。

2.4. 元块组

如果没有 META_BG 选项,出于安全考虑,所有块组描述符副本都保留在第一个块组中。给定默认的 128MiB (2^27 字节) 块组大小和 64 字节的组描述符,ext4 最多可以有 2^27/64 = 2^21 个块组。这会将整个文件系统大小限制为 2^21 * 2^27 = 2^48 字节或 256TiB。

解决此问题的方法是使用元块组功能 (META_BG),该功能已在所有 2.6 版本的 ext3 中存在。使用 META_BG 功能,ext4 文件系统被划分为多个元块组。每个元块组都是块组的集群,其组描述符结构可以存储在单个磁盘块中。对于块大小为 4 KB 的 ext4 文件系统,单个元块组分区包括 64 个块组,或 8 GiB 的磁盘空间。元块组功能将组描述符的位置从整个文件系统的拥挤的第一个块组移动到每个元块组本身的第一个组中。备份位于每个元块组的第二个和最后一个组中。这会将 2^21 的最大块组限制提高到硬限制 2^32,从而支持 512PiB 的文件系统。

文件系统格式的更改替换了当前方案,在该方案中,超级块后跟一组可变长度的块组描述符。相反,超级块和单个块组描述符块放置在元块组的第一个、第二个和最后一个块组的开头。元块组是可以由单个块组描述符块描述的块组的集合。由于块组描述符结构的大小为 64 字节,因此对于块大小为 1KB 的文件系统,元块组包含 16 个块组,对于块大小为 4KB 的文件系统,元块组包含 64 个块组。可以使用这种新的块组描述符布局来创建文件系统,也可以在线调整现有文件系统的大小,并且超级块中的字段 s_first_meta_bg 将指示使用这种新布局的第一个块组。

请参阅有关块和索引节点位图部分的 BLOCK_UNINIT 的重要说明。

2.5. 延迟块组初始化

ext4 的一个新功能是三个块组描述符标志,使 mkfs 可以跳过初始化块组元数据的其他部分。具体来说,INODE_UNINIT 和 BLOCK_UNINIT 标志表示可以计算该组的索引节点和块位图,因此不初始化磁盘上的位图块。这通常适用于空块组或仅包含固定位置的块组元数据的块组。INODE_ZEROED 标志表示索引节点表已初始化;mkfs 将取消设置此标志,并依赖内核在后台初始化索引节点表。

通过不将零写入位图和索引节点表,mkfs 时间大大减少。请注意,功能标志是 RO_COMPAT_GDT_CSUM,但是 dumpe2fs 输出将其打印为“uninit_bg”。它们是同一件事。

2.6. 特殊索引节点

ext4 为特殊功能保留了一些索引节点,如下所示

索引节点号

目的

0

不存在;没有索引节点 0。

1

缺陷块列表。

2

根目录。

3

用户配额。

4

组配额。

5

引导加载程序。

6

取消删除目录。

7

保留的组描述符索引节点。(“调整大小索引节点”)

8

日志索引节点。

9

“排除”索引节点,用于快照?

10

副本索引节点,用于某些非上游功能?

11

传统的第一个非保留索引节点。通常,这是 lost+found 目录。请参见超级块中的 s_first_ino。

请注意,还有一些从非保留索引节点号中分配的索引节点,用于其他不从标准目录层次结构引用的文件系统功能。这些通常从超级块引用。他们是

超级块字段

描述

s_lpf_ino

lost+found 目录的索引节点号。

s_prj_quota_inum

用于跟踪项目配额的配额文件的索引节点号

s_orphan_file_inum

用于跟踪孤立索引节点的文件的索引节点号。

2.7. 块和索引节点分配策略

ext4 意识到(无论如何,比 ext3 更好)数据局部性通常是文件系统的一个理想特性。在旋转磁盘上,保持相关块彼此靠近可以减少磁头执行器和磁盘访问数据块所需的移动量,从而加快磁盘 IO。在 SSD 上,当然没有移动部件,但局部性可以增加每次传输请求的大小,同时减少请求的总数。这种局部性还可能具有将写入集中在单个擦除块上的效果,这可以显着加快文件重写速度。因此,尽可能减少碎片是很有用的。

ext4 用于对抗碎片的首个工具是多块分配器。当首次创建文件时,块分配器会推测性地为该文件分配 8KiB 的磁盘空间,假设该空间很快会被写入。当文件关闭时,未使用的推测性分配当然会被释放,但如果推测正确(通常对于小型文件的完整写入是这种情况),则文件数据将以单个多块范围写入。ext4 使用的第二个相关技巧是延迟分配。在这种方案下,当文件需要更多块来吸收文件写入时,文件系统会推迟决定磁盘上的确切位置,直到所有脏缓冲区都被写入磁盘。通过不承诺特定的位置,直到绝对必要时(达到提交超时,或者调用 sync(),或者内核内存耗尽),希望文件系统可以做出更好的位置决策。

ext4(以及 ext3)使用的第三个技巧是,它试图将文件的数据块与它的 inode 放在同一个块组中。当文件系统首先必须读取文件的 inode 以了解文件的数据块所在位置,然后再寻址到文件的数据块以开始 I/O 操作时,这减少了寻道惩罚。

第四个技巧是,在可行的情况下,目录中的所有 inode 都与目录放在同一个块组中。这里的工作假设是目录中的所有文件可能是相关的,因此尝试将它们放在一起是有用的。

第五个技巧是将磁盘卷分割成 128MB 的块组;这些小型容器如上所述用于尝试保持数据局部性。然而,这里有一个刻意的怪癖 - 当在根目录中创建目录时,inode 分配器会扫描块组,并将该目录放置在它可以找到的负载最轻的块组中。这鼓励目录在磁盘上分散;当顶层目录/文件 blob 填满一个块组时,分配器只需移动到下一个块组。据称,这种方案可以使块组上的负载均匀化,但作者怀疑那些不幸位于旋转驱动器末端的目录在性能方面会受到不公平的待遇。

当然,如果所有这些机制都失败了,始终可以使用 e4defrag 来整理文件碎片。

2.8. 校验和

从 2012 年初开始,元数据校验和被添加到所有主要的 ext4 和 jbd2 数据结构中。相关的特性标志是 metadata_csum。所需的校验和算法在超级块中指示,但截至 2012 年 10 月,唯一支持的算法是 crc32c。一些数据结构没有空间容纳完整的 32 位校验和,因此只存储了较低的 16 位。启用 64 位特性会增加数据结构的大小,以便可以为许多数据结构存储完整的 32 位校验和。但是,现有的 32 位文件系统无法扩展以启用 64 位模式,至少在没有用于执行此操作的实验性 resize2fs 补丁的情况下无法扩展。

可以通过针对底层设备运行 tune2fs -O metadata_csum 来为现有文件系统添加校验和。如果 tune2fs 遇到目录块,它们没有足够的空间来添加校验和,它会要求你运行 e2fsck -D 来重建带有校验和的目录。这还具有从目录文件中删除空闲空间并重新平衡 htree 索引的额外好处。如果你 _忽略_ 这一步,你的目录将不会受到校验和的保护!

下表描述了进入每种类型的校验和的数据元素。校验和函数是超级块描述的任何函数(截至 2013 年 10 月为 crc32c),除非另有说明。

元数据

长度

成分

超级块

__le32

整个超级块,直到校验和字段。UUID 存在于超级块内部。

MMP

__le32

UUID + 整个 MMP 块,直到校验和字段。

扩展属性

__le32

UUID + 整个扩展属性块。校验和字段设置为零。

目录条目

__le32

UUID + inode 编号 + inode 生成 + 目录块,直到包含校验和字段的伪条目。

HTREE 节点

__le32

UUID + inode 编号 + inode 生成 + 所有有效的范围 + HTREE 尾部。校验和字段设置为零。

范围

__le32

UUID + inode 编号 + inode 生成 + 整个范围块,直到校验和字段。

位图

__le32 或 __le16

UUID + 整个位图。校验和存储在组描述符中,如果组描述符大小为 32 字节(即 ^64 位),则会被截断

索引节点数

__le32

UUID + inode 编号 + inode 生成 + 整个 inode。校验和字段设置为零。每个 inode 都有自己的校验和。

组描述符

__le16

如果 metadata_csum,则为 UUID + 组号 + 整个描述符;否则如果 gdt_csum,则为 crc16(UUID + 组号 + 整个描述符)。在所有情况下,只存储较低的 16 位。

2.9. 大分配

目前,块的默认大小为 4KiB,这是大多数支持 MMU 的硬件上常用的页面大小。这很幸运,因为 ext4 代码没有准备好处理块大小超过页面大小的情况。但是,对于主要包含大型文件的文件系统,希望能够以多个块为单位分配磁盘块,以减少碎片和元数据开销。bigalloc 功能正是提供了这种能力。

bigalloc 功能 (EXT4_FEATURE_RO_COMPAT_BIGALLOC) 将 ext4 更改为使用集群分配,以便 ext4 块分配位图中的每一位都寻址块数量的 2 的幂。例如,如果文件系统主要存储 4-32 兆字节范围内的大型文件,那么设置 1 兆字节的集群大小可能是有意义的。这意味着块分配位图中的每一位现在寻址 256 个 4k 块。这会将 2T 文件系统的块分配位图的总大小从 64 兆字节缩小到 256 千字节。这也意味着一个块组寻址 32 千兆字节而不是 128 兆字节,这也减少了元数据的文件系统开销。

管理员可以在 mkfs 时设置块集群大小(它存储在超级块中的 s_log_cluster_size 字段中);从那时起,块位图跟踪集群,而不是单个块。这意味着块组的大小可以是几千兆字节(而不是仅仅 128MiB);但是,最小的分配单元会成为一个集群,而不是一个块,即使对于目录也是如此。淘宝有一个补丁集,将 “使用集群而不是块的单位” 扩展到范围树,但尚不清楚这些补丁去了哪里——它们最终演变成了 “范围树 v2”,但截至 2015 年 5 月,该代码尚未落地。

2.10. 内联数据

内联数据功能旨在处理文件的数据非常小,以至于很容易放入 inode 的情况,这(理论上)减少了磁盘块消耗并减少了寻道。如果文件小于 60 字节,则数据会以内联方式存储在 inode.i_block 中。如果文件的其余部分可以放入扩展属性空间,那么它可能会被发现为 inode 主体内的扩展属性“system.data”(“ibody EA”)。这当然会限制附加到 inode 的扩展属性的数量。如果数据大小增加到 i_block + ibody EA 之外,则会分配一个常规块并将内容移动到该块。

在更改以压缩用于存储内联数据的扩展属性键之前,应该能够在 256 字节的 inode 中存储 160 字节的数据(截至 2015 年 6 月,当 i_extra_isize 为 28 时)。在此之前,由于 inode 空间的低效使用,限制为 156 字节。

内联数据功能需要存在一个“system.data”的扩展属性,即使属性值为零长度。

2.10.1. 内联目录

i_block 的前四个字节是父目录的 inode 编号。接下来是 56 字节的空间,用于目录条目数组;请参阅 struct ext4_dir_entry。如果 inode 主体中存在“system.data”属性,则 EA 值也是 struct ext4_dir_entry 的数组。请注意,对于内联目录,i_block 和 EA 空间被视为单独的 dirent 块;目录条目不能跨越这两个块。

内联目录条目未进行校验和,因为 inode 校验和应保护所有内联数据内容。

2.11. 大型扩展属性值

为了使 ext4 能够存储不适合 inode 或附加到 inode 的单个扩展属性块中的扩展属性值,EA_INODE 功能允许我们将值存储在常规文件 inode 的数据块中。此 “EA inode” 仅从扩展属性名称索引链接,不得出现在目录条目中。inode 的 i_atime 字段用于存储 xattr 值的校验和;i_ctime/i_version 存储 64 位引用计数,这使得多个所有者 inode 之间可以共享大型 xattr 值。为了向后兼容此功能的旧版本,i_mtime/i_generation 可能 会存储对 一个 所有者 inode 的 inode 编号和 i_generation 的反向引用(在 EA inode 未被多个 inode 引用时),以验证 EA inode 是正在访问的正确 inode。

2.12. Verity 文件

ext4 支持 fs-verity,这是一种文件系统功能,它为各个只读文件提供基于 Merkle 树的哈希。大多数 fs-verity 对于所有支持它的文件系统来说都是通用的;有关 fs-verity 的文档,请参阅 Documentation/filesystems/fsverity.rst。但是,verity 元数据的磁盘布局是文件系统特定的。在 ext4 上,verity 元数据存储在文件数据本身结束后,格式如下

  • 零填充到下一个 65536 字节的边界。此填充实际上不必在磁盘上分配,即,它可能是一个空洞。

  • Merkle 树,如 Documentation/filesystems/fsverity.rst 中所述,树的层级按从根到叶的顺序存储,每层中的树块按其自然顺序存储。

  • 零填充到下一个文件系统块边界。

  • verity 描述符,如 Documentation/filesystems/fsverity.rst 中所述,可以选择附加签名 blob。

  • 零填充到下一个偏移量,该偏移量在文件系统块边界前 4 个字节处。

  • verity 描述符的大小(以字节为单位),表示为 4 字节的小端整数。

Verity inode 设置了 EXT4_VERITY_FL 标志,并且它们必须使用区段 (extent),即必须设置 EXT4_EXTENTS_FL 标志,并且必须清除 EXT4_INLINE_DATA_FL 标志。 它们可以设置 EXT4_ENCRYPT_FL 标志,在这种情况下,verity 元数据以及数据本身也会被加密。

Verity 文件不能在 verity 元数据末尾之后分配块。

Verity 和 DAX 不兼容,尝试在文件上同时设置这两个标志将会失败。