3. 全局结构

文件系统被分片成多个块组,每个块组在固定位置都有静态元数据。

3.1. 超级块

超级块记录了有关封闭文件系统的各种信息,例如块计数、inode 计数、支持的功能、维护信息等等。

如果设置了 sparse_super 功能标志,则超级块和组描述符的冗余副本仅保留在组号为 0 或 3、5 或 7 的幂的组中。如果未设置该标志,则冗余副本保留在所有组中。

超级块校验和是根据超级块结构计算的,其中包括 FS UUID。

ext4 超级块在 struct ext4_super_block 中布局如下

偏移

大小

名称

描述

0x0

__le32

s_inodes_count

inode 总数。

0x4

__le32

s_blocks_count_lo

块总数。

0x8

__le32

s_r_blocks_count_lo

只有超级用户才能分配此数量的块。

0xC

__le32

s_free_blocks_count_lo

可用块数。

0x10

__le32

s_free_inodes_count

可用 inode 数。

0x14

__le32

s_first_data_block

第一个数据块。对于 1k 块文件系统,这必须至少为 1,对于所有其他块大小,通常为 0。

0x18

__le32

s_log_block_size

块大小为 2 ^ (10 + s_log_block_size)。

0x1C

__le32

s_log_cluster_size

如果启用了 bigalloc,则簇大小为 2 ^ (10 + s_log_cluster_size) 块。否则,s_log_cluster_size 必须等于 s_log_block_size。

0x20

__le32

s_blocks_per_group

每个组的块数。

0x24

__le32

s_clusters_per_group

如果启用了 bigalloc,则每个组的簇数。否则,s_clusters_per_group 必须等于 s_blocks_per_group。

0x28

__le32

s_inodes_per_group

每个组的 inode 数。

0x2C

__le32

s_mtime

挂载时间,自纪元以来的秒数。

0x30

__le32

s_wtime

写入时间,自纪元以来的秒数。

0x34

__le16

s_mnt_count

自上次 fsck 以来挂载的次数。

0x36

__le16

s_max_mnt_count

超过此挂载次数需要 fsck。

0x38

__le16

s_magic

魔术签名,0xEF53

0x3A

__le16

s_state

文件系统状态。有关更多信息,请参见 super_state

0x3C

__le16

s_errors

检测到错误时的行为。有关更多信息,请参见 super_errors

0x3E

__le16

s_minor_rev_level

次修订级别。

0x40

__le32

s_lastcheck

上次检查的时间,自纪元以来的秒数。

0x44

__le32

s_checkinterval

检查之间的最大时间,以秒为单位。

0x48

__le32

s_creator_os

创建者操作系统。有关更多信息,请参见表 super_creator

0x4C

__le32

s_rev_level

修订级别。有关更多信息,请参见表 super_revision

0x50

__le16

s_def_resuid

保留块的默认 uid。

0x52

__le16

s_def_resgid

保留块的默认 gid。

这些字段仅适用于 EXT4_DYNAMIC_REV 超级块。

注意:兼容功能集和不兼容功能集之间的区别在于,如果内核不知道不兼容功能集中设置的位,则应拒绝挂载文件系统。

e2fsck 的要求更为严格;如果它不知道兼容或不兼容功能集中的功能,则必须中止,并且不要试图干预它不理解的事情...

0x54

__le32

s_first_ino

第一个非保留的 inode。

0x58

__le16

s_inode_size

inode 结构的大小,以字节为单位。

0x5A

__le16

s_block_group_nr

此超级块的块组编号。

0x5C

__le32

s_feature_compat

兼容功能集标志。即使内核不理解标志,仍然可以读取/写入此 fs;fsck 不应该这样做。有关更多信息,请参见 super_compat 表。

0x60

__le32

s_feature_incompat

不兼容功能集。如果内核或 fsck 不理解这些位中的一位,它应该停止。有关更多信息,请参见 super_incompat 表。

0x64

__le32

s_feature_ro_compat

只读兼容功能集。如果内核不理解这些位中的一位,它仍然可以只读挂载。有关更多信息,请参见 super_rocompat 表。

0x68

__u8

s_uuid[16]

卷的 128 位 UUID。

0x78

char

s_volume_name[16]

卷标。

0x88

char

s_last_mounted[64]

文件系统上次挂载的目录。

0xC8

__le32

s_algorithm_usage_bitmap

用于压缩(在 e2fsprogs/Linux 中未使用)

性能提示。只有在启用 EXT4_FEATURE_COMPAT_DIR_PREALLOC 标志时,才应发生目录预分配。

0xCC

__u8

s_prealloc_blocks

#. 要尝试为 ... 文件预分配的块?(在 e2fsprogs/Linux 中未使用)

0xCD

__u8

s_prealloc_dir_blocks

#. 为目录预分配的块。(在 e2fsprogs/Linux 中未使用)

0xCE

__le16

s_reserved_gdt_blocks

为将来的文件系统扩展保留的 GDT 条目数。

只有在设置了 EXT4_FEATURE_COMPAT_HAS_JOURNAL 时,日志记录支持才有效。

0xD0

__u8

s_journal_uuid[16]

日志超级块的 UUID

0xE0

__le32

s_journal_inum

日志文件的 inode 号。

0xE4

__le32

s_journal_dev

如果设置了外部日志功能标志,则为日志文件的设备号。

0xE8

__le32

s_last_orphan

要删除的孤立 inode 列表的开头。

0xEC

__le32

s_hash_seed[4]

HTREE 哈希种子。

0xFC

__u8

s_def_hash_version

用于目录哈希的默认哈希算法。有关更多信息,请参见 super_def_hash

0xFD

__u8

s_jnl_backup_type

如果此值为 0 或 EXT3_JNL_BACKUP_BLOCKS (1),则 s_jnl_blocks 字段包含 inode 的 i_block[] 数组和 i_size 的重复副本。

0xFE

__le16

s_desc_size

如果设置了 64 位不兼容功能标志,则组描述符的大小,以字节为单位。

0x100

__le32

s_default_mount_opts

默认挂载选项。有关更多信息,请参见 super_mountopts 表。

0x104

__le32

s_first_meta_bg

如果启用了 meta_bg 功能,则为第一个元块块组。

0x108

__le32

