4. 动态结构

动态元数据是在将文件和块分配给文件时即时创建的。

4.1. 索引节点

在常规的 UNIX 文件系统中,inode 存储与文件相关的所有元数据(时间戳、块映射、扩展属性等),而不是目录条目。要查找与文件关联的信息,必须遍历目录文件以查找与文件关联的目录条目,然后加载 inode 以查找该文件的元数据。ext4 似乎通过在目录条目中存储文件类型的副本(通常存储在 inode 中)来作弊(出于性能原因)。(将所有这些与 FAT 进行比较,FAT 将所有文件信息直接存储在目录条目中,但不支持硬链接,并且由于其更简单的块分配器和对链表的广泛使用,通常比 ext4 更容易进行寻道。)

inode 表是 struct ext4_inode 的线性数组。表的大小设置为具有足够的块来存储至少 sb.s_inode_size * sb.s_inodes_per_group 字节。包含 inode 的块组的编号可以计算为 (inode_number - 1) / sb.s_inodes_per_group,并且组的表中的偏移量为 (inode_number - 1) % sb.s_inodes_per_group。没有 inode 0。

inode 校验和是根据 FS UUID、inode 编号和 inode 结构本身计算的。

inode 表条目以 struct ext4_inode 形式布局。

偏移量

大小

名称

描述

0x0

__le16

i_mode

文件模式。请参阅下面的 i_mode 表。

0x2

__le16

i_uid

所有者 UID 的低 16 位。

0x4

__le32

i_size_lo

以字节为单位的大小的低 32 位。

0x8

__le32

i_atime

上次访问时间,以自 epoch 以来的秒数为单位。但是,如果设置了 EA_INODE inode 标志,则此 inode 存储扩展属性值,并且此字段包含该值的校验和。

0xC

__le32

i_ctime

上次 inode 更改时间,以自 epoch 以来的秒数为单位。但是,如果设置了 EA_INODE inode 标志,则此 inode 存储扩展属性值,并且此字段包含属性值的引用计数的低 32 位。

0x10

__le32

i_mtime

上次数据修改时间,以自 epoch 以来的秒数为单位。但是,如果设置了 EA_INODE inode 标志,则此 inode 存储扩展属性值,并且此字段包含拥有扩展属性的 inode 的编号。

0x14

__le32

i_dtime

删除时间,以自 epoch 以来的秒数为单位。

0x18

__le16

i_gid

GID 的低 16 位。

0x1A

__le16

i_links_count

硬链接计数。通常,ext4 不允许 inode 具有超过 65,000 个硬链接。这适用于文件和目录,这意味着一个目录中不能有超过 64,998 个子目录(每个子目录的“..”条目计为一个硬链接,目录本身的“.”条目也计为一个硬链接)。启用 DIR_NLINK 功能后,ext4 通过将此字段设置为 1 来指示硬链接的数量未知,从而支持超过 64,998 个子目录。

0x1C

__le32

i_blocks_lo

“块”计数的低 32 位。如果在文件系统上未设置 huge_file 功能标志,则该文件在磁盘上占用 i_blocks_lo 512 字节的块。如果设置了 huge_file 并且在 inode.i_flags 中未设置 EXT4_HUGE_FILE_FL,则该文件在磁盘上占用 i_blocks_lo + (i_blocks_hi << 32) 512 字节的块。如果设置了 huge_file 并且在 inode.i_flags 中设置了 EXT4_HUGE_FILE_FL,则该文件在磁盘上占用 (i_blocks_lo + i_blocks_hi << 32) 文件系统块。

0x20

__le32

i_flags

Inode 标志。请参阅下面的 i_flags 表。

0x24

4 字节

i_osd1

有关更多详细信息,请参见 i_osd1 表。

0x28

60 字节

i_block[EXT4_N_BLOCKS=15]

块映射或 extent 树。请参阅“inode.i_block 的内容”部分。

0x64

__le32

i_generation

文件版本(对于 NFS)。

0x68

__le32

i_file_acl_lo

扩展属性块的低 32 位。当然,ACL 只是许多可能的扩展属性之一;我认为此字段的名称是扩展属性的第一个用途是用于 ACL 的结果。

0x6C

__le32

i_size_high / i_dir_acl

文件/目录大小的高 32 位。在 ext2/3 中,此字段名为 i_dir_acl,尽管它通常设置为零并且从未使用过。

0x70

__le32

i_obso_faddr

(已过时)片段地址。

0x74

12 字节

i_osd2

有关更多详细信息,请参见 i_osd2 表。

0x80

__le16

i_extra_isize

此 inode 的大小 - 128。或者,原始 ext2 inode 之外的扩展 inode 字段的大小,包括此字段。

0x82

__le16

i_checksum_hi

inode 校验和的高 16 位。

