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 |
inodes |
2^32 |
2^32 |
2^32 |
2^32 |
文件系统大小 |
4TiB |
8TiB |
16TiB |
256TiB |
每块组块数 |
8,192 |
16,384 |
32,768 |
524,288 |
每块组inodes数 |
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 |
inodes |
2^32 |
2^32 |
2^32 |
2^32 |
文件系统大小 |
16ZiB |
32ZiB |
64ZiB |
1YiB |
每块组块数 |
8,192 |
16,384 |
32,768 |
524,288 |
每块组inodes数 |
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 |
注意:不使用 extents(即使用块映射)的文件必须放置在文件系统的前 2^32 个块内。使用 extents 的文件必须放置在文件系统的前 2^48 个块内。对于更大的文件系统会发生什么尚不清楚。
2.2. 布局¶
标准块组的布局大致如下(以下每个字段将在下面的单独部分中讨论):
组 0 填充 |
ext4 超级块 |
组描述符 |
保留的 GDT 块 |
数据块位图 |
inode 位图 |
inode 表 |
数据块 |
---|---|---|---|---|---|---|---|
1024 字节 |
1 个块 |
多个块 |
多个块 |
1 个块 |
1 个块 |
多个块 |
更多块 |
对于块组 0 的特殊情况,前 1024 字节是未使用的,以便安装 x86 引导扇区和其他特殊内容。超级块将从偏移量 1024 字节处开始,无论它位于哪个块(通常是块 0)。但是,如果由于某种原因块大小等于 1024,那么块 0 将被标记为已使用,超级块位于块 1。对于所有其他块组,没有填充。
ext4 驱动程序主要处理超级块和块组 0 中找到的组描述符。超级块和组描述符的冗余副本会写入磁盘上的某些块组,以防磁盘开头部分损坏,尽管并非所有块组都必然包含冗余副本(详见下一段)。如果该组没有冗余副本,则块组以数据块位图开始。另请注意,当文件系统首次格式化时,mkfs 会在块组描述符之后和块位图开始之前分配“保留 GDT 块”空间,以允许文件系统未来扩展。默认情况下,文件系统允许以原始文件系统大小的 1024 倍增加大小。
inode 表的位置由 grp.bg_inode_table_*
给出。它是一个连续的块范围,足以包含 sb.s_inodes_per_group * sb.s_inode_size
字节。
至于块组中项目的顺序,通常规定超级块和组描述符表(如果存在)将位于块组的开头。位图和 inode 表可以位于任何位置,并且位图完全有可能在 inode 表之后,或者两者位于不同的组中 (flex_bg)。剩余空间用于文件数据块、间接块映射、范围树块和扩展属性。
2.3. 弹性块组¶
从 ext4 开始,有一个名为弹性块组 (flex_bg) 的新特性。在 flex_bg 中,几个块组被绑定在一起成为一个逻辑块组;flex_bg 中第一个块组的位图空间和 inode 表空间被扩展,以包含 flex_bg 中所有其他块组的位图和 inode 表。例如,如果 flex_bg 大小为 4,那么组 0 将(按顺序)包含超级块、组描述符、组 0-3 的数据块位图、组 0-3 的 inode 位图、组 0-3 的 inode 表,并且组 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),该特性已包含在 ext3 的所有 2.6 版本中。启用 META_BG 特性后,ext4 文件系统被划分为许多元块组。每个元块组都是一个块组的集群,其组描述符结构可以存储在一个磁盘块中。对于块大小为 4 KB 的 ext4 文件系统,单个元块组分区包含 64 个块组,即 8 GiB 的磁盘空间。元块组特性将组描述符的位置从整个文件系统的第一个拥挤的块组移动到每个元块组自身的第一个组中。备份位于每个元块组的第二个和最后一个组中。这将 2^21 的最大块组限制提高到硬性限制 2^32,从而支持 512PiB 的文件系统。
文件系统格式的更改取代了当前超级块后跟可变长度块组描述符集的方案。相反,超级块和单个块组描述符块放置在元块组的第一个、第二个和最后一个块组的开头。元块组是可以通过单个块组描述符块描述的块组集合。由于块组描述符结构的大小为 64 字节,因此对于块大小为 1KB 的文件系统,一个元块组包含 16 个块组;对于块大小为 4KB 的文件系统,包含 64 个块组。文件系统可以使用这种新的块组描述符布局创建,或者现有文件系统可以在线调整大小,超级块中的 s_first_meta_bg 字段将指示第一个使用这种新布局的块组。
请参阅关于块和 inode 位图部分中有关 BLOCK_UNINIT
的重要说明。
2.5. 惰性块组初始化¶
ext4 的一个新特性是三个块组描述符标志,它们使 mkfs 能够跳过初始化块组元数据的其他部分。具体来说,INODE_UNINIT 和 BLOCK_UNINIT 标志意味着该组的 inode 和块位图可以计算得出,因此磁盘上的位图块不会被初始化。这通常发生在空块组或只包含固定位置块组元数据的块组中。INODE_ZEROED 标志意味着 inode 表已初始化;mkfs 将取消设置此标志并依赖内核在后台初始化 inode 表。
通过不向位图和 inode 表写入零,mkfs 时间大大减少。请注意,特性标志是 RO_COMPAT_GDT_CSUM,但 dumpe2fs 输出将其打印为“uninit_bg”。它们是同一回事。
2.6. 特殊 inode¶
ext4 为特殊功能保留了一些 inode,如下所示:
inode 号 |
用途 |
---|---|
0 |
不存在;没有 inode 0。 |
1 |
坏块列表。 |
2 |
根目录。 |
3 |
用户配额。 |
4 |
组配额。 |
5 |
引导加载程序。 |
6 |
取消删除目录。 |
7 |
保留组描述符 inode。(“resize inode”) |
8 |
日志 inode。 |
9 |
“排除”inode,用于快照(?) |
10 |
副本 inode,用于某些非上游特性? |
11 |
传统的第一个非保留 inode。通常这是 lost+found 目录。请参阅超级块中的 s_first_ino。 |
请注意,还有一些从非保留 inode 号分配的 inode 用于其他文件系统特性,这些特性未从标准目录层次结构中引用。它们通常从超级块中引用。它们是:
超级块字段 |
描述 |
---|---|
s_lpf_ino |
lost+found 目录的 inode 号。 |
s_prj_quota_inum |
跟踪项目配额的配额文件的 inode 号 |
s_orphan_file_inum |
跟踪孤立 inode 的文件的 inode 号。 |
2.7. 块和 inode 分配策略¶
ext4 认识到(无论如何都比 ext3 更好)数据局部性通常是文件系统的一个理想特性。在旋转磁盘上,将相关块彼此靠近可以减少磁头致动器和磁盘访问数据块所需的移动量,从而加快磁盘 I/O。在 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 位。启用 64bit 特性会增加数据结构大小,以便为许多数据结构存储完整的 32 位校验和。但是,现有的 32 位文件系统无法扩展以启用 64bit 模式,至少在没有实验性 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 字节(即 ^64bit)则被截断。 |
inodes |
__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 字段中);从那时起,块位图跟踪的是簇,而不是单个块。这意味着块组可以达到几 GB 大小(而不仅仅是 128MiB);但是,即使对于目录,最小分配单位也变为簇,而不是块。淘宝曾有一个补丁集,旨在将“使用簇作为单位而不是块”扩展到范围树,但目前尚不清楚这些补丁去了哪里——它们最终演变为“范围树 v2”,但截至 2015 年 5 月,该代码尚未合并。
2.10. 内联数据¶
内联数据特性旨在处理文件数据非常小以至于可以轻松放入 inode 的情况,这(理论上)减少了磁盘块消耗并减少了寻道。如果文件小于 60 字节,则数据以内联方式存储在 inode.i_block
中。如果文件的其余部分可以放入扩展属性空间,那么它可能会作为 inode 正文(“ibody EA”)中的扩展属性“system.data”找到。这当然限制了可以附加到 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 位引用计数,这使得大型 xattr 值可以在多个拥有 inode 之间共享。为了向后兼容此特性的旧版本,i_mtime/i_generation 可能 存储对 一个 拥有 inode 的 inode 号和 i_generation 的反向引用(在 EA inode 未被多个 inode 引用的情况下),以验证访问的 EA inode 是否正确。
2.12. Verity 文件¶
ext4 支持 fs-verity,这是一项文件系统特性,为单个只读文件提供基于 Merkle 树的哈希。fs-verity 的大部分内容与所有支持它的文件系统通用;请参阅 Documentation/filesystems/fsverity.rst 以获取 fs-verity 文档。然而,verity 元数据的磁盘布局是文件系统特有的。在 ext4 上,verity 元数据存储在文件数据本身之后,格式如下:
填充零到下一个 65536 字节边界。此填充实际上不必在磁盘上分配,即它可能是一个空洞。
Merkle 树,如 Documentation/filesystems/fsverity.rst 中所述,树级别按从根到叶的顺序存储,每个级别内的树块按其自然顺序存储。
填充零到下一个文件系统块边界。
verity 描述符,如 Documentation/filesystems/fsverity.rst 中所述,可选择附加签名 blob。
填充零到距离文件系统块边界前 4 字节的下一个偏移量。
verity 描述符的大小(以字节为单位),表示为 4 字节小端整数。
Verity inode 设置了 EXT4_VERITY_FL,并且它们必须使用 extents,即 EXT4_EXTENTS_FL 必须设置,EXT4_INLINE_DATA_FL 必须清除。它们可以设置 EXT4_ENCRYPT_FL,在这种情况下,verity 元数据和数据本身都会被加密。
Verity 文件不能在 verity 元数据结束之后分配块。
Verity 和 DAX 不兼容,尝试在一个文件上同时设置这两个标志将失败。
2.13. 原子块写入¶
2.13.1. 引言¶
原子(非撕裂)块写入确保要么整个写入都提交到磁盘,要么都不提交。这可以防止在断电或系统崩溃期间发生“撕裂写入”。ext4 文件系统支持对具有 extents 的常规文件进行原子写入(仅限直接 I/O),前提是底层存储设备支持硬件原子写入。这通过以下两种方式支持:
单文件系统块原子写入:EXT4 从 v6.13 开始支持单文件系统块的原子写入操作。在此模式下,原子写入单元的最小和最大大小均设置为文件系统块大小。例如,在 64KB 页面大小的系统上,使用 16KB 文件系统块大小进行 16KB 的原子写入是可能的。
使用 Bigalloc 的多文件系统块原子写入:EXT4 现在还支持使用名为 bigalloc 的特性进行跨多个文件系统块的原子写入。原子写入单元的最小和最大大小由文件系统块大小和簇大小决定,具体取决于底层设备支持的原子写入单元限制。
2.13.2. 要求¶
ext4 中原子写入的基本要求:
必须启用 extents 特性(ext4 默认启用)
底层块设备必须支持原子写入
对于单文件系统块原子写入:
具有适当块大小(最大为页面大小)的文件系统
对于多文件系统块原子写入:
必须启用 bigalloc 特性
必须正确配置簇大小
注意:EXT4 不支持基于软件或 COW 的原子写入,这意味着 ext4 上的原子写入仅在底层存储设备支持的情况下才受支持。
2.13.3. 多文件系统块实现细节¶
bigalloc 特性将 ext4 更改为以多个文件系统块(也称为簇)为单位进行分配。使用 bigalloc,块位图中的每个位都表示一个簇(2 的幂次方的块数),而不是单个文件系统块。EXT4 支持使用 bigalloc 进行多文件系统块原子写入,但受以下约束。最小原子写入大小是文件系统块大小和最小硬件原子写入单位中的较大者;最大原子写入大小是 bigalloc 簇大小和最大硬件原子写入单位中的较小者。Bigalloc 确保所有分配都与簇大小对齐,如果分区/逻辑卷的起始本身正确对齐,则满足硬件设备的 LBA 对齐要求。
以下是 bigalloc 中用于原子写入的块分配策略:
对于具有完全映射范围的区域,无需额外工作
对于追加写入,分配一个新的映射范围
对于完全是空洞的区域,创建未写入的范围
对于大型未写入范围,该范围会被拆分为两个适当请求大小的未写入范围
对于混合映射区域(空洞、未写入范围或已映射范围的组合),
ext4_map_blocks()
会在一个循环中以 EXT4_GET_BLOCKS_ZERO 标志调用,通过向其写入零并将范围内的任何未写入范围转换为已写入,从而将该区域转换为单个连续映射范围。
注意:在单个连续的底层范围上写入,无论是已映射还是未写入,本身都没有问题。然而,在执行原子写入时,必须避免写入混合映射区域(即包含已映射和未写入范围组合的区域)。
原因是,通过 pwritev2() 并带有 RWF_ATOMIC 标志发出的原子写入,要求要么所有数据都写入,要么都不写入。在写入操作期间发生系统崩溃或意外断电的情况下,受影响的区域(稍后读取时)必须反映完整的旧数据或完整的最新数据,但绝不能是两者的混合。
为了强制执行此保证,我们确保在写入任何数据之前,写入目标由单个连续的范围支持。这至关重要,因为 ext4 会将未写入范围转换为已写入范围的操作推迟到 I/O 完成路径(通常在 ->end_io() 中)。如果允许写入操作在混合映射区域(包含已映射和未写入范围)上进行,并且在写入过程中发生故障,则系统在重新启动后可能会观察到部分更新的区域,即已映射区域上的新数据,以及从未标记为已写入的未写入范围上的陈旧(旧)数据。这违反了原子性或撕裂写入防止保证。
为防止此类撕裂写入,ext4 通过 ext4_map_blocks_atomic()
在 ext4_iomap_alloc
中主动为整个请求区域分配单个连续范围。如果分配是在混合映射上完成的,EXT4 还会强制提交当前的日志事务。这确保了在执行实际写入 I/O 之前,此范围内的任何待处理元数据更新(例如未写入到已写入范围的转换)与文件数据块处于一致状态。如果提交失败,则必须中止整个 I/O,以防止任何可能的撕裂写入。只有在此步骤之后,实际数据写入操作才由 iomap 执行。
2.13.4. 处理跨叶块的拆分范围¶
存在一种特殊边缘情况,即我们有逻辑上和物理上连续的范围,但它们存储在磁盘范围树的不同叶节点中。这发生的原因是磁盘范围树的合并只发生在叶块内,除了两级树可以完全合并并折叠到 inode 中的情况。如果存在这种布局,并且在最坏的情况下,由于内存压力导致范围状态缓存条目被回收,ext4_map_blocks()
可能永远不会为这些拆分的叶范围返回单个连续范围。
为解决此边缘情况,添加了一个新的获取块标志 EXT4_GET_BLOCKS_QUERY_LEAF_BLOCKS flag
以增强 ext4_map_query_blocks()
的查找行为。
这个新的获取块标志允许 ext4_map_blocks()
首先检查范围状态缓存中是否存在完整范围的条目。如果不存在,它会使用 ext4_map_query_blocks()
查询磁盘范围树。如果找到的范围位于叶节点的末尾,它会探测下一个逻辑块 (lblk) 以检测相邻叶中的连续范围。
目前,为了保持效率,只查询一个额外的叶块,因为原子写入通常受限于较小的尺寸(例如 [块大小, 簇大小])。
2.13.5. 处理日志事务¶
为支持多文件系统块原子写入,我们确保在以下期间保留足够的日志信用:
在
ext4_iomap_alloc()
中的块分配时间。我们首先查询底层请求范围内是否存在混合映射。如果存在,则我们最多保留m_len
的信用,假设每个交替块都可以是一个未写入范围后跟一个空洞。在调用
->end_io()
期间,我们确保启动单个事务以进行未写入到已写入的转换。转换循环主要仅用于处理跨叶块的拆分范围。
2.14. 如何操作¶
2.14.1. 创建支持原子写入的文件系统¶
首先检查块设备支持的原子写入单位。详见 硬件支持。
对于具有较大块大小(在块大小 < 页面大小的系统上)的单文件系统块原子写入
# Create an ext4 filesystem with a 16KB block size
# (requires page size >= 16KB)
mkfs.ext4 -b 16384 /dev/device
对于使用 bigalloc 的多文件系统块原子写入
# Create an ext4 filesystem with bigalloc and 64KB cluster size
mkfs.ext4 -F -O bigalloc -b 4096 -C 65536 /dev/device
其中 -b
指定块大小,-C
指定簇大小(以字节为单位),-O bigalloc
启用 bigalloc 特性。
2.14.2. 应用程序接口¶
应用程序可以使用带 RWF_ATOMIC
标志的 pwritev2()
系统调用执行原子写入
pwritev2(fd, iov, iovcnt, offset, RWF_ATOMIC);
写入必须与文件系统的块大小对齐,且不能超过文件系统的最大原子写入单元大小。详见 generic_atomic_write_valid()
。
带 STATX_WRITE_ATOMIC
标志的 statx()
系统调用可提供以下详细信息:
stx_atomic_write_unit_min
:原子写入请求的最小大小。
stx_atomic_write_unit_max
:原子写入请求的最大大小。
stx_atomic_write_segments_max
:分段上限。可以聚合到一个写入操作中的独立内存缓冲区数量(例如,IOV_ITER 的 iovcnt 参数)。目前,此值始终设置为一。
如果支持原子写入,statx->attributes
中的 STATX_ATTR_WRITE_ATOMIC 标志将被设置。
2.15. 硬件支持¶
底层存储设备必须支持原子写入操作。现代 NVMe 和 SCSI 设备通常提供此功能。Linux 内核通过 sysfs 暴露此信息:
/sys/block/<device>/queue/atomic_write_unit_min
- 最小原子写入大小/sys/block/<device>/queue/atomic_write_unit_max
- 最大原子写入大小
这些属性的非零值表示设备支持原子写入。
2.16. 另请参阅¶
大分配 (Bigalloc) - 关于 bigalloc 特性的文档
块和 inode 分配策略 - 关于 ext4 中块分配的文档
6.13 版本中对原子块写入的支持:https://lwn.net/Articles/1009298/