s_mkfs_time

文件系统创建时间,自纪元以来的秒数。

0x10C

__le32

s_jnl_blocks[17]

日志 inode 的 i_block[] 数组的备份副本,在前 15 个元素中,以及第 16 个和第 17 个元素中的 i_size_high 和 i_size。

只有在设置了 EXT4_FEATURE_COMPAT_64BIT 时,64 位支持才有效。

0x150

__le32

s_blocks_count_hi

块计数的高 32 位。

0x154

__le32

s_r_blocks_count_hi

保留块计数的高 32 位。

0x158

__le32

s_free_blocks_count_hi

可用块计数的高 32 位。

0x15C

__le16

s_min_extra_isize

所有 inode 至少有 # 个字节。

0x15E

__le16

s_want_extra_isize

新的 inode 应该保留 # 个字节。

0x160

__le32

s_flags

杂项标志。有关更多信息,请参见 super_flags 表。

0x164

__le16

s_raid_stride

RAID 条带大小。这是在移动到下一个磁盘之前从磁盘读取或写入的逻辑块数。这会影响文件系统元数据的放置,这将有望使 RAID 存储更快。

0x166

__le16

s_mmp_interval

#. 在多重挂载防止 (MMP) 检查中等待的秒数。理论上,MMP 是一种在超级块中记录哪个主机和设备已挂载文件系统的机制,以防止多次挂载。此功能似乎没有实现...

0x168

__le64

s_mmp_block

用于多重挂载保护数据的块号。

0x170

__le32

s_raid_stripe_width

RAID 条带宽度。这是在返回到当前磁盘之前从磁盘读取或写入的逻辑块数。块分配器使用它来尝试减少 RAID5/6 中的读取-修改-写入操作的数量。

0x174

__u8

s_log_groups_per_flex

灵活块组的大小为 2 ^ s_log_groups_per_flex

0x175

__u8

s_checksum_type

元数据校验和算法类型。唯一有效的值是 1 (crc32c)。

0x176

__le16

s_reserved_pad

0x178

__le64

s_kbytes_written

在此文件系统的生命周期内写入的千字节数。

0x180

__le32

s_snapshot_inum

活动快照的 inode 号。(在 e2fsprogs/Linux 中未使用。)

0x184

__le32

s_snapshot_id

活动快照的顺序 ID。(在 e2fsprogs/Linux 中未使用。)

0x188

__le64

s_snapshot_r_blocks_count

为活动快照的未来使用保留的块数。(在 e2fsprogs/Linux 中未使用。)

0x190

__le32

s_snapshot_list

磁盘快照列表头部的 inode 号。(在 e2fsprogs/Linux 中未使用。)

0x194

__le32

s_error_count

看到的错误数量。

0x198

__le32

s_first_error_time

首次发生错误的时间,自 Unix 纪元以来的秒数。

0x19C

__le32

s_first_error_ino

首次错误涉及的 inode。

0x1A0

__le64

s_first_error_block

首次错误涉及的块号。

0x1A8

__u8

s_first_error_func[32]

发生错误的函数名称。

0x1C8

__le32

s_first_error_line

发生错误的行号。

0x1CC

__le32

s_last_error_time

最近一次错误的时间,自 Unix 纪元以来的秒数。

0x1D0

__le32

s_last_error_ino

最近一次错误涉及的 inode。

0x1D4

__le32

s_last_error_line

最近一次发生错误的行号。

0x1D8

__le64

s_last_error_block

最近一次错误涉及的块号。

0x1E0

__u8

s_last_error_func[32]

最近一次发生错误的函数名称。

0x200

__u8

s_mount_opts[64]

挂载选项的 ASCIIZ 字符串。

0x240

__le32

s_usr_quota_inum

用户配额文件的 inode 号。

0x244

__le32

s_grp_quota_inum

配额文件的 inode 号。

0x248

__le32

s_overhead_blocks

文件系统中开销块/簇的数量。(嗯?此字段始终为零,这意味着内核会动态计算它。)

0x24C

__le32

s_backup_bgs[2]

包含超级块备份的块组(如果使用 sparse_super2)。

0x254

__u8

s_encrypt_algos[4]

正在使用的加密算法。在任何时候最多可以使用四种算法;有效的算法代码在下面的 super_encrypt 表中给出。

0x258

__u8

s_encrypt_pw_salt[16]

用于加密的 string2key 算法的盐值。

0x268

__le32

s_lpf_ino

lost+found 的 inode 号。

0x26C

__le32

s_prj_quota_inum

跟踪项目配额的 inode。

0x270

__le32

s_checksum_seed

用于 metadata_csum 计算的校验和种子。该值为 crc32c(~0, $orig_fs_uuid)。

0x274

__u8

s_wtime_hi

s_wtime 字段的高 8 位。

0x275

__u8

s_mtime_hi

s_mtime 字段的高 8 位。

0x276

__u8

s_mkfs_time_hi

s_mkfs_time 字段的高 8 位。

0x277

__u8

s_lastcheck_hi

s_lastcheck 字段的高 8 位。

0x278

__u8

s_first_error_time_hi

s_first_error_time 字段的高 8 位。

0x279

__u8

s_last_error_time_hi

s_last_error_time 字段的高 8 位。

0x27A

__u8

s_pad[2]

零填充。

0x27C

__le16

s_encoding

文件名字符集编码。

0x27E

__le16

s_encoding_flags

文件名字符集编码标志。

0x280

__le32

s_orphan_file_inum

孤立文件 inode 号。

0x284

__le32

s_reserved[94]

填充到块的末尾。

0x3FC

__le32

s_checksum

超级块校验和。

超级块状态是以下各项的组合

描述

0x0001

已干净卸载

0x0002

检测到错误

0x0004

正在恢复孤立项

超级块错误策略是以下之一

描述

1

继续

2

重新挂载为只读

3

内核崩溃

文件系统创建者是以下之一

描述

0

Linux

1

Hurd

2

Masix

3

FreeBSD

4

Lites

超级块修订版本是以下之一

描述

0

原始格式

1

具有动态 inode 大小的 v2 格式

请注意,EXT4_DYNAMIC_REV 指的是修订版本 1 或更新的文件系统。

超级块兼容特性字段是以下各项的任意组合

描述