0x84

__le32

i_ctime_extra

额外的更改时间位。这提供了亚秒级的精度。请参阅 Inode 时间戳部分。

0x88

__le32

i_mtime_extra

额外的修改时间位。这提供了亚秒级的精度。

0x8C

__le32

i_atime_extra

额外的访问时间位。这提供了亚秒级的精度。

0x90

__le32

i_crtime

文件创建时间,以自 epoch 以来的秒数为单位。

0x94

__le32

i_crtime_extra

额外的文件创建时间位。这提供了亚秒级的精度。

0x98

__le32

i_version_hi

版本号的高 32 位。

0x9C

__le32

i_projid

项目 ID。

i_mode 值是以下标志的组合

描述

0x1

S_IXOTH(其他人可以执行)

0x2

S_IWOTH(其他人可以写入)

0x4

S_IROTH(其他人可以读取)

0x8

S_IXGRP(组成员可以执行)

0x10

S_IWGRP(组成员可以写入)

0x20

S_IRGRP(组成员可以读取)

0x40

S_IXUSR(所有者可以执行)

0x80

S_IWUSR(所有者可以写入)

0x100

S_IRUSR(所有者可以读取)

0x200

S_ISVTX(粘滞位)

0x400

S_ISGID(设置 GID)

0x800

S_ISUID(设置 UID)

这些是互斥的文件类型

0x1000

S_IFIFO(FIFO)

0x2000

S_IFCHR(字符设备)

0x4000

S_IFDIR(目录)

0x6000

S_IFBLK(块设备)

0x8000

S_IFREG(常规文件)

0xA000

S_IFLNK(符号链接)

0xC000

S_IFSOCK(套接字)

i_flags 字段是这些值的组合

描述

0x1

此文件需要安全删除 (EXT4_SECRM_FL)。(未实现)

0x2

如果需要取消删除,则应保留此文件 (EXT4_UNRM_FL)。(未实现)

0x4

文件已压缩 (EXT4_COMPR_FL)。(未真正实现)

0x8

对文件的所有写入必须是同步的 (EXT4_SYNC_FL)。

0x10

文件是不可变的 (EXT4_IMMUTABLE_FL)。

0x20

文件只能追加 (EXT4_APPEND_FL)。

0x40

dump(1) 实用程序不应转储此文件 (EXT4_NODUMP_FL)。

0x80

不要更新访问时间 (EXT4_NOATIME_FL)。

0x100

脏压缩文件 (EXT4_DIRTY_FL)。 (未使用)

0x200

文件具有一个或多个压缩簇 (EXT4_COMPRBLK_FL)。 (未使用)

0x400

不要压缩文件 (EXT4_NOCOMPR_FL)。 (未使用)

0x800

加密的 inode (EXT4_ENCRYPT_FL)。此位值以前是 EXT4_ECOMPR_FL(压缩错误),从未被使用过。

0x1000

目录具有哈希索引 (EXT4_INDEX_FL)。

0x2000

AFS 魔术目录 (EXT4_IMAGIC_FL)。

0x4000

文件数据必须始终通过日志写入 (EXT4_JOURNAL_DATA_FL)。

0x8000

不应合并文件尾部 (EXT4_NOTAIL_FL)。 (ext4 未使用)

0x10000

所有目录条目数据都应同步写入(请参见 dirsync)(EXT4_DIRSYNC_FL)。

0x20000

目录层次结构的顶部 (EXT4_TOPDIR_FL)。

0x40000

这是一个巨大的文件 (EXT4_HUGE_FILE_FL)。

0x80000

Inode 使用 extent (EXT4_EXTENTS_FL)。

0x100000

Verity 保护的文件 (EXT4_VERITY_FL)。

0x200000

Inode 在其数据块中存储一个大的扩展属性值 (EXT4_EA_INODE_FL)。

0x400000

此文件已在 EOF 之后分配了块 (EXT4_EOFBLOCKS_FL)。 (已弃用)

0x01000000

Inode 是快照 (EXT4_SNAPFILE_FL)。 (不在主线中)

0x04000000

正在删除快照 (EXT4_SNAPFILE_DELETED_FL)。 (不在主线中)

0x08000000

快照收缩已完成 (EXT4_SNAPFILE_SHRUNK_FL)。 (不在主线中)

0x10000000

Inode 具有内联数据 (EXT4_INLINE_DATA_FL)。

0x20000000

使用相同的项目 ID 创建子级 (EXT4_PROJINHERIT_FL)。

0x80000000

为 ext4 库保留 (EXT4_RESERVED_FL)。

聚合标志

0x705BDFFF

用户可见的标志。

0x604BC0FF

