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 功能标志,则该文件在磁盘上占用 |
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 |
所有目录条目数据都应同步写入(请参见 |
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 是快照 ( |
0x04000000 |
正在删除快照 ( |
0x08000000 |
快照收缩已完成 ( |
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 |
|
1901-12-13 至 1969-12-31 |
0 0 |
0 |
0 |
|
1970-01-01 至 2038-01-19 |
0 1 |
1 |
0x100000000 |
|
2038-01-19 至 2106-02-07 |
0 1 |
0 |
0x100000000 |
|
2106-02-07 至 2174-02-25 |
1 0 |
1 |
0x200000000 |
|
2174-02-25 至 2242-03-16 |
1 0 |
0 |
0x200000000 |
|
2242-03-16 至 2310-04-04 |
1 1 |
1 |
0x300000000 |
|
2310-04-04 至 2378-04-22 |
1 1 |
0 |
0x300000000 |
|
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.1. 符号链接¶
如果目标字符串小于 60 个字节,符号链接的目标将存储在此字段中。否则,将使用 extent 或块映射来分配数据块以存储链接目标。
4.2.2. 直接/间接块寻址¶
在 ext2/3 中,文件块号通过(最多)三级 1-1 块映射映射到逻辑块号。要查找存储特定文件块的逻辑块,代码将遍历这个越来越复杂的结构。请注意,既没有幻数也没有校验和来提供任何程度的信心,证明该块不是充满了垃圾。
i.i_block 偏移量 |
它指向哪里 |
||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 到 11 |
直接映射到文件块 0 到 11。 |
||||||||||||
12 |
间接块:(文件块 12 到 (
|
||||||||||||
13 |
双重间接块:(文件块
|
||||||||||||
14 |
三重间接块:(文件块 (
|
请注意,使用这种块映射方案,即使对于一个大的连续文件,也需要填写大量的映射数据!这种效率低下导致了 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_entries
个 struct ext4_extent_idx
的实例;这些索引条目中的每一个都指向一个包含 extent 树中更多节点的块。如果节点是叶节点 (eh.eh_depth == 0
),则标头后跟 eh.eh_entries
个 struct 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 层深:逻辑块号最多可以是 |
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 长度为 |
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 字节 |
目录哈希是以下值之一
值 |
描述 |
---|---|
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_root
和 struct dx_node
中的哈希映射记录为 struct dx_entry
,长度为 8 字节
偏移量 |
Type |
名称 |
描述 |
---|---|---|---|
0x0 |
__le32 |
hash |
哈希码。 |
0x4 |
__le32 |
block |
htree 中下一个节点的块号(在目录文件中,而不是文件系统块)。 |
(如果您认为这一切都非常聪明和奇怪,作者也是这么认为的。)
如果启用了元数据校验和,则目录块的最后 8 个字节(恰好是一个 dx_entry 的长度)用于存储 struct dx_tail
,其中包含校验和。 根据需要调整 dx_root/dx_node 结构中的 limit
和 count
条目,以将 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_header
或 struct 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
字段。