0x1

目录预分配 (COMPAT_DIR_PREALLOC)。

0x2

“imagic inodes”。从代码中不清楚它做了什么 (COMPAT_IMAGIC_INODES)。

0x4

具有日志 (COMPAT_HAS_JOURNAL)。

0x8

支持扩展属性 (COMPAT_EXT_ATTR)。

0x10

具有用于文件系统扩展的保留 GDT 块 (COMPAT_RESIZE_INODE)。需要 RO_COMPAT_SPARSE_SUPER。

0x20

具有目录索引 (COMPAT_DIR_INDEX)。

0x40

“Lazy BG”。不在 Linux 内核中,似乎用于未初始化的块组? (COMPAT_LAZY_BG)

0x80

“排除 inode”。未使用。(COMPAT_EXCLUDE_INODE)。

0x100

“排除位图”。似乎用于指示与快照相关的排除位图的存在?在内核中未定义或在 e2fsprogs 中未使用 (COMPAT_EXCLUDE_BITMAP)。

0x200

稀疏超级块,v2。如果设置此标志,则 SB 字段 s_backup_bgs 指向包含备份超级块的两个块组 (COMPAT_SPARSE_SUPER2)。

0x400

支持快速提交。尽管快速提交块向后不兼容,但快速提交块并非始终存在于日志中。如果快速提交块存在于日志中,则设置 JBD2 不兼容特性 (JBD2_FEATURE_INCOMPAT_FAST_COMMIT) (COMPAT_FAST_COMMIT)。

0x1000

已分配孤立文件。这是一个特殊文件,用于更有效地跟踪未链接但仍处于打开状态的 inode。当文件中可能存在任何条目时,我们还会设置适当的 rocompat 特性 (RO_COMPAT_ORPHAN_PRESENT)。

超级块不兼容特性字段是以下各项的任意组合

描述

0x1

压缩 (INCOMPAT_COMPRESSION)。

0x2

目录条目记录文件类型。请参阅下面的 ext4_dir_entry_2 (INCOMPAT_FILETYPE)。

0x4

文件系统需要恢复 (INCOMPAT_RECOVER)。

0x8

文件系统具有单独的日志设备 (INCOMPAT_JOURNAL_DEV)。

0x10

元块组。请参阅前面有关此功能的讨论 (INCOMPAT_META_BG)。

0x40

此文件系统中的文件使用 extent (INCOMPAT_EXTENTS)。

0x80

启用 2^64 块的文件系统大小 (INCOMPAT_64BIT)。

0x100

多重挂载保护 (INCOMPAT_MMP)。

0x200

灵活块组。请参阅前面有关此功能的讨论 (INCOMPAT_FLEX_BG)。

0x400

Inode 可用于存储大型扩展属性值 (INCOMPAT_EA_INODE)。

0x1000

目录条目中的数据 (INCOMPAT_DIRDATA)。(未实现?)

0x2000

元数据校验和种子存储在超级块中。此功能使管理员能够在文件系统挂载时更改 metadata_csum 文件系统的 UUID;如果没有此功能,则校验和定义需要重写所有元数据块 (INCOMPAT_CSUM_SEED)。

0x4000

大型目录 >2GB 或 3 级 htree (INCOMPAT_LARGEDIR)。在此功能之前,目录的大小不能超过 4GiB,并且 htree 的深度不能超过 2 级。如果启用此功能,则目录可以大于 4GiB,并且最大 htree 深度为 3。

0x8000

inode 中的数据 (INCOMPAT_INLINE_DATA)。

0x10000

文件系统上存在加密的 inode。(INCOMPAT_ENCRYPT)。

超级块只读兼容特性字段是以下各项的任意组合

描述

0x1

稀疏超级块。请参阅前面有关此功能的讨论 (RO_COMPAT_SPARSE_SUPER)。

0x2

此文件系统已用于存储大于 2GiB 的文件 (RO_COMPAT_LARGE_FILE)。

0x4

未在内核或 e2fsprogs 中使用 (RO_COMPAT_BTREE_DIR)。

0x8

此文件系统中的文件大小以逻辑块(而不是 512 字节扇区)为单位表示。这意味着一个非常大的文件! (RO_COMPAT_HUGE_FILE)

0x10

组描述符具有校验和。除了检测损坏外,这对于使用未初始化的组进行延迟格式化也很有用 (RO_COMPAT_GDT_CSUM)。

0x20

指示旧的 ext3 32,000 子目录限制不再适用 (RO_COMPAT_DIR_NLINK)。如果目录的 i_links_count 递增超过 64,999,则将其设置为 1。

0x40

指示此文件系统上存在大型 inode (RO_COMPAT_EXTRA_ISIZE)。

0x80

此文件系统具有快照 (RO_COMPAT_HAS_SNAPSHOT)。

0x100

配额 (RO_COMPAT_QUOTA)。

0x200

此文件系统支持“bigalloc”,这意味着文件 extent 以簇(块)而不是块为单位跟踪 (RO_COMPAT_BIGALLOC)。

0x400

此文件系统支持元数据校验和。(RO_COMPAT_METADATA_CSUM;暗示 RO_COMPAT_GDT_CSUM,但不能设置 GDT_CSUM)

0x800

文件系统支持副本。此功能既不在内核中也不在 e2fsprogs 中。(RO_COMPAT_REPLICA)

0x1000

只读文件系统映像;内核不会以读写方式挂载此映像,并且大多数工具会拒绝写入该映像。(RO_COMPAT_READONLY)

0x2000

文件系统跟踪项目配额。(RO_COMPAT_PROJECT)

0x8000

文件系统上可能存在 Verity inode。(RO_COMPAT_VERITY)

0x10000

指示孤立文件可能具有有效的孤立条目,因此我们需要在挂载文件系统时清理它们 (RO_COMPAT_ORPHAN_PRESENT)。

s_def_hash_version 字段是以下之一

描述

0x0

旧版。

0x1

半 MD4。

0x2

Tea。

0x3

旧版,未签名。

0x4

半 MD4,未签名。

0x5

Tea,未签名。

s_default_mount_opts 字段是以下各项的任意组合

描述

0x0001

在(重新)挂载时打印调试信息。(EXT4_DEFM_DEBUG)

0x0002