用户可修改的标志。请注意,虽然可以使用 setattr 设置 EXT4_JOURNAL_DATA_FL 和 EXT4_EXTENTS_FL,但它们不在内核的 EXT4_FL_USER_MODIFIABLE 掩码中,因为它需要以特殊方式处理这些标志的设置,并且它们已从直接保存到 i_flags 的标志集中屏蔽掉。

osd1 字段具有多个含义,具体取决于创建者

Linux

偏移量

大小

名称

描述

0x0

__le32

l_i_version

Inode 版本。但是,如果设置了 EA_INODE inode 标志,则此 inode 存储扩展属性值,并且此字段包含属性值的引用计数的高 32 位。

Hurd

偏移量

大小

名称

描述

0x0

__le32

h_i_translator

??

Masix

偏移量

大小

名称

描述

0x0

__le32

m_i_reserved

??

osd2 字段具有多个含义,具体取决于文件系统创建者

Linux

偏移量

大小

名称

描述

0x0

__le16

l_i_blocks_high

块计数的高 16 位。请参阅附加到 i_blocks_lo 的注释。

0x2

__le16

l_i_file_acl_high

扩展属性块的高 16 位(历史上是文件 ACL 位置)。请参见下面的扩展属性部分。

0x4

__le16

l_i_uid_high

所有者 UID 的高 16 位。

0x6

__le16

l_i_gid_high

GID 的高 16 位。

0x8

__le16

l_i_checksum_lo

inode 校验和的低 16 位。

0xA

__le16

l_i_reserved

未使用。

Hurd

偏移量

大小

名称

描述

0x0

__le16

h_i_reserved1

??

0x2

__u16

h_i_mode_high

文件模式的高 16 位。

0x4

__le16

h_i_uid_high

所有者 UID 的高 16 位。

0x6

__le16

h_i_gid_high

GID 的高 16 位。

0x8

__u32

h_i_author

作者代码?

Masix

偏移量

大小

名称

描述

0x0

__le16

h_i_reserved1

??

0x2

__u16

m_i_file_acl_high

扩展属性块的高 16 位(历史上是文件 ACL 位置)。

0x4

__u32

m_i_reserved2[2]

??

4.1.1. Inode 大小

在 ext2 和 ext3 中,inode 结构大小固定为 128 字节 (EXT2_GOOD_OLD_INODE_SIZE),并且每个 inode 的磁盘记录大小为 128 字节。从 ext4 开始,可以在格式化时为文件系统中的所有 inode 分配一个更大的磁盘 inode,以便在原始 ext2 inode 的末尾之外提供空间。磁盘上的 inode 记录大小记录在超级块中,为 s_inode_size。struct ext4_inode 实际使用的字节数超过原始 128 字节的 ext2 inode 记录在每个 inode 的 i_extra_isize 字段中,这允许 struct ext4_inode 为新内核增长,而无需升级所有磁盘上的 inode。对 EXT2_GOOD_OLD_INODE_SIZE 之外的字段的访问应验证是否在 i_extra_isize 范围内。默认情况下,ext4 inode 记录为 256 字节,并且(截至 2019 年 8 月)inode 结构为 160 字节 (i_extra_isize = 32)。inode 结构末尾和 inode 记录末尾之间的额外空间可用于存储扩展属性。每个 inode 记录可以与文件系统块大小一样大,尽管这效率不高。

4.1.2. 查找 Inode

每个块组包含 sb->s_inodes_per_group 个 inode。因为 inode 0 被定义为不存在,所以可以使用以下公式来查找 inode 所在的块组:bg = (inode_num - 1) / sb->s_inodes_per_group。可以在块组的 inode 表中找到特定的 inode,其位置为 index = (inode_num - 1) % sb->s_inodes_per_group。要获取 inode 表中的字节地址,请使用 offset = index * sb->s_inode_size

4.1.3. Inode 时间戳

在 inode 结构的低 128 字节中记录了四个时间戳——inode 更改时间 (ctime)、访问时间 (atime)、数据修改时间 (mtime) 和删除时间 (dtime)。这四个字段是 32 位有符号整数,表示自 Unix epoch(1970-01-01 00:00:00 GMT)以来的秒数,这意味着这些字段将在 2038 年 1 月溢出。如果文件系统没有 orphan_file 功能,则未从任何目录链接但仍处于打开状态的 inode(孤立 inode)会将 dtime 字段重载以用于孤立列表。超级块字段 s_last_orphan 指向孤立列表中的第一个 inode;然后 dtime 是下一个孤立 inode 的编号,如果没有更多孤立 inode,则为零。

