4. 动态结构

当文件和块被分配给文件时,动态元数据会被即时创建。

4.1. 索引节点

在常规的 UNIX 文件系统中,inode 存储所有与文件相关的元数据(时间戳、块映射、扩展属性等),而不是目录项。要查找与文件相关联的信息,必须遍历目录文件以找到与文件关联的目录项,然后加载 inode 以找到该文件的元数据。ext4 似乎作弊(出于性能原因),通过在目录项中存储文件类型的副本(通常存储在 inode 中)。(将这一切与 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

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

0xC

__le32

i_ctime

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

0x10

__le32

i_mtime

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

0x14

__le32

i_dtime

删除时间,自纪元以来的秒数。

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]

块映射或扩展树。请参阅“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

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

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

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

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

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

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 纪元(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 位。在此“额外”的 32 位字段中,较低两位用于将 32 位秒字段扩展为 34 位宽;较高的 30 位用于提供纳秒时间戳精度。因此,时间戳应在 2446 年 5 月之前不会溢出。dtime 没有扩展。还有一个第五个时间戳用于记录 inode 创建时间 (crtime);此字段为 64 位宽,并以与 64 位 [cma]time 相同的方式解码。crtime 和 dtime 都无法通过常规 stat() 接口访问,但 debugfs 会报告它们。

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

额外的纪元位

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 位内核错误地将额外的纪元位 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 个)

请注意,使用此块映射方案,即使对于一个大的连续文件,也需要填充大量的映射数据!这种低效率导致了下面讨论的范围映射方案的创建。

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

4.2.3. 范围树

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

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

范围树头记录在 struct ext4_extent_header 中,长度为 12 字节

偏移量

大小

名称

描述

0x0

__le16

eh_magic

魔数,0xF30A。

0x2

__le16

eh_entries

头部后面有效条目的数量。

0x4

__le16

eh_max

头部后面可能跟的最大条目数。

0x6

__le16

eh_depth

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

0x8

__le32

eh_generation

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

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

偏移量

大小

名称

描述

0x0

__le32

ei_block

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

0x4

__le32

ei_leaf_lo

树中下一层较低的范围节点的块号的低 32 位。指向的树节点可以是另一个内部节点或下面描述的叶子节点。

0x8

__le16

ei_leaf_hi

前一个字段的高 16 位。

0xA

__u16

ei_unused

范围树的叶子节点被记录为 struct ext4_extent,长度也为 12 字节

偏移量

大小

名称

描述

0x0

__le32

ee_block

此范围覆盖的第一个文件块号。

0x4

__le16

ee_len

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

0x6

__le16

ee_start_hi

此范围指向的块号的高 16 位。

0x8

__le32

ee_start_lo

此范围指向的块号的低 32 位。

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

struct ext4_extent_tail 的长度为 4 个字节

偏移量

大小

名称

描述

0x0

__le32

eb_checksum

范围块的校验和,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) 标志,则此目录将使用哈希 b 树 (htree) 来组织和查找目录项。为了与 ext2 向后兼容只读,这棵树实际上隐藏在目录文件中,伪装成“空”目录数据块!前面说过,线性目录项表的末尾用指向 inode 0 的条目表示;这被(滥用)来欺骗旧的线性扫描算法,让它认为目录块的其余部分是空的,从而继续前进。

树的根始终位于目录的第一个数据块中。按照 ext2 的惯例,“.” 和 “..” 条目必须出现在第一个块的开头,因此它们以两个 struct ext4_dir_entry_2 的形式放在这里,而不是存储在树中。根节点的其余部分包含有关树的元数据,最后是一个 hash->block 映射,用于查找 htree 中较低的节点。如果 dx_root.info.indirect_levels 非零,则 htree 有两个级别;根节点的映射指向的数据块是一个内部节点,该节点由一个次要哈希值索引。此树中的内部节点包含一个清零的 struct ext4_dir_entry_2,后跟一个次要哈希->块映射,用于查找叶节点。叶节点包含所有 struct ext4_dir_entry_2 的线性数组;所有这些条目(大概)都哈希为相同的值。如果发生溢出,则条目只会溢出到下一个叶节点,并且(在内部节点映射中)使我们到达下一个叶节点的哈希的最低有效位会被设置。

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

要将目录作为线性数组遍历(例如旧代码所做的),代码只需读取目录中的每个数据块。用于 htree 的块将显示没有条目(除了 '.' 和 '..'),因此只有叶节点才会显示任何有趣的内容。

htree 的根位于 struct dx_root 中,其长度为一个数据块的完整长度

偏移量

类型

名称

描述

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,其长度也是一个数据块的完整长度

偏移量

类型

名称

描述

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 个字节

偏移量

类型

名称

描述

0x0

__le32

hash

哈希码。

0x4

__le32

block

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

(如果您认为这一切都非常聪明和特别,作者也这么认为。)

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

偏移量

类型

名称

描述

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

偏移量

类型

名称

描述

0x0

__le32

h_magic

用于识别的魔术数字,0xEA020000。此值由 Linux 驱动程序设置,但 e2fsprogs 似乎没有检查它 (?)。

扩展属性块的开头位于 struct ext4_xattr_header 中,其长度为 32 个字节

偏移量

类型

名称

描述

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 位块号以及整个块(标头 + 条目)计算校验和。

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 内的属性不需要按排序顺序存储。

偏移量

类型

名称

描述

0x0

__u8

e_name_len

名称的长度。

0x1

__u8

e_name_index

属性名称索引。下面对此进行了讨论。

0x2

__le16

e_value_offs

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

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),并且 e_id 字段仅为命名的用户和组 ACL 存储。