新文件采用包含目录的 gid(而不是当前进程的 fsgid)。(EXT4_DEFM_BSDGROUPS)

0x0004

支持用户空间提供的扩展属性。(EXT4_DEFM_XATTR_USER)

0x0008

支持 POSIX 访问控制列表 (ACL)。(EXT4_DEFM_ACL)

0x0010

不支持 32 位 UID。(EXT4_DEFM_UID16)

0x0020

所有数据和元数据都提交到日志。(EXT4_DEFM_JMODE_DATA)

0x0040

所有数据在元数据提交到日志之前刷新到磁盘。(EXT4_DEFM_JMODE_ORDERED)

0x0060

不保留数据排序;数据可以在元数据写入后写入。(EXT4_DEFM_JMODE_WBACK)

0x0100

禁用写入刷新。(EXT4_DEFM_NOBARRIER)

0x0200

跟踪文件系统中哪些块是元数据,因此不应将其用作数据块。此选项将在 3.18 上默认启用,希望如此。(EXT4_DEFM_BLOCK_VALIDITY)

0x0400

启用 DISCARD 支持,其中会告知存储设备有关块变为未使用的信息。(EXT4_DEFM_DISCARD)

0x0800

禁用延迟分配。(EXT4_DEFM_NODELALLOC)

s_flags 字段是以下各项的任意组合

描述

0x0001

正在使用签名的目录哈希。

0x0002

正在使用未签名的目录哈希。

0x0004

用于测试开发代码。

s_encrypt_algos 列表可以包含以下任何项

描述

0

无效算法 (ENCRYPTION_MODE_INVALID)。

1

XTS 模式下的 256 位 AES (ENCRYPTION_MODE_AES_256_XTS)。

2

GCM 模式下的 256 位 AES 加密 (ENCRYPTION_MODE_AES_256_GCM)。

3

CBC 模式下的 256 位 AES 加密 (ENCRYPTION_MODE_AES_256_CBC)。

超级块的总大小为 1024 字节。

3.2. 块组描述符

文件系统上的每个块组都有一个与之关联的描述符。如上面的“布局”部分所述,组描述符(如果存在)是块组中的第二个项。标准配置是每个块组包含块组描述符表的完整副本,除非设置了 sparse_super 功能标志。

请注意,组描述符如何记录位图和 inode 表的位置(即它们可以浮动)。这意味着在块组中,只有超级块和组描述符表是固定位置的数据结构。 flex_bg 机制利用此属性将多个块组分组到一个弹性组中,并将所有组的位图和 inode 表布局到弹性组的第一个组中的一个长运行中。

如果设置了 meta_bg 功能标志,则会将多个块组分组到一个元组中。请注意,在 meta_bg 的情况下,较大的元组中的前两个和最后两个块组仅包含元组内组的组描述符。

flex_bg 和 meta_bg 似乎不是互斥的功能。

在 ext2、ext3 和 ext4(未启用 64 位功能时)中,块组描述符仅长 32 个字节,因此在 bg_checksum 处结束。在启用了 64 位功能的 ext4 文件系统上,块组描述符扩展到至少下面描述的 64 个字节;大小存储在超级块中。

如果设置了 gdt_csum 并且未设置 metadata_csum,则块组校验和是 FS UUID、组号和组描述符结构的 crc16。如果设置了 metadata_csum,则块组校验和是 FS UUID、组号和组描述符结构的校验和的低 16 位。块和 inode 位图校验和都是针对 FS UUID、组号和整个位图计算的。

块组描述符在 struct ext4_group_desc 中布局。

偏移

大小

名称

描述

0x0

__le32

bg_block_bitmap_lo

块位图位置的低 32 位。

0x4

__le32

bg_inode_bitmap_lo

inode 位图位置的低 32 位。

0x8

__le32

bg_inode_table_lo

inode 表位置的低 32 位。

0xC

__le16

bg_free_blocks_count_lo

空闲块计数的低 16 位。

0xE

__le16

bg_free_inodes_count_lo

空闲 inode 计数的低 16 位。

0x10

__le16

bg_used_dirs_count_lo

目录计数的低 16 位。

0x12

__le16

bg_flags

块组标志。请参阅下面的 bgflags 表。

0x14

__le32

bg_exclude_bitmap_lo

快照排除位图位置的低 32 位。

0x18

__le16

bg_block_bitmap_csum_lo

块位图校验和的低 16 位。

0x1A

__le16

bg_inode_bitmap_csum_lo

inode 位图校验和的低 16 位。

0x1C

__le16

bg_itable_unused_lo

未使用的 inode 计数的低 16 位。如果设置,则无需扫描超过此组的 inode 表中的第 (sb.s_inodes_per_group - gdt.bg_itable_unused) 个条目。

0x1E

__le16

bg_checksum

组描述符校验和;如果设置了 RO_COMPAT_GDT_CSUM 功能,则为 crc16(sb_uuid+group_num+bg_desc),如果设置了 RO_COMPAT_METADATA_CSUM 功能,则为 crc32c(sb_uuid+group_num+bg_desc) & 0xFFFF。在计算 crc16 校验和时跳过 bg_desc 中的 bg_checksum 字段,如果使用 crc32c 校验和,则将其设置为零。

仅当启用了 64 位功能且 s_desc_size > 32 时,这些字段才存在。

0x20

__le32

bg_block_bitmap_hi

块位图位置的高 32 位。

0x24

__le32

bg_inode_bitmap_hi

inode 位图位置的高 32 位。

0x28

__le32

bg_inode_table_hi

inode 表位置的高 32 位。

0x2C

__le16

bg_free_blocks_count_hi

空闲块计数的高 16 位。

0x2E

__le16

bg_free_inodes_count_hi

空闲 inode 计数的高 16 位。

0x30

__le16

bg_used_dirs_count_hi

目录计数的高 16 位。

0x32

__le16

bg_itable_unused_hi

未使用的 inode 计数的高 16 位。

0x34

__le32

bg_exclude_bitmap_hi

快照排除位图位置的高 32 位。

0x38

__le16

bg_block_bitmap_csum_hi

块位图校验和的高 16 位。

0x3A

__le16

bg_inode_bitmap_csum_hi

inode 位图校验和的高 16 位。

0x3C