如果 inode 结构大小 sb->s_inode_size 大于 128 字节并且 i_inode_extra 字段足够大以包含相应的 i_[cma]time_extra 字段,则 ctime、atime 和 mtime inode 字段将扩展为 64 位。在这个“extra” 32 位字段中,较低的两位用于将 32 位秒字段扩展为 34 位宽;较高的 30 位用于提供纳秒级的时间戳精度。因此,时间戳直到 2446 年 5 月才会溢出。dtime 没有扩展。还有一个第五个时间戳来记录 inode 创建时间 (crtime);此字段为 64 位宽,并以与 64 位 [cma]time 相同的方式解码。crtime 和 dtime 都无法通过常规的 stat() 接口访问,但 debugfs 会报告它们。

我们使用 32 位有符号时间值加上 (2^32 * (额外 epoch 位))。换句话说

额外的 epoch 位

32 位时间的 MSB

将有符号 32 位调整为 64 位 tv_sec

解码后的 64 位 tv_sec

有效时间范围

0 0

1

0

-0x80000000 - -0x00000001

1901-12-13 至 1969-12-31

0 0

0

0

0x000000000 - 0x07fffffff

1970-01-01 至 2038-01-19

0 1

1

0x100000000

0x080000000 - 0x0ffffffff

2038-01-19 至 2106-02-07

0 1

0

0x100000000

0x100000000 - 0x17fffffff

2106-02-07 至 2174-02-25

1 0

1

0x200000000

0x180000000 - 0x1ffffffff

2174-02-25 至 2242-03-16

1 0

0

0x200000000

0x200000000 - 0x27fffffff

2242-03-16 至 2310-04-04

1 1

1

0x300000000

0x280000000 - 0x2ffffffff

2310-04-04 至 2378-04-22

1 1

0

0x300000000

0x300000000 - 0x37fffffff

2378-04-22 至 2446-05-10

这是一种有些奇怪的编码,因为正值的数量实际上是负值的七倍。解码和编码超出 2038 年的日期也存在长期存在的错误,截至内核 3.12 和 e2fsprogs 1.42.8,这些错误似乎尚未修复。 64 位内核错误地使用额外的 epoch 位 1,1 来表示 1901 年到 1970 年之间的日期。在某个时候,内核将被修复,e2fsck 将修复这种情况,前提是在 2310 年之前运行它。

4.2. inode.i_block 的内容

根据 inode 描述的文件类型,inode.i_block 中的 60 个字节的存储空间可以以不同的方式使用。一般来说,常规文件和目录将使用它来存储文件块索引信息,而特殊文件将使用它来存储特殊用途。

4.2.2. 直接/间接块寻址

在 ext2/3 中,文件块号通过(最多)三级 1-1 块映射映射到逻辑块号。要查找存储特定文件块的逻辑块,代码将遍历这个越来越复杂的结构。请注意,既没有幻数也没有校验和来提供任何程度的信心,证明该块不是充满了垃圾。

i.i_block 偏移量

它指向哪里

0 到 11

直接映射到文件块 0 到 11。

12

间接块:(文件块 12 到 ($block_size / 4) + 11,或者如果 4KiB 块则为 12 到 1035)

间接块偏移量

它指向哪里

0 到 ($block_size / 4)

直接映射到 ($block_size / 4) 个块(如果 4KiB 块则为 1024)

13

双重间接块:(文件块 $block_size/4 + 12 到 ($block_size / 4) ^ 2 + ($block_size / 4) + 11,或者如果 4KiB 块则为 1036 到 1049611)

双重间接块偏移量

它指向哪里

0 到 ($block_size / 4)

映射到 ($block_size / 4) 个间接块(如果 4KiB 块则为 1024)

间接块偏移量

它指向哪里

0 到 ($block_size / 4)

直接映射到 ($block_size / 4) 个块(如果 4KiB 块则为 1024)

14

三重间接块:(文件块 ($block_size / 4) ^ 2 + ($block_size / 4) + 12 到 ($block_size / 4) ^ 3 + ($block_size / 4) ^ 2 + ($block_size / 4) + 12,或者如果 4KiB 块则为 1049612 到 1074791436)

三重间接块偏移量

它指向哪里

0 到 ($block_size / 4)

映射到 ($block_size / 4) 个双重间接块(如果 4KiB 块则为 1024)

双重间接块偏移量

它指向哪里

0 到 ($block_size / 4)

映射到 ($block_size / 4) 个间接块(如果 4KiB 块则为 1024)

间接块偏移量

它指向哪里

0 到 ($block_size / 4)

直接映射到 ($block_size / 4) 个块(如果 4KiB 块则为 1024)

请注意,使用这种块映射方案,即使对于一个大的连续文件,也需要填写大量的映射数据!这种效率低下导致了 extent 映射方案的创建,如下所述。

另请注意,使用此映射方案的文件不能放置在高于 2^32 个块的位置。

4.2.3. Extent 树

