原子块写入

简介

原子(无撕裂)块写入确保要么整个写入都提交到磁盘,要么都不提交。这可以防止在断电或系统崩溃时发生“撕裂写入”。ext4 文件系统在支持硬件原子写入的底层存储设备上,支持对带有区段(extents)的常规文件进行原子写入(仅限于直接 I/O)。这通过以下两种方式支持:

  1. 单文件系统块原子写入:EXT4 自 v6.13 起支持单个文件系统块的原子写入操作。在此模式下,原子写入单元的最小和最大大小都设置为文件系统块大小。例如,在 64KB 页面大小的系统上,使用 16KB 文件系统块大小执行 16KB 的原子写入是可行的。

  2. 使用 Bigalloc 的多文件系统块原子写入:EXT4 现在还支持使用称为 bigalloc 的功能跨多个文件系统块的原子写入。原子写入单元的最小和最大大小根据底层设备支持的原子写入单元限制,由文件系统块大小和簇大小决定。

要求

ext4 中原子写入的基本要求

  1. 必须启用区段(extents)功能(ext4 默认启用)

  2. 底层块设备必须支持原子写入

  3. 对于单文件系统块原子写入

    1. 具有适当块大小的文件系统(最大可达页面大小)

  4. 对于多文件系统块原子写入

    1. 必须启用 bigalloc 功能

    2. 必须正确配置簇大小

注意:EXT4 不支持基于软件或写时复制(COW)的原子写入,这意味着 ext4 上的原子写入仅在底层存储设备支持时才受支持。

多文件系统块实现细节

bigalloc 功能将 ext4 的分配单位更改为多个文件系统块,也称为簇。使用 bigalloc,块位图中的每个位代表一个簇(2 的幂个块),而不是单个文件系统块。EXT4 支持使用 bigalloc 进行多文件系统块原子写入,但受以下约束:最小原子写入大小是文件系统块大小与最小硬件原子写入单元中的较大者;最大原子写入大小是 bigalloc 簇大小与最大硬件原子写入单元中的较小者。Bigalloc 确保所有分配都与簇大小对齐,如果分区/逻辑卷的起始位置本身已正确对齐,则满足硬件设备的 LBA 对齐要求。

以下是 bigalloc 中针对原子写入的块分配策略:

  • 对于已完全映射区段的区域,无需额外操作

  • 对于追加写入,会分配一个新的已映射区段

  • 对于完全是空洞的区域,会创建未写入区段

  • 对于大型未写入区段,该区段会被拆分为两个适当请求大小的未写入区段

  • 对于混合映射区域(空洞、未写入区段或已映射区段的组合),会循环调用 ext4_map_blocks() 并带有 EXT4_GET_BLOCKS_ZERO 标志,通过向其写入零并将范围内找到的任何未写入区段转换为已写入区段,从而将该区域转换为单个连续的已映射区段。

注意:对单个连续的底层区段进行写入,无论是已映射还是未写入的,本身并没有问题。但是,在执行原子写入时,必须避免写入混合映射区域(即包含已映射和未写入区段组合的区域)。

原因是,当通过 pwritev2() 并带有 RWF_ATOMIC 标志发出原子写入时,要求要么所有数据都被写入,要么都不写入。在写入操作期间发生系统崩溃或意外断电的情况下,受影响的区域(在后续读取时)必须反映完整的旧数据或完整的最新数据,但绝不能是两者的混合。

为了强制执行此保证,我们确保在写入任何数据之前,写入目标由单个连续的区段支持。这至关重要,因为 ext4 会将未写入区段转换为已写入区段的操作推迟到 I/O 完成路径(通常在 ->end_io() 中)进行。如果允许对混合映射区域(包含已映射和未写入区段)进行写入,并且在写入过程中发生故障,系统在重启后可能会观察到部分更新的区域,即已映射区域上的新数据,以及未标记为已写入的未写入区段上的陈旧(旧)数据。这违反了原子性和/或撕裂写入预防保证。