__u32

bg_reserved

填充到 64 个字节。

块组标志可以是以下各项的任意组合

描述

0x1

inode 表和位图未初始化 (EXT4_BG_INODE_UNINIT)。

0x2

块位图未初始化 (EXT4_BG_BLOCK_UNINIT)。

0x4

inode 表已置零 (EXT4_BG_INODE_ZEROED)。

3.3. 块和 inode 位图

数据块位图跟踪块组内数据块的使用情况。

inode 位图记录 inode 表中哪些条目正在使用中。

与大多数位图一样,一位表示一个数据块或 inode 表条目的使用状态。这意味着块组大小为 8 * 逻辑块中的字节数。

注意:如果为给定的块组设置了 BLOCK_UNINIT,则内核和 e2fsprogs 代码的各个部分会假装块位图包含零(即组中的所有块都是空闲的)。但是,并非一定没有使用中的块 - 如果设置了 meta_bg,则位图和组描述符位于组内。不幸的是,ext2fs_test_block_bitmap2() 将为这些位置返回“0”,这会产生令人困惑的 debugfs 输出。

3.4. Inode 表

Inode 表在 mkfs 时静态分配。每个块组描述符都指向表的开头,超级块记录每个组的 inode 数量。有关更多信息,请参阅有关 inode 的部分。

3.5. 多重挂载保护

多重挂载保护 (MMP) 是一项保护文件系统免受多个主机同时尝试使用文件系统的功能。当打开文件系统(用于挂载或 fsck 等)时,节点(称其为节点 A)上运行的 MMP 代码会检查序列号。如果序列号为 EXT4_MMP_SEQ_CLEAN,则继续打开。如果序列号为 EXT4_MMP_SEQ_FSCK,则 (希望) fsck 正在运行,并且打开立即失败。否则,打开代码将等待两倍的指定 MMP 检查间隔,并再次检查序列号。如果序列号已更改,则表示文件系统在另一台机器上处于活动状态,并且打开失败。如果 MMP 代码通过所有这些检查,则会生成一个新的 MMP 序列号并写入 MMP 块,并且挂载继续进行。

在文件系统处于活动状态时,内核会设置一个计时器,以在指定的 MMP 检查间隔重新检查 MMP 块。要执行重新检查,将重新读取 MMP 序列号;如果它与内存中的 MMP 序列号不匹配,则另一个节点(节点 B)已挂载文件系统,并且节点 A 以只读方式重新挂载文件系统。如果序列号匹配,则内存和磁盘中的序列号都会递增,并且重新检查完成。

只要打开操作成功,主机名和设备文件名就会写入 MMP 块。MMP 代码不使用这些值;它们纯粹是为了提供信息。

针对 FS UUID 和 MMP 结构计算校验和。MMP 结构 (struct mmp_struct) 如下所示

偏移

类型

名称

描述

0x0

__le32

mmp_magic

MMP 的魔术数,0x004D4D50 (“MMP”)。

0x4

__le32

mmp_seq

序列号,定期更新。

0x8

__le64

mmp_time

上次更新 MMP 块的时间。

0x10

char[64]

mmp_nodename

打开文件系统的节点的主机名。

0x50

char[32]

mmp_bdevname

文件系统的块设备名称。

0x70

__le16

mmp_check_interval

MMP 重新检查间隔,以秒为单位。

0x72

__le16

mmp_pad1

零。

0x74

__le32[226]

mmp_pad2

零。

0x3FC

__le32

mmp_checksum

MMP 块的校验和。

3.6. 日志 (jbd2)

在 ext3 中引入的 ext4 文件系统使用日志来保护文件系统在系统崩溃时免受元数据不一致的影响。最多可以在文件系统中保留 10,240,000 个文件系统块(有关日志大小限制的更多详细信息,请参阅 man mke2fs(8)),作为将“重要”数据写入磁盘的最快位置。一旦重要的数据事务完全写入磁盘并从磁盘写入缓存中刷新,也会将提交数据的记录写入日志。在稍后的某个时间点,日志代码会将事务写入它们在磁盘上的最终位置(这可能涉及大量寻道或大量小型读写擦除),然后再擦除提交记录。如果系统在第二次慢速写入期间崩溃,则日志可以一直重放到最新的提交记录,从而保证通过日志写入磁盘的任何内容的原子性。这样做的效果是保证文件系统不会在元数据更新的中途卡住。

出于性能原因,ext4 默认只通过日志写入文件系统元数据。这意味着在崩溃后,无法保证文件数据块处于任何一致的状态。如果此默认保证级别 (data=ordered) 不令人满意,则可以使用挂载选项来控制日志行为。如果 data=journal,则所有数据和元数据都会通过日志写入磁盘。这比较慢,但最安全。如果 data=writeback,则在通过日志将元数据写入磁盘之前,不会将脏数据块刷新到磁盘。

data=ordered 模式下,Ext4 还支持快速提交,这有助于显着减少提交延迟。默认的 data=ordered 模式通过将元数据块记录到日志中来工作。在快速提交模式下,Ext4 仅将重新创建受影响的元数据所需的最小增量存储在与 JBD2 共享的快速提交空间中。一旦快速提交区域填满,或者如果快速提交不可行,或者如果 JBD2 提交计时器关闭,Ext4 将执行传统的完整提交。完整提交会使之前发生的所有快速提交失效,从而使快速提交区域为空以进行进一步的快速提交。此功能需要在 mkfs 时启用。

日志 inode 通常是 inode 8。日志 inode 的前 68 个字节在 ext4 超级块中复制。日志本身是文件系统中的普通(但隐藏的)文件。该文件通常会占用整个块组,尽管 mke2fs 尝试将其放置在磁盘的中间位置。

jbd2 中的所有字段都以大端顺序写入磁盘。这与 ext4 相反。

注意:ext4 和 ocfs2 都使用 jbd2。

嵌入在 ext4 文件系统中的日志的最大大小为 2^32 个块。jbd2 本身似乎并不关心。

3.6.1. 布局

一般来说,日志具有以下格式

超级块 (Superblock)

描述符块 (descriptor_block) (数据块 data_blocks 或 撤销块 revocation_block) [更多数据或撤销] 提交块 (commit_block)

[更多事务...]

一个事务