在 ext4 中,文件到逻辑块的映射已被 extent 树取代。在旧方案下,分配 1,000 个块的连续运行需要一个间接块来映射所有 1,000 个条目;使用 extent,映射减少到单个 struct ext4_extent,其中 ee_len = 1000。如果启用了 flex_bg,则可以使用单个 extent 分配非常大的文件,从而大大减少元数据块的使用,并提高磁盘效率。inode 必须设置 extent 标志 (0x80000) 才能使用此功能。

Extent 被排列成树。树的每个节点都以 struct ext4_extent_header 开头。如果节点是内部节点 (eh.eh_depth > 0),则标头后跟 eh.eh_entriesstruct ext4_extent_idx 的实例;这些索引条目中的每一个都指向一个包含 extent 树中更多节点的块。如果节点是叶节点 (eh.eh_depth == 0),则标头后跟 eh.eh_entriesstruct ext4_extent 的实例;这些实例指向文件的数据块。extent 树的根节点存储在 inode.i_block 中,这允许记录前四个 extent,而无需使用额外的元数据块。

extent 树标头记录在 struct ext4_extent_header 中,长度为 12 字节

偏移量

大小

名称

描述

0x0

__le16

eh_magic

幻数,0xF30A。

0x2

__le16

eh_entries

标头后有效条目的数量。

0x4

__le16

eh_max

可以跟随标头的最大条目数。

0x6

__le16

eh_depth

extent 节点在 extent 树中的深度。 0 = 此 extent 节点指向数据块;否则,此 extent 节点指向其他 extent 节点。 extent 树最多可以有 5 层深:逻辑块号最多可以是 2^32,满足 4*(((blocksize - 12)/12)^n) >= 2^32 的最小 n 是 5。

0x8

__le32

eh_generation

树的世代。(由 Lustre 使用,但不是标准 ext4)。

extent 树的内部节点,也称为索引节点,记录为 struct ext4_extent_idx,长度为 12 字节

偏移量

大小

名称

描述

0x0

__le32

ei_block

此索引节点涵盖从“块”开始的文件块。

0x4

__le32

ei_leaf_lo

树中下一层较低的 extent 节点的块号的低 32 位。 指向的树节点可以是另一个内部节点,也可以是叶节点,如下所述。

0x8

__le16

ei_leaf_hi

前一个字段的高 16 位。

0xA

__u16

ei_unused

extent 树的叶节点记录为 struct ext4_extent,长度也为 12 字节

偏移量

大小

名称

描述

0x0

__le32

ee_block

此 extent 涵盖的第一个文件块号。

0x4

__le16

ee_len

extent 覆盖的块数。如果此字段的值 <= 32768,则 extent 已初始化。如果该字段的值 > 32768,则 extent 未初始化,实际的 extent 长度为 ee_len - 32768。因此,已初始化 extent 的最大长度为 32768 个块,未初始化 extent 的最大长度为 32767 个块。

0x6

__le16

ee_start_hi

此 extent 指向的块号的高 16 位。

0x8

__le32

ee_start_lo

此 extent 指向的块号的低 32 位。

在引入元数据校验和之前,extent 标头 + extent 条目始终在每个 extent 树数据块的末尾留下至少 4 个字节的未分配空间(因为 (2^x % 12) >= 4)。因此,32 位校验和被插入到这个空间中。 inode 中的 4 个 extent 不需要校验和,因为 inode 已经过校验和。校验和是根据 FS UUID、inode 编号、inode 世代和直到(但不包括)校验和本身的整个 extent 块计算得出的。

struct ext4_extent_tail 长度为 4 字节

偏移量

大小

名称

描述

0x0

__le32

eb_checksum

extent 块的校验和,crc32c(uuid+inum+igeneration+extentblock)

4.2.4. 内联数据

如果为文件系统启用了内联数据功能,并且为 inode 设置了标志,则可以将文件数据的前 60 个字节存储在此处。

4.3. 目录条目

在 ext4 文件系统中,目录或多或少是一个平面文件,它将任意字节字符串(通常是 ASCII)映射到文件系统上的 inode 编号。文件系统中可能有许多目录条目引用相同的 inode 编号——这些条目称为硬链接,这也是硬链接无法引用其他文件系统上的文件的原因。因此,通过读取与目录文件关联的数据块以查找所需的特定目录条目来找到目录条目。

4.3.1. 线性(经典)目录

默认情况下,每个目录都以“几乎线性”的数组列出其条目。 我写“几乎”是因为它不是内存意义上的线性数组,因为目录条目不会跨文件系统块拆分。 因此,更准确的说法是,目录是一系列数据块,每个块都包含一个线性目录条目数组。 每个每个块数组的结尾都通过到达块的末尾来表示; 块中的最后一个条目的记录长度使其一直延伸到块的末尾。 整个目录的结尾当然是通过到达文件结尾来表示的。 未使用的目录条目由 inode = 0 表示。 默认情况下,文件系统使用 struct ext4_dir_entry_2 作为目录条目,除非未设置“filetype”功能标志,在这种情况下,它使用 struct ext4_dir_entry

