日志 (jbd2)

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

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

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

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

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

注意:ext4 和 ocfs2 都使用 jbd2。

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

布局

一般来说,日志采用以下格式:

超级块

descriptor_block(数据块或撤销块)[更多数据或撤销] commmit_block

[更多事务...]

一个事务

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

外部日志

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

1024 字节填充

ext4 超级块

日志超级块

descriptor_block(数据块或撤销块)[更多数据或撤销] commmit_block

[更多事务...]

一个事务

块头

日志中的每个块都以一个通用的 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

块撤销记录。通过使日志跳过写入随后被重写的块,从而加快恢复速度。

超级块

日志的超级块比 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

描述符块

描述符块包含一个日志块标签数组,这些标签描述了日志中后续数据块的最终位置。描述符块是开放编码的,而不是由数据结构完全描述,但无论如何,这里是块结构。描述符块至少占用 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 + 描述符块的校验和,此字段设置为零。

数据块

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

撤销块

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

**注意**:此机制**不**用于表达“此日志块已被其他日志块取代”,正如作者 (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 + 撤销块的校验和

提交块

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

提交块由 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

事务提交的时间,自 epoch 以来的秒数。

0x38

__be32

h_commit_nsec

上述时间戳的纳秒部分。

快速提交

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

标签

含义

值结构

描述

EXT4_FC_TAG_HEAD

快速提交区域头部

struct ext4_fc_head

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

EXT4_FC_TAG_ADD_RANGE

向 inode 添加 extent

struct ext4_fc_add_range

存储要在此 inode 中添加的 inode 号和 extent。

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。

快速提交回放幂等性

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

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

  • 将目录项 b 链接到 inode 10。

  • 解除目录项 a 的链接。

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

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

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

  1. rm A

  2. mv B A

  3. read A

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

(假设在回放之前,目录项 A 链接到 inode 10,目录项 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 标签来修复。因此,通过将非幂等过程转换为一系列幂等结果,快速提交确保了回放期间的幂等性。

日志检查点

对日志进行检查点操作可确保所有事务及其关联的缓冲区都已提交到磁盘。正在进行的事务将被等待并包含在检查点中。检查点在文件系统的关键更新期间内部使用,包括日志恢复、文件系统大小调整以及释放 `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 不能同时设置。此 ioctl 在对系统进行快照时或为了符合内容删除 SLO 时可能很有用。