请注意,一个事务以描述符和一些数据,或者以块撤销列表开始。一个完成的事务总是以提交结束。如果没有提交记录(或者校验和不匹配),该事务将在重放期间被丢弃。

3.6.2. 外部日志

可选地,可以创建一个带有外部日志设备的 ext4 文件系统(而不是使用保留 inode 的内部日志)。在这种情况下,在文件系统设备上,s_journal_inum 应该为零,并且 s_journal_uuid 应该被设置。在日志设备上,通常的位置会有一个 ext4 超级块,并具有匹配的 UUID。日志超级块将位于超级块之后的下一个完整块中。

1024 字节的填充

ext4 超级块

日志超级块

描述符块 (descriptor_block) (数据块 data_blocks 或 撤销块 revocation_block) [更多数据或撤销] 提交块 (commit_block)

[更多事务...]

一个事务

3.6.3. 块头

日志中的每个块都以一个通用的 12 字节的头 struct journal_header_s 开始。

偏移

类型

名称

描述

0x0

__be32

h_magic

jbd2 魔数,0xC03B3998。

0x4

__be32

h_blocktype

描述此块包含的内容。请参阅下面的 jbd2_blocktype 表。

0x8

__be32

h_sequence

与此块相关的事务 ID。

日志块类型可以是以下任何一种:

描述

1

描述符。此块位于一系列通过日志写入的数据块之前,这些数据块在事务期间写入。

2

块提交记录。此块表示事务完成。

3

日志超级块,v1。

4

日志超级块,v2。

5

块撤销记录。这通过使日志能够跳过写入随后被重写过的块来加速恢复。

3.6.4. 超级块

与 ext4 的超级块相比,日志的超级块要简单得多。其中保留的关键数据是日志的大小,以及在哪里找到事务日志的起始位置。

日志超级块记录为 struct journal_superblock_s,长度为 1024 字节。

偏移

类型

名称

描述

描述日志的静态信息。

0x0

journal_header_t (12 字节)

s_header

标识此为超级块的公共头。

0xC

__be32

s_blocksize

日志设备块大小。

0x10

__be32

s_maxlen

此日志中的块总数。

0x14

__be32

s_first

日志信息的第一个块。

描述日志当前状态的动态信息。

0x18

__be32

s_sequence

日志中期望的第一个提交 ID。

0x1C

__be32

s_start

日志起始位置的块号。与注释相反,此字段为零并不表示日志是干净的!

0x20

__be32

s_errno

错误值,由 jbd2_journal_abort() 设置。

剩余字段仅在 v2 超级块中有效。

0x24

__be32

s_feature_compat;

兼容功能集。请参阅下面的 jbd2_compat 表。

0x28

__be32

s_feature_incompat

不兼容的功能集。请参阅下面的 jbd2_incompat 表。

0x2C

__be32

s_feature_ro_compat

只读兼容的功能集。目前没有任何这些功能。

0x30

__u8

s_uuid[16]

日志的 128 位 uuid。在挂载时,会将其与 ext4 超级块中的副本进行比较。

0x40

__be32

s_nr_users

共享此日志的文件系统数量。

0x44

__be32

s_dynsuper

动态超级块副本的位置。(未使用?)

0x48

__be32

s_max_transaction

每个事务的日志块限制。(未使用?)

0x4C

__be32

s_max_trans_data

每个事务的数据块限制。(未使用?)

0x50

__u8

s_checksum_type

用于日志的校验和算法。有关更多信息,请参阅 jbd2_checksum_type

0x51

__u8[3]

s_padding2

0x54

__be32

s_num_fc_blocks

日志中快速提交块的数量。

0x58

__be32

s_head

日志头(第一个未使用的块)的块号,仅当日志为空时才为最新状态。

0x5C

__u32

s_padding[40]

0xFC

__be32

s_checksum

整个超级块的校验和,此字段设置为零。

0x100

__u8

s_users[16*48]

所有共享日志的文件系统的 ID。e2fsprogs/Linux 不允许共享外部日志,但我认为使用 jbd2 代码的 Lustre (或 ocfs2?) 可能会这样做。

日志兼容功能是以下各项的任意组合:

描述

0x1

日志维护数据块上的校验和。(JBD2_FEATURE_COMPAT_CHECKSUM)

日志不兼容功能是以下各项的任意组合:

描述

0x1

日志具有块撤销记录。(JBD2_FEATURE_INCOMPAT_REVOKE)

0x2

日志可以处理 64 位块号。(JBD2_FEATURE_INCOMPAT_64BIT)

0x4

日志异步提交。(JBD2_FEATURE_INCOMPAT_ASYNC_COMMIT)

0x8

此日志使用磁盘校验和格式的 v2 版本。每个日志元数据块都有自己的校验和,并且描述符表中的块标签包含日志中每个数据块的校验和。(JBD2_FEATURE_INCOMPAT_CSUM_V2)

0x10

此日志使用磁盘校验和格式的 v3 版本。这与 v2 相同,但日志块标签大小是固定的,与块号大小无关。(JBD2_FEATURE_INCOMPAT_CSUM_V3)

0x20

日志具有快速提交块。(JBD2_FEATURE_INCOMPAT_FAST_COMMIT)

日志校验和类型代码是以下之一。crc32 或 crc32c 是最有可能的选择。

描述

1

CRC32

2

MD5

3

SHA1

4

CRC32C

3.6.5. 描述符块

描述符块包含一个日志块标签数组,这些标签描述了日志中后续数据块的最终位置。描述符块是开放编码的,而不是由数据结构完全描述,但无论如何,这里是块结构。描述符块至少消耗 36 个字节,但使用一个完整的块。

偏移

类型

名称

描述符

0x0

journal_header_t

(开放编码)

公共块头。

0xC

struct journal_block_tag_s

开放编码数组[]

足够的标签来填充块或描述此描述符块之后的所有数据块。

日志块标签具有以下任何格式,具体取决于设置的日志功能和块标签标志。

如果设置了 JBD2_FEATURE_INCOMPAT_CSUM_V3,则日志块标签定义为 struct journal_block_tag3_s,其外观如下。大小为 16 或 32 字节。

偏移

类型

名称

描述符

0x0

__be32

t_blocknr