原始目录条目格式是 struct ext4_dir_entry,最多 263 字节长,尽管在磁盘上您需要引用 dirent.rec_len 才能确定。

偏移量

大小

名称

描述

0x0

__le32

inode

此目录条目指向的 inode 的编号。

0x4

__le16

rec_len

此目录条目的长度。 必须是 4 的倍数。

0x6

__le16

name_len

文件名的长度。

0x8

char

name[EXT4_NAME_LEN]

文件名。

由于文件名不能超过 255 个字节,因此新的目录条目格式缩短了 name_len 字段,并将该空间用于文件类型标志,可能是为了避免在目录树遍历期间必须加载每个 inode。 此格式为 ext4_dir_entry_2,最多 263 字节长,尽管在磁盘上您需要引用 dirent.rec_len 才能确定。

偏移量

大小

名称

描述

0x0

__le32

inode

此目录条目指向的 inode 的编号。

0x4

__le16

rec_len

此目录条目的长度。

0x6

__u8

name_len

文件名的长度。

0x7

__u8

file_type

文件类型代码,请参见下面的 ftype 表。

0x8

char

name[EXT4_NAME_LEN]

文件名。

目录文件类型是以下值之一

描述

0x0

未知。

0x1

常规文件。

0x2

目录。

0x3

字符设备文件。

0x4

块设备文件。

0x5

FIFO。

0x6

套接字。

0x7

符号链接。

为了支持既加密又区分大小写的目录,我们还必须在目录条目中包含哈希信息。 我们将 ext4_extended_dir_entry_2 附加到 ext4_dir_entry_2,但点和点点的条目除外,它们保持不变。 该结构紧跟在 name 之后,并包含在 rec_len 列出的大小中。 如果目录条目使用此扩展,则最多可为 271 字节。

偏移量

大小

名称

描述

0x0

__le32

hash

目录名称的哈希值

0x4

__le32

minor_hash

目录名称的次要哈希值

为了将校验和添加到这些经典目录块,一个伪造的 struct ext4_dir_entry 被放置在每个叶块的末尾以保存校验和。 目录条目长 12 字节。 inode 编号和 name_len 字段设置为零,以欺骗旧软件忽略表面上为空的目录条目,并且校验和存储在名称通常所在的位置。 该结构为 struct ext4_dir_entry_tail

偏移量

大小

名称

描述

0x0

__le32

det_reserved_zero1

Inode 编号,必须为零。

0x4

__le16

det_rec_len

此目录条目的长度,必须为 12。

0x6

__u8

det_reserved_zero2

文件名长度,必须为零。

0x7

__u8

det_reserved_ft

文件类型,必须为 0xDE。

0x8

__le32

det_checksum

目录叶块校验和。

叶目录块校验和是针对 FS UUID、目录的 inode 编号、目录的 inode 世代编号和整个目录条目块(直到但不包括)伪造的目录条目计算得出的。

4.3.2. 哈希树目录

目录条目的线性数组对于性能而言并不是很好,因此向 ext3 添加了一个新功能,以提供基于目录条目名称哈希的更快(但很奇怪)的平衡树。 如果 inode 中设置了 EXT4_INDEX_FL (0x1000) 标志,则此目录使用哈希 btree (htree) 来组织和查找目录条目。 为了向后与 ext2 只读兼容,此树实际上隐藏在目录文件中,伪装成“空”目录数据块! 先前说过,线性目录条目表的末尾用指向 inode 0 的条目表示; 这(被滥用)来欺骗旧的线性扫描算法,使其认为目录块的其余部分是空的,从而继续前进。

树的根始终位于目录的第一个数据块中。 根据 ext2 惯例,“.”和“..”条目必须出现在第一个块的开头,因此将它们作为两个 struct ext4_dir_entry_2 s 放置在此处,而不是存储在树中。 根节点的其余部分包含有关树的元数据,最后包含哈希 -> 块映射以查找 htree 中较低的节点。 如果 dx_root.info.indirect_levels 为非零,则 htree 有两层; 根节点的映射指向的数据块是一个内部节点,该节点由次要哈希索引。 此树中的内部节点包含一个归零的 struct ext4_dir_entry_2,后跟一个次要哈希 -> 块映射以查找叶节点。 叶节点包含所有 struct ext4_dir_entry_2 的线性数组; 所有这些条目(大概)都哈希到相同的值。 如果存在溢出,则条目会简单地溢出到下一个叶节点中,并且(在内部节点映射中)获得此下一个叶节点的哈希的最低有效位已设置。

