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 功能标志,则该文件在磁盘上占用 |
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 |
所有目录项数据都应同步写入(请参阅 |
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 |
扩展属性块(历史上是文件 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 |
|
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 位内核错误地将额外的纪元位 1,1 用于 1901 年至 1970 年之间的日期。在某个时候,内核将被修复,并且 e2fsck 将修复这种情况,前提是在 2310 年之前运行它。
4.2. inode.i_block 的内容¶
根据 inode 描述的文件类型,inode.i_block
中的 60 个字节的存储空间可以以不同的方式使用。一般来说,常规文件和目录将使用它来存储文件块索引信息,而特殊文件将使用它来存储特殊用途的信息。
4.2.1. 符号链接¶
如果目标字符串的长度小于 60 字节,则符号链接的目标将存储在此字段中。否则,将使用范围或块映射来分配数据块以存储链接目标。
4.2.2. 直接/间接块寻址¶
在 ext2/3 中,文件块号通过(最多)三级 1-1 块映射映射到逻辑块号。要查找存储特定文件块的逻辑块,代码将遍历此日益复杂的结构。请注意,既没有魔术数字也没有校验和来提供任何级别的置信度,即该块不是充满了垃圾。
i.i_block 偏移量 |
它指向哪里 |
||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 到 11 |
直接映射到文件块 0 到 11。 |
||||||||||||
12 |
间接块:(文件块 12 到 (
|
||||||||||||
13 |
双重间接块:(文件块
|
||||||||||||
14 |
三重间接块:(文件块 (
|
请注意,使用此块映射方案,即使对于一个大的连续文件,也需要填充大量的映射数据!这种低效率导致了下面讨论的范围映射方案的创建。
还要注意,使用此映射方案的文件不能放置在高于 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_entries
个 struct ext4_extent_idx
实例;这些索引条目中的每一个都指向一个包含范围树中更多节点的块。如果节点是叶子节点 ( eh.eh_depth == 0
),则头部后面跟随着 eh.eh_entries
个 struct 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 层深:逻辑块号最多可以是 |
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,则该范围未初始化,实际范围长度为 |
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 字节的 |
目录哈希是以下值之一
值 |
描述 |
---|---|
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_root
和 struct dx_node
中存在的哈希映射被记录为 struct dx_entry
,其长度为 8 个字节
偏移量 |
类型 |
名称 |
描述 |
---|---|---|---|
0x0 |
__le32 |
hash |
哈希码。 |
0x4 |
__le32 |
block |
htree 中下一个节点的块号(在目录文件中,而不是文件系统块)。 |
(如果您认为这一切都非常聪明和特别,作者也这么认为。)
如果启用了元数据校验和,则目录块的最后 8 个字节(精确地为 dx_entry 的长度)用于存储 struct dx_tail
,其中包含校验和。limit
和 count
条目在 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_header
或 struct 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 存储。