对应数据块应在磁盘上结束位置的低 32 位。

0x4

__be32

t_flags

与描述符一起使用的标志。有关更多信息,请参阅 jbd2_tag_flags 表。

0x8

__be32

t_blocknr_high

对应数据块应在磁盘上结束位置的高 32 位。如果未启用 JBD2_FEATURE_INCOMPAT_64BIT,则此值为零。

0xC

__be32

t_checksum

日志 UUID、序列号和数据块的校验和。

此字段似乎是开放编码的。它始终位于标签的末尾,在 t_checksum 之后。如果设置了“相同 UUID”标志,则此字段不存在。

0x8 或 0xC

char

uuid[16]

与此标签一起使用的 UUID。此字段似乎是从 struct journal_s 中的 j_uuid 字段复制的,但只有 tune2fs 才会触及该字段。

日志标签标志是以下各项的任意组合:

描述

0x1

磁盘上的块被转义。数据块的前四个字节恰好与 jbd2 魔数匹配。

0x2

此块与之前的 UUID 相同,因此省略了 UUID 字段。

0x4

数据块已通过事务删除。(未使用?)

0x8

这是此描述符块中的最后一个标签。

如果未设置 JBD2_FEATURE_INCOMPAT_CSUM_V3,则日志块标签定义为 struct journal_block_tag_s,其外观如下。大小为 8、12、24 或 28 字节。

偏移

类型

名称

描述符

0x0

__be32

t_blocknr

对应数据块应在磁盘上结束位置的低 32 位。

0x4

__be16

t_checksum

日志 UUID、序列号和数据块的校验和。请注意,仅存储较低的 16 位。

0x6

__be16

t_flags

与描述符一起使用的标志。有关更多信息,请参阅 jbd2_tag_flags 表。

仅当超级块指示支持 64 位块号时,才存在下一个字段。

0x8

__be32

t_blocknr_high

对应数据块应在磁盘上结束位置的高 32 位。

此字段似乎是开放编码的。它始终位于标签的末尾,在 t_flags 或 t_blocknr_high 之后。如果设置了“相同 UUID”标志,则此字段不存在。

0x8 或 0xC

char

uuid[16]

与此标签一起使用的 UUID。此字段似乎是从 struct journal_s 中的 j_uuid 字段复制的,但只有 tune2fs 才会触及该字段。

如果设置了 JBD2_FEATURE_INCOMPAT_CSUM_V2 或 JBD2_FEATURE_INCOMPAT_CSUM_V3,则块的末尾是 struct jbd2_journal_block_tail,其外观如下

偏移

类型

名称

描述符

0x0

__be32

t_checksum

日志 UUID + 描述符块的校验和,此字段设置为零。

3.6.6. 数据块

通常,通过日志写入到磁盘的数据块在描述符块之后按原样写入到日志文件中。但是,如果块的前四个字节与 jbd2 魔数匹配,则会将这四个字节替换为零,并在描述符块标签中设置“转义”标志。

3.6.7. 撤销块

撤销块用于防止在早期事务中重放块。这用于标记曾经记录在日志中但不再记录在日志中的块。通常,如果元数据块被释放并重新分配为文件数据块,则会发生这种情况;在这种情况下,在将文件块写入磁盘后,日志重放会导致损坏。

注意:此机制不用于表达“此日志块被此其他日志块取代”,正如作者 (djwong) 错误地认为的那样。添加到事务的任何块都将导致删除该块的所有现有撤销记录。

撤销块在 struct jbd2_journal_revoke_header_s 中描述,至少为 16 字节长,但使用一个完整的块

偏移

类型

名称

描述

0x0

journal_header_t

r_header

公共块头。

0xC

__be32

r_count

此块中使用的字节数。

0x10

__be32 或 __be64

blocks[0]

要撤销的块。

在 r_count 之后是一个线性块号数组,这些块号在此事务中被有效撤销。如果超级块声明支持 64 位块号,则每个块号的大小为 8 个字节,否则为 4 个字节。

如果设置了 JBD2_FEATURE_INCOMPAT_CSUM_V2 或 JBD2_FEATURE_INCOMPAT_CSUM_V3,则撤销块的末尾是 struct jbd2_journal_revoke_tail,其格式如下

偏移

类型

名称

描述

0x0

__be32

r_checksum

日志 UUID + 撤销块的校验和

3.6.8. 提交块

提交块是一个哨兵,指示事务已完全写入日志。一旦此提交块到达日志,则存储在此事务中的数据就可以写入其在磁盘上的最终位置。

提交块由 struct commit_header 描述,其长度为 32 字节(但使用一个完整的块)。

偏移

类型

名称

描述符

0x0

journal_header_s

(开放编码)

公共块头。

0xC

unsigned char

h_chksum_type

用于验证事务中数据块完整性的校验和类型。有关更多信息,请参阅 jbd2_checksum_type

0xD

unsigned char

h_chksum_size

校验和使用的字节数。最可能是 4。

0xE

unsigned char

h_padding[2]

0x10

__be32

h_chksum[JBD2_CHECKSUM_BYTES]

用于存储校验和的 32 字节空间。如果设置了 JBD2_FEATURE_INCOMPAT_CSUM_V2 或 JBD2_FEATURE_INCOMPAT_CSUM_V3,则第一个 __be32 是日志 UUID 和整个提交块的校验和,此字段为零。如果设置了 JBD2_FEATURE_COMPAT_CHECKSUM,则第一个 __be32 是已写入事务的所有块的 crc32。

0x30

__be64

h_commit_sec

事务提交的时间,以自纪元以来的秒数表示。

0x38

__be32

h_commit_nsec

上述时间戳的纳秒部分。

3.6.9. 快速提交

快速提交区域组织为标签长度值的日志。每个 TLV 的开头都有一个 struct ext4_fc_tl,其中存储了整个字段的标签和长度。其后是可变长度的特定于标签的值。以下是支持的标签及其含义的列表

标签

含义

值结构体

描述

EXT4_FC_TAG_HEAD

快速提交区域头部

struct ext4_fc_head

存储这些快速提交应应用后的事务 TID。

EXT4_FC_TAG_ADD_RANGE

向 inode 添加范围

struct ext4_fc_add_range

存储要在此 inode 中添加的 inode 编号和范围