要将目录作为 htree 遍历,代码会计算所需文件名的哈希值,并使用它来查找相应的块号。 如果树是平坦的,则该块是可以搜索的目录条目的线性数组; 否则,将计算文件名的次要哈希,并将其与第二个块进行比较以查找相应的第三个块号。 第三个块号将是目录条目的线性数组。

要将目录作为线性数组遍历(例如旧代码),代码只需读取目录中的每个数据块。 用于 htree 的块将看起来没有条目(除了“.”和“..”),因此只有叶节点似乎具有任何有趣的内容。

htree 的根在 struct dx_root 中,该结构是数据块的完整长度

偏移量

Type

名称

描述

0x0

__le32

dot.inode

此目录的 inode 编号。

0x4

__le16

dot.rec_len

此记录的长度,12。

0x6

u8

dot.name_len

名称的长度,1。

0x7

u8

dot.file_type

此条目的文件类型,0x2(目录)(如果设置了功能标志)。

0x8

char

dot.name[4]

“.000”

0xC

__le32

dotdot.inode

父目录的 inode 编号。

0x10

__le16

dotdot.rec_len

block_size - 12。 记录长度足够长,可以覆盖所有 htree 数据。

0x12

u8

dotdot.name_len

名称的长度,2。

0x13

u8

dotdot.file_type

此条目的文件类型,0x2(目录)(如果设置了功能标志)。

0x14

char

dotdot_name[4]

“..00”

0x18

__le32

struct dx_root_info.reserved_zero

零。

0x1C

u8

struct dx_root_info.hash_version

哈希类型,请参见下面的 dirhash 表。

0x1D

u8

struct dx_root_info.info_length

树信息的长度,0x8。

0x1E

u8

struct dx_root_info.indirect_levels

htree 的深度。 如果设置了 INCOMPAT_LARGEDIR 功能,则不能大于 3; 否则不能大于 2。

0x1F

u8

struct dx_root_info.unused_flags

0x20

__le16

limit

可以跟随此标头的 dx_entries 的最大数量,外加 1 个标头本身。

0x22

__le16

count

跟随此标头的 dx_entries 的实际数量,外加 1 个标头本身。

0x24

__le32

block

与 hash=0 一起的块号(在目录文件中)。

0x28

struct dx_entry

entries[0]

尽可能多的 8 字节 struct dx_entry 适合数据块的其余部分。

目录哈希是以下值之一

描述

0x0

旧版。

0x1

半 MD4。

0x2

Tea。

0x3

旧版,无符号。

0x4

半 MD4,无符号。

0x5

Tea,无签名。

0x6

Siphash。

htree 的内部节点记录为 struct dx_node,它也是数据块的完整长度

偏移量

Type

名称

描述

0x0

__le32

fake.inode

零,使其看起来好像未使用此条目。

0x4

__le16

fake.rec_len

块的大小,以隐藏所有 dx_node 数据。

0x6

u8

name_len

零。 此“未使用”目录条目没有名称。

0x7

u8

file_type

零。 此“未使用”目录条目没有文件类型。

0x8

__le16

limit

可以跟随此标头的 dx_entries 的最大数量,外加 1 个标头本身。

0xA

__le16

count

跟随此标头的 dx_entries 的实际数量,外加 1 个标头本身。

0xE

__le32

block

与此块的最低哈希值一起使用的块号(在目录文件中)。 此值存储在父块中。

0x12

struct dx_entry

entries[0]

尽可能多的 8 字节 struct dx_entry 适合数据块的其余部分。

存在于 struct dx_rootstruct dx_node 中的哈希映射记录为 struct dx_entry,长度为 8 字节

偏移量

Type

名称

描述

0x0

__le32

hash

哈希码。

0x4

__le32

block

htree 中下一个节点的块号(在目录文件中,而不是文件系统块)。

(如果您认为这一切都非常聪明和奇怪,作者也是这么认为的。)