为了防止此类撕裂写入,ext4 通过 ext4_map_blocks_atomic()ext4_iomap_alloc 中为整个请求区域主动分配一个单一的连续区段。如果分配发生在混合映射上,EXT4 还会强制提交当前的日志事务。这确保了在该范围内的任何挂起的元数据更新(例如未写入区段到已写入区段的转换)在执行实际写入 I/O 之前与文件数据块处于一致状态。如果提交失败,则必须中止整个 I/O 以防止任何可能的撕裂写入。只有在此步骤之后,iomap 才执行实际的数据写入操作。

处理跨叶块的拆分区段

可能存在一种特殊边缘情况,即逻辑上和物理上连续的区段存储在磁盘区段树的不同叶节点中。发生这种情况的原因是,磁盘区段树的合并只发生在叶块内部,除非是两级树,它可能被合并并完全折叠到 inode 中。如果存在这样的布局,并且在最坏的情况下,由于内存压力导致区段状态缓存条目被回收,ext4_map_blocks() 可能永远不会为这些拆分的叶区段返回单个连续区段。

为了解决这种边缘情况,添加了一个新的获取块标志 EXT4_GET_BLOCKS_QUERY_LEAF_BLOCKS flag,以增强 ext4_map_query_blocks() 的查找行为。

这个新的获取块标志允许 ext4_map_blocks() 首先检查区段状态缓存中是否存在整个范围的条目。如果不存在,它会使用 ext4_map_query_blocks() 查询磁盘区段树。如果找到的区段位于叶节点的末尾,它会探测下一个逻辑块(lblk)以检测相邻叶中的连续区段。

目前为了保持效率,只查询一个额外的叶块,因为原子写入通常被限制在较小的大小(例如 [blocksize, clustersize])内。

处理日志事务

为了支持多文件系统块原子写入,我们确保在以下阶段保留足够的日志信用:

  1. ext4_iomap_alloc() 中的块分配时间。我们首先查询底层请求范围内是否存在混合映射。如果存在,则我们保留最多 m_len 的信用,假设每个交替的块都可以是一个未写入区段,后跟一个空洞。

  2. ->end_io() 调用期间,我们确保启动单个事务来执行未写入到已写入的转换。转换循环主要仅用于处理跨叶块的拆分区段。

如何操作

创建支持原子写入的文件系统

首先检查块设备支持的原子写入单元。有关更多详细信息,请参阅硬件支持

对于具有较大块大小的单文件系统块原子写入(在块大小小于页面大小的系统上)

# Create an ext4 filesystem with a 16KB block size
# (requires page size >= 16KB)
mkfs.ext4 -b 16384 /dev/device

对于使用 bigalloc 的多文件系统块原子写入

# Create an ext4 filesystem with bigalloc and 64KB cluster size
mkfs.ext4 -F -O bigalloc -b 4096 -C 65536 /dev/device

其中 -b 指定块大小,-C 指定以字节为单位的簇大小,-O bigalloc 启用 bigalloc 功能。

应用程序接口

应用程序可以使用 pwritev2() 系统调用与 RWF_ATOMIC 标志来执行原子写入

pwritev2(fd, iov, iovcnt, offset, RWF_ATOMIC);

写入必须与文件系统的块大小对齐,并且不能超过文件系统的最大原子写入单元大小。有关更多详细信息,请参阅 generic_atomic_write_valid()

带有 STATX_WRITE_ATOMIC 标志的 statx() 系统调用可以提供以下详细信息:

  • stx_atomic_write_unit_min:原子写入请求的最小大小。

  • stx_atomic_write_unit_max:原子写入请求的最大大小。

  • stx_atomic_write_segments_max:段的上限。可以聚合成一个写入操作的独立内存缓冲区的数量(例如,IOV_ITERiovcnt 参数)。目前,这始终设置为一。

如果支持原子写入,statx->attributes 中的 STATX_ATTR_WRITE_ATOMIC 标志会设置。

硬件支持

底层存储设备必须支持原子写入操作。现代 NVMe 和 SCSI 设备通常提供此功能。Linux 内核通过 sysfs 暴露此信息:

  • /sys/block/<device>/queue/atomic_write_unit_min - 最小原子写入大小

  • /sys/block/<device>/queue/atomic_write_unit_max - 最大原子写入大小

这些属性的非零值表示设备支持原子写入。

另请参阅