EXT4_FC_TAG_DEL_RANGE

删除 inode 的逻辑偏移

struct ext4_fc_del_range

存储需要删除的 inode 编号和逻辑偏移范围

EXT4_FC_TAG_CREAT

为新创建的文件创建目录条目

struct ext4_fc_dentry_info

存储新创建文件的父 inode 编号、inode 编号和目录条目

EXT4_FC_TAG_LINK

将目录条目链接到 inode

struct ext4_fc_dentry_info

存储父 inode 编号、inode 编号和目录条目

EXT4_FC_TAG_UNLINK

取消链接 inode 的目录条目

struct ext4_fc_dentry_info

存储父 inode 编号、inode 编号和目录条目

EXT4_FC_TAG_PAD

填充(未使用的区域)

快速提交区域中未使用的字节。

EXT4_FC_TAG_TAIL

标记快速提交的结尾

struct ext4_fc_tail

存储提交的 TID,此标签表示其结尾的快速提交的 CRC

3.6.10. 快速提交重放幂等性

如果恢复代码遵循某些规则,则快速提交标签本质上是幂等的。提交路径在提交时遵循的指导原则是,它存储特定操作的结果,而不是存储过程。

让我们考虑这个重命名操作:“mv /a /b”。假设 dirent '/a' 与 inode 10 相关联。在快速提交期间,我们不将此操作存储为过程“将 a 重命名为 b”,而是将结果文件系统状态存储为一系列“结果”

  • 将 dirent b 链接到 inode 10

  • 取消链接 dirent a

  • inode 10 具有有效的引用计数

现在,当恢复代码运行时,它需要在文件系统上“强制执行”此状态。这就是保证快速提交重放的幂等性的原因。

让我们以一个非幂等过程为例,看看快速提交如何使其幂等。考虑以下操作序列

  1. rm A

  2. mv B A

  3. read A

如果我们按原样存储此操作序列,则重放不是幂等的。假设在重放时,我们在 (2) 之后崩溃。在第二次重放期间,文件 A(实际上是作为“mv B A”操作的结果创建的)将被删除。因此,当我们尝试读取 A 时,名为 A 的文件将不存在。因此,此操作序列不是幂等的。但是,如上所述,快速提交不是存储过程,而是存储每个过程的结果。因此,上述过程的快速提交日志如下

(假设在重放之前,dirent A 链接到 inode 10,dirent B 链接到 inode 11)

  1. 取消链接 A

  2. 将 A 链接到 inode 11

  3. 取消链接 B

  4. inode 11

如果我们崩溃在 (3) 之后,我们将有文件 A 链接到 inode 11。在第二次重放期间,我们将删除文件 A(inode 11)。但是我们将重新创建它并使其指向 inode 11。我们找不到 B,因此我们将跳过该步骤。此时,inode 11 的引用计数不可靠,但这可以通过重放最后一个 inode 11 标签来修复。因此,通过将非幂等过程转换为一系列幂等结果,快速提交确保了重放期间的幂等性。

3.6.11. 日志检查点

对日志进行检查点操作可确保所有事务及其关联的缓冲区都已提交到磁盘。正在进行的事务将被等待并包含在检查点中。检查点在对文件系统进行关键更新(包括日志恢复、文件系统大小调整和释放 journal_t 结构)时在内部使用。

可以通过 ioctl EXT4_IOC_CHECKPOINT 从用户空间触发日志检查点。此 ioctl 采用单个 u64 参数作为标志。当前支持三个标志。首先,EXT4_IOC_CHECKPOINT_FLAG_DRY_RUN 可用于验证 ioctl 的输入。如果存在任何无效输入,则会返回错误,否则会返回成功,而不会执行任何检查点操作。这可用于检查系统上是否存在 ioctl,并验证参数或标志是否存在问题。另外两个标志是 EXT4_IOC_CHECKPOINT_FLAG_DISCARD 和 EXT4_IOC_CHECKPOINT_FLAG_ZEROOUT。这些标志分别导致日志块在日志检查点完成后被丢弃或用零填充。EXT4_IOC_CHECKPOINT_FLAG_DISCARD 和 EXT4_IOC_CHECKPOINT_FLAG_ZEROOUT 不能同时设置。当快照系统或为了符合内容删除 SLO 时,ioctl 可能很有用。

3.7. 孤立文件

在 unix 中,可能存在从目录层次结构取消链接但由于它们是打开的而仍然存活的 inode。在发生崩溃的情况下,文件系统必须清理这些 inode,否则它们(以及它们引用的块)将泄漏。类似地,如果我们截断或扩展文件,我们需要不能在单个日志事务中执行该操作。在这种情况下,我们将 inode 跟踪为孤立 inode,以便在发生崩溃时截断分配给文件的额外块。

传统上,ext4 以单链表的形式跟踪孤立 inode,其中超级块包含最后一个孤立 inode 的 inode 编号(s_last_orphan 字段),然后每个 inode 包含先前孤立的 inode 的 inode 编号(我们为此重载 i_dtime inode 字段)。但是,对于导致大量创建孤立 inode 的工作负载,此文件系统全局单链表是一个可伸缩性瓶颈。启用孤立文件功能 (COMPAT_ORPHAN_FILE) 后,文件系统有一个特殊的 inode(通过 s_orphan_file_inum 从超级块引用),其中包含多个块。这些块中的每一个都具有以下结构

偏移

类型

名称

描述

0x0

__le32 条目数组

孤立 inode 条目

每个 __le32 条目要么为空 (0),要么包含孤立 inode 的 inode 编号。

块大小 - 8

__le32

ob_magic

存储在孤立块尾部的魔术值 (0x0b10ca04)

块大小 - 4

__le32

ob_checksum

孤立块的校验和。

当可写地挂载具有孤立文件功能的文件系统时,我们在超级块中设置 RO_COMPAT_ORPHAN_PRESENT 功能,以指示可能存在有效的孤立条目。如果我们在挂载文件系统时看到此功能,我们将读取整个孤立文件,并像往常一样处理那里找到的所有孤立 inode。当干净地卸载文件系统时,我们会删除 RO_COMPAT_ORPHAN_PRESENT 功能,以避免不必要地扫描孤立文件,并使文件系统与较旧的内核完全兼容。