如果启用了元数据校验和,则目录块的最后 8 个字节(恰好是一个 dx_entry 的长度)用于存储 struct dx_tail,其中包含校验和。 根据需要调整 dx_root/dx_node 结构中的 limitcount 条目,以将 dx_tail 放入块中。 如果没有空间用于 dx_tail,则会通知用户运行 e2fsck -D 以重建目录索引(这将确保有空间用于校验和。 dx_tail 结构长 8 个字节,如下所示

偏移量

Type

名称

描述

0x0

u32

dt_reserved

零。

0x4

__le32

dt_checksum

htree 目录块的校验和。

校验和是针对 FS UUID、htree 索引标头(dx_root 或 dx_node)、所有正在使用的 htree 索引 (dx_entry) 和尾块 (dx_tail) 计算得出的。

4.4. 扩展属性

扩展属性 (xattrs) 通常存储在磁盘上的单独数据块中,并通过 inode.i_file_acl* 从 inode 引用。 扩展属性的第一个用途似乎是存储文件 ACL 和其他安全数据 (selinux)。 使用 user_xattr 挂载选项,用户可以存储扩展属性,只要所有属性名称都以“user”开头即可; 从 Linux 3.0 开始,此限制似乎已消失。

可以在两个地方找到扩展属性。 第一个地方是在每个 inode 条目的末尾和下一个 inode 条目的开头之间。 例如,如果 inode.i_extra_isize = 28 且 sb.inode_size = 256,则有 256 - (128 + 28) = 100 个字节可用于 inode 内扩展属性存储。 可以在其中找到扩展属性的第二个位置是在 inode.i_file_acl 指向的块中。 从 Linux 3.11 开始,此块不可能包含指向第二个扩展属性块(甚至群集的其余块)的指针。 理论上,每个属性的值都可以存储在单独的数据块中,但从 Linux 3.11 开始,代码不允许这样做。

通常假定键是 ASCIIZ 字符串,而值可以是字符串或二进制数据。

扩展属性(存储在 inode 之后)具有一个长度为 4 字节的标头 ext4_xattr_ibody_header

偏移量

Type

名称

描述

0x0

__le32

h_magic

用于标识的幻数,0xEA020000。 此值由 Linux 驱动程序设置,尽管 e2fsprogs 似乎不检查它(?)

扩展属性块的起始位置位于 struct ext4_xattr_header 中,其长度为 32 字节。

偏移量

Type

名称

描述

0x0

__le32

h_magic

用于识别的魔数,为 0xEA020000。

0x4

__le32

h_refcount

引用计数。

0x8

__le32

h_blocks

使用的磁盘块数量。

0xC

__le32

h_hash

所有属性的哈希值。

0x10

__le32

h_checksum

扩展属性块的校验和。

0x14

__u32

h_reserved[3]

零。

校验和是根据 FS UUID、扩展属性块的 64 位块号以及整个块(header + entries)计算得出的。

struct ext4_xattr_headerstruct ext4_xattr_ibody_header 之后是一个 struct ext4_xattr_entry 数组;这些条目中的每一个都至少有 16 字节长。当存储在外部块中时,struct ext4_xattr_entry 条目必须按排序顺序存储。排序顺序为 e_name_index,然后是 e_name_len,最后是 e_name。存储在 inode 中的属性不需要按排序顺序存储。

偏移量

Type

名称

描述

0x0

__u8

e_name_len

名称的长度。

0x1

__u8

e_name_index

属性名称索引。 下面会讨论。

0x2

__le16

e_value_offs

此属性的值在其存储的磁盘块上的位置。 多个属性可以共享相同的值。 对于 inode 属性,此值相对于第一个条目的起始位置;对于块,此值相对于块的起始位置(即 header)。

0x4

__le32

e_value_inum

存储值的 inode。 零表示该值与此条目位于同一块中。 仅当启用了 INCOMPAT_EA_INODE 功能时才使用此字段。

0x8

__le32

e_value_size

属性值的长度。

0xC

__le32

e_hash

属性名称和属性值的哈希值。 内核不会更新 inode 内属性的哈希值,因此对于这种情况,该值必须为零,因为 e2fsck 会验证任何非零哈希值,无论 xattr 位于何处。

0x10

char

e_name[e_name_len]

属性名称。 不包括尾随 NULL。

属性值可以位于条目表之后。 似乎需要将它们对齐到 4 字节边界。 这些值从块的末尾开始存储,并向 xattr_header/xattr_entry 表增长。 当两者发生冲突时,溢出会被放入单独的磁盘块中。 如果磁盘块已满,则文件系统将返回 -ENOSPC。

ext4_xattr_entry 的前四个字段设置为零以标记键列表的结尾。

4.4.1. 属性名称索引

从逻辑上讲,扩展属性是一系列键=值对。 假设键是 NULL 结尾的字符串。 为了减少键占用的磁盘空间量,键字符串的开头与属性名称索引匹配。 如果找到匹配项,则设置属性名称索引字段,并从键名中删除匹配的字符串。 以下是将名称索引值映射到键前缀的映射

名称索引

键前缀

0

(无前缀)

1

“user.”

2

“system.posix_acl_access”

3

“system.posix_acl_default”

4

“trusted.”

6

“security.”

7

“system.” (仅限 inline_data?)

8

“system.richacl” (仅限 SuSE 内核?)

例如,如果属性键为 “user.fubar”,则属性名称索引设置为 1,并且 “fubar” 名称记录在磁盘上。

4.4.2. POSIX ACL

POSIX ACL 存储在 Linux 内核(和 libacl)内部 ACL 格式的简化版本中。 主要区别在于版本号不同 (1),并且仅为命名的用户和组 ACL 存储 e_id 字段。