2. 支持的文件操作

下面将讨论 iomap 实现的高级文件操作。

2.1. 缓存 I/O

缓存 I/O 是 Linux 中默认的文件 I/O 路径。文件内容缓存在内存中(“页缓存”)以满足读写请求。脏缓存将在某个时刻写回磁盘,可以通过 fsync 及其变体强制执行。

iomap 实现了几乎所有的 folio 和页缓存管理,而这些在传统 I/O 模型下文件系统必须自行实现。这意味着文件系统无需了解分配、映射、管理最新和脏状态以及页缓存 folio 回写的细节。在传统 I/O 模型下,这是通过缓冲区头的链表来管理的,效率非常低,而 iomap 使用每 folio 位图。除非文件系统明确选择使用缓冲区头,否则它们将不会被使用,这使得缓存 I/O 效率更高,页缓存维护者也更满意。

2.1.1. struct address_space_operations

以下 iomap 函数可以直接从 address_space_operations 结构中引用

  • iomap_dirty_folio

  • iomap_release_folio

  • iomap_invalidate_folio

  • iomap_is_partially_uptodate

以下 address space 操作可以轻松封装

  • read_folio

  • readahead

  • writepages

  • bmap

  • swap_activate

2.1.2. struct iomap_folio_ops

页缓存操作的 ->iomap_begin 函数可以将 struct iomap::folio_ops 字段设置为一个 ops 结构体,以覆盖 iomap 的默认行为

struct iomap_folio_ops {
    struct folio *(*get_folio)(struct iomap_iter *iter, loff_t pos,
                               unsigned len);
    void (*put_folio)(struct inode *inode, loff_t pos, unsigned copied,
                      struct folio *folio);
    bool (*iomap_valid)(struct inode *inode, const struct iomap *iomap);
};

iomap 调用这些函数

  • get_folio:在开始写入之前,用于分配并返回一个锁定的 folio 的活动引用。如果未提供此函数,iomap 将调用 iomap_get_folio。这可用于为写入设置每 folio 文件系统状态

  • put_folio:在页缓存操作完成后,用于解锁并释放 folio。如果未提供此函数,iomap 将自行调用 folio_unlockfolio_put。这可用于提交->get_folio 设置的每 folio 文件系统状态。

  • iomap_valid:文件系统在 ->iomap_begin->iomap_end 之间可能不持有锁,因为页缓存操作会获取 folio 锁、在用户空间页面上发生页错误、启动回写以回收内存,或执行其他耗时操作。如果文件的空间映射数据是可变的,则在分配、安装和锁定该 folio 所需的时间内,特定页缓存 folio 的映射可能会发生变化

    对于页缓存,如果回写不获取 i_rwseminvalidate_lock 并更新映射信息,则可能发生竞争。如果文件系统允许并发写入,也可能发生竞争。对于此类文件,在获取 folio 锁后必须重新验证映射,以便 iomap 能够正确管理 folio。

    fsdax 不需要这种重新验证,因为它没有回写,也不支持未写入的区段。

    受此类竞争影响的文件系统必须提供一个 ->iomap_valid 函数来判断映射是否仍然有效。如果映射无效,将再次采样映射。

    为了支持做出有效性决策,文件系统的 ->iomap_begin 函数在填充其他 iomap 字段的同时,可以设置 struct iomap::validity_cookie。一个简单的验证 cookie 实现是一个序列计数器。如果文件系统在每次修改 inode 的区段映射时递增序列计数器,则可以在 ->iomap_begin 期间将其放置在 struct iomap::validity_cookie 中。如果在将映射传回 ->iomap_valid 时,发现 cookie 中的值与文件系统持有的值不同,则应认为 iomap 过时,验证失败。

这些 struct kiocb 标志对于 iomap 的缓存 I/O 至关重要

  • IOCB_NOWAIT:开启 IOMAP_NOWAIT

  • IOCB_DONTCACHE:开启 IOMAP_DONTCACHE

2.1.3. 内部每 Folio 状态

如果文件系统块大小与页缓存 folio 的大小匹配,则假定所有磁盘 I/O 操作都将作用于整个 folio。对于这种情况,只需 folio 的最新状态(内存内容至少与磁盘上的内容一样新)和脏状态(内存内容比磁盘上的内容新)即可。

如果文件系统块大小小于页缓存 folio 的大小,iomap 会自行跟踪每文件系统块的最新状态和脏状态。这使得 iomap 能够处理“bs < ps”的文件系统以及页缓存中的大 folio。

iomap 内部跟踪每个文件系统块的两个状态位

  • uptodate:iomap 将尝试保持 folio 完全最新。如果发生读(预读)错误,则不会将这些文件系统块标记为最新。当 folio 中的所有文件系统块都最新时,folio 本身将被标记为最新。

  • dirty:当程序写入文件时,iomap 将设置每块的脏状态。当 folio 中的任何文件系统块变脏时,folio 本身将被标记为脏。

iomap 还跟踪正在进行的读写磁盘 I/O 量。这种结构比 struct buffer_head 轻量得多,因为它每个 folio 只有一个,并且每个文件系统块的开销是两位,而不是 104 字节。

希望在页缓存中启用大 folio 的文件系统应在初始化核心 inode 时调用 mapping_set_large_folios

2.1.4. 缓存预读和读取

iomap_readahead 函数启动对页缓存的预读。iomap_read_folio 函数将一个 folio 的数据读入页缓存。->iomap_beginflags 参数将设置为零。页缓存会在调用文件系统之前获取其所需的任何锁。

2.1.5. 缓存写入

iomap_file_buffered_write 函数将一个 iocb 写入页缓存。IOMAP_WRITEIOMAP_WRITE | IOMAP_NOWAIT 将作为 flags 参数传递给 ->iomap_begin。调用者通常在调用此函数之前以共享或排他模式获取 i_rwsem

2.1.5.1. mmap 写错误

iomap_page_mkwrite 函数处理页缓存中 folio 的写错误。IOMAP_WRITE | IOMAP_FAULT 将作为 flags 参数传递给 ->iomap_begin。调用者通常在调用此函数之前以共享或排他模式获取 mmap invalidate_lock

2.1.5.2. 缓存写入失败

对页缓存进行短写入后,未写入的区域将不会被标记为脏。文件系统必须安排取消此类保留,因为回写不会消耗该保留。iomap_write_delalloc_release 可以从 ->iomap_end 函数中调用,以查找缓存了新(IOMAP_F_NEW)延迟分配映射的 folio 的所有干净区域。它会获取 invalidate_lock

文件系统必须提供一个函数 punch,以便在此状态下为每个文件范围调用。此函数只能删除延迟分配保留,以防另一个与当前线程竞争的线程成功写入同一区域并触发回写以将脏数据刷新到磁盘。

2.1.5.3. 文件操作的清零

文件系统可以调用 iomap_zero_range 来对非截断文件操作的页缓存进行清零,这些操作未对齐文件系统块大小。IOMAP_ZERO 将作为 flags 参数传递给 ->iomap_begin。调用者通常在调用此函数之前以排他模式持有 i_rwseminvalidate_lock

2.1.5.4. 解除 Reflink 文件数据共享

文件系统可以调用 iomap_file_unshare 来强制与另一个文件共享存储的文件抢先将共享数据复制到新分配的存储。 IOMAP_WRITE | IOMAP_UNSHARE 将作为 flags 参数传递给 ->iomap_begin。 调用者通常在调用此函数之前以排他模式持有 i_rwseminvalidate_lock

2.1.6. 截断

文件系统可以在文件截断操作期间调用 iomap_truncate_page,以将页缓存中从 EOF 到文件系统块末尾的字节清零。truncate_setsizetruncate_pagecache 将处理 EOF 块之后的所有内容。IOMAP_ZERO 将作为 flags 参数传递给 ->iomap_begin。调用者通常在调用此函数之前以排他模式持有 i_rwseminvalidate_lock

2.1.7. 页缓存回写

文件系统可以调用 iomap_writepages 来响应将脏页缓存 folio 写回磁盘的请求。mappingwbc 参数应不变地传递。wpc 指针应由文件系统分配并初始化为零。

页缓存将在尝试安排每个 folio 进行回写之前锁定它。它不会锁定 i_rwseminvalidate_lock

即使回写失败,通过下面描述的 ->map_blocks 机制处理的所有 folio 的脏位都将被清除。这是为了防止存储设备故障时出现脏 folio 凝块;一个 -EIO 会被记录下来供用户空间通过 fsync 收集。

ops 结构必须指定,并且如下所示

2.1.7.1. struct iomap_writeback_ops

struct iomap_writeback_ops {
    int (*map_blocks)(struct iomap_writepage_ctx *wpc, struct inode *inode,
                      loff_t offset, unsigned len);
    int (*submit_ioend)(struct iomap_writepage_ctx *wpc, int status);
    void (*discard_folio)(struct folio *folio, loff_t pos);
};

字段如下

  • map_blocks:将 wpc->iomap 设置为由 offsetlen 给出的文件范围(以字节为单位)的空间映射。iomap 为每个脏 folio 中的每个脏文件系统块调用此函数,尽管它会重用 folio 中连续脏文件系统块的映射。不要在此处返回 IOMAP_INLINE 映射;->iomap_end 函数必须处理已写入数据的持久化。不要在此处返回 IOMAP_DELALLOC 映射;iomap 当前需要映射到已分配的空间。如果映射未更改,文件系统可以跳过潜在昂贵的映射查找。这种重新验证必须由文件系统自行实现;目前尚不清楚 iomap::validity_cookie 是否可以为此目的重新使用。此函数必须由文件系统提供。

  • submit_ioend:允许文件系统挂钩到回写 bio 提交。这可能包括预写空间记账更新,或安装自定义的 ->bi_end_io 函数用于内部目的,例如将 ioend 完成推迟到工作队列以在提交 bio 之前从进程上下文运行元数据更新事务。此函数是可选的。

  • discard_folio:在 ->map_blocks 未能为脏 folio 的任何部分安排 I/O 后,iomap 会调用此函数。该函数应丢弃可能已为写入进行的任何保留。该 folio 将被标记为干净,并在页缓存中记录一个 -EIO。文件系统可以使用此回调来移除延迟分配保留,以避免对干净页缓存进行延迟分配保留。此函数是可选的。

2.1.7.2. 页缓存回写完成

为了处理磁盘 I/O 回写完成后必须进行的簿记工作,iomap 创建了 struct iomap_ioend 对象的链,这些对象封装了用于将页缓存数据写入磁盘的 bio。默认情况下,iomap 通过清除附加到 ioend 的 folio 上的回写位来完成回写 ioend。如果写入失败,它还会设置 folio 和地址空间上的错误位。这可以在中断或进程上下文中发生,具体取决于存储设备。

需要更新内部簿记(例如未写入区段转换)的文件系统应提供 ->submit_ioend 函数以将其自己的函数设置为 struct iomap_end::bio::bi_end_io。此函数应在完成其自身工作(例如未写入区段转换)后调用 iomap_finish_ioends

某些文件系统可能希望通过批处理来分摊运行元数据事务的成本,以进行回写后更新。它们还可能要求事务从进程上下文运行,这意味着将批处理传递给工作队列。iomap ioends 包含一个 list_head 以实现批处理。

给定一批 ioend,iomap 提供了一些辅助函数来帮助分摊

  • iomap_sort_ioends:按文件偏移量对列表中所有 ioend 进行排序。

  • iomap_ioend_try_merge:给定一个不在任何列表中的 ioend 和一个单独的已排序 ioend 列表,将列表中开头的尽可能多的 ioend 合并到给定的 ioend 中。ioend 只有在文件范围和存储地址连续;未写入和共享状态相同;并且写入 I/O 结果相同的情况下才能合并。合并的 ioend 成为其自己的列表。

  • iomap_finish_ioends:完成一个可能链接有其他 ioend 的 ioend。

2.2. 直接 I/O

在 Linux 中,直接 I/O 定义为直接向存储设备发出,绕过页缓存的文件 I/O。iomap_dio_rw 函数为文件实现了 O_DIRECT(直接 I/O)读写。

ssize_t iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
                     const struct iomap_ops *ops,
                     const struct iomap_dio_ops *dops,
                     unsigned int dio_flags, void *private,
                     size_t done_before);

如果文件系统需要在向存储设备发出 I/O 之前或之后执行额外工作,它可以提供 dops 参数。done_before 参数告诉请求已传输了多少。它用于在请求的部分内容已同步完成时异步继续请求。

如果 iocb 的写入在调用之前已经启动,则应设置 done_before 参数。I/O 的方向由传入的 iocb 确定。

dio_flags 参数可以设置为以下值的任意组合

  • IOMAP_DIO_FORCE_WAIT:即使 kiocb 不是同步的,也要等待 I/O 完成。

  • IOMAP_DIO_OVERWRITE_ONLY:对此范围执行纯覆盖写入,否则返回 -EAGAIN 错误。文件系统可以使用此功能为复杂的不对齐 I/O 写入路径提供优化的快速路径。如果可以执行纯覆盖写入,则无需与其他 I/O 对同一文件系统块进行序列化,因为不存在陈旧数据暴露或数据丢失的风险。如果无法执行纯覆盖写入,则文件系统可以执行所需的序列化步骤,以提供对不对齐 I/O 范围的独占访问,以便安全地执行分配和子块清零。文件系统可以使用此标志来尝试减少锁竞争,但需要进行大量详细检查才能正确执行。

  • IOMAP_DIO_PARTIAL:如果发生页错误,则返回已经取得的任何进展。调用者可以处理页错误并重试操作。如果调用者决定重试操作,它应将所有先前调用的累积返回值作为 done_before 参数传递给下一次调用。

这些 struct kiocb 标志对于 iomap 的直接 I/O 至关重要

  • IOCB_NOWAIT:开启 IOMAP_NOWAIT

  • IOCB_SYNC:确保设备在完成调用之前将数据持久化到磁盘。对于纯覆盖写入,I/O 可以启用 FUA。

  • IOCB_HIPRI:轮询 I/O 完成而不是等待中断。仅对异步 I/O 有意义,并且仅当整个 I/O 可以作为单个 struct bio 发出时。

  • IOCB_DIO_CALLER_COMP:尝试从调用者的进程上下文运行 I/O 完成。有关更多详细信息,请参见 linux/fs.h

文件系统应从 ->read_iter->write_iter 调用 iomap_dio_rw,并在文件的 ->open 函数中设置 FMODE_CAN_ODIRECT。它们不应设置已弃用的 ->direct_IO

如果文件系统希望在直接 I/O 完成之前执行自己的工作,它应该调用 __iomap_dio_rw。如果其返回值不是错误指针或 NULL 指针,文件系统应在完成其内部工作后将返回值传递给 iomap_dio_complete

2.2.1. 返回值

iomap_dio_rw 可以返回以下之一

  • 非负的已传输字节数。

  • -ENOTBLK:回退到缓存 I/O。如果 iomap 本身在向存储设备发出 I/O 之前无法使页缓存失效,则它将返回此值。->iomap_begin->iomap_end 函数也可能返回此值。

  • -EIOCBQUEUED:异步直接 I/O 请求已排队,将单独完成。

  • 任何其他负错误代码。

2.2.2. 直接读取

直接 I/O 读取启动从存储设备到调用者缓冲区的读 I/O。在启动读 I/O 之前,页缓存的脏部分会刷新到存储设备。->iomap_beginflags 值将是 IOMAP_DIRECT,并可与以下增强功能任意组合

  • IOMAP_NOWAIT,如前所述。

调用者通常在调用此函数之前以共享模式持有 i_rwsem

2.2.3. 直接写入

直接 I/O 写入启动从调用者缓冲区到存储设备的写 I/O。在启动写 I/O 之前,页缓存的脏部分会刷新到存储设备。页缓存会在写 I/O 之前和之后都失效。->iomap_beginflags 值将是 IOMAP_DIRECT | IOMAP_WRITE,并可与以下增强功能任意组合

  • IOMAP_NOWAIT,如前所述。

  • IOMAP_OVERWRITE_ONLY:不允许分配块和清零部分块。整个文件范围必须映射到单个已写入或未写入的区段。如果映射是未写入的且文件系统无法处理未对齐区域的清零而不暴露陈旧内容,则文件 I/O 范围必须与文件系统块大小对齐。

  • IOMAP_ATOMIC:此写入以撕裂写入保护发出。撕裂写入保护可能基于硬件卸载或文件系统提供的软件机制。

    对于基于硬件卸载的支持,写入只能创建一个 bio,并且写入不能拆分为多个 I/O 请求,即必须设置标志 REQ_ATOMIC。要写入的文件范围必须对齐,以满足文件系统和底层块设备的原子提交能力的要求。如果需要文件系统元数据更新(例如未写入区段转换或写时复制),则整个文件范围的所有更新也必须原子提交。非撕裂写入可能比单个文件块更长。在所有情况下,映射起始磁盘块必须具有与写入偏移量相同的对齐方式。文件系统必须设置 IOMAP_F_ATOMIC_BIO 以通知 iomap 核心基于硬件卸载的非撕裂写入。

    对于基于文件系统提供的软件机制的非撕裂写入,适用于硬件卸载的非撕裂写入的所有磁盘块对齐和单个 bio 限制不适用。该机制通常用作无法发出基于硬件卸载的非撕裂写入时的回退,例如写入范围覆盖多个区段,这意味着无法发出单个 bio。整个文件范围的所有文件系统元数据更新也必须原子提交。

调用者通常在调用此函数之前以共享或排他模式持有 i_rwsem

2.2.4. struct iomap_dio_ops:

struct iomap_dio_ops {
    void (*submit_io)(const struct iomap_iter *iter, struct bio *bio,
                      loff_t file_offset);
    int (*end_io)(struct kiocb *iocb, ssize_t size, int error,
                  unsigned flags);
    struct bio_set *bio_set;
};

此结构的字段如下

  • submit_io:iomap 在构建了请求 I/O 的 struct bio 对象并希望将其提交给块设备时调用此函数。如果未提供函数,则将直接调用 submit_bio。希望在之前执行额外工作(例如 btrfs 的数据复制)的文件系统应实现此函数。

  • end_io:在 struct bio 完成后调用此函数。此函数应执行未写入区段映射的写入后转换、处理写入失败等。flags 参数可以设置为以下组合

    • IOMAP_DIO_UNWRITTEN:映射是未写入的,因此 ioend 应将区段标记为已写入。

    • IOMAP_DIO_COW:写入映射中的空间需要写时复制操作,因此 ioend 应切换映射。

  • bio_set:这允许文件系统提供自定义的 bio_set 用于分配直接 I/O bio。这使文件系统能够存储额外的每 bio 信息以供私用。如果此字段为 NULL,则将使用通用的 struct bio 对象。

希望在 I/O 完成后执行额外工作的文件系统应通过 ->submit_io 设置自定义的 ->bi_end_io 函数。之后,自定义的 endio 函数必须调用 iomap_dio_bio_end_io 来完成直接 I/O。

2.3. DAX I/O

某些存储设备可以直接映射为内存。这些设备支持一种称为“fsdax”的新访问模式,允许通过 CPU 和内存控制器进行加载和存储。

2.3.1. fsdax 读取

fsdax 读取执行从存储设备到调用者缓冲区的内存复制。->iomap_beginflags 值将是 IOMAP_DAX,并可与以下增强功能任意组合

  • IOMAP_NOWAIT,如前所述。

调用者通常在调用此函数之前以共享模式持有 i_rwsem

2.3.2. fsdax 写入

fsdax 写入启动从调用者缓冲区到存储设备的内存复制。->iomap_beginflags 值将是 IOMAP_DAX | IOMAP_WRITE,并可与以下增强功能任意组合

  • IOMAP_NOWAIT,如前所述。

  • IOMAP_OVERWRITE_ONLY:调用者要求从此映射执行纯覆盖写入。这要求文件系统区段映射已经以 IOMAP_MAPPED 类型存在并跨越写入 I/O 请求的整个范围。如果文件系统无法以允许 iomap 基础设施执行纯覆盖写入的方式映射此请求,则必须以 -EAGAIN 失败映射操作。

调用者通常在调用此函数之前以排他模式持有 i_rwsem

2.3.2.1. fsdax mmap 错误

dax_iomap_fault 函数处理 fsdax 存储的读写错误。对于读错误,IOMAP_DAX | IOMAP_FAULT 将作为 flags 参数传递给 ->iomap_begin。对于写错误,IOMAP_DAX | IOMAP_FAULT | IOMAP_WRITE 将作为 flags 参数传递给 ->iomap_begin

调用者通常持有与调用其 iomap 页缓存对应函数时相同的锁。

2.3.3. fsdax 截断、fallocate 和解除共享

对于 fsdax 文件,提供了以下函数来替换其 iomap 页缓存 I/O 对应函数。->iomap_beginflags 参数与页缓存对应函数相同,并添加了 IOMAP_DAX

  • dax_file_unshare

  • dax_zero_range

  • dax_truncate_page

调用者通常持有与调用其 iomap 页缓存对应函数时相同的锁。

2.3.4. fsdax 重复数据删除

实现 FIDEDUPERANGE ioctl 的文件系统必须使用自己的 iomap 读取操作调用 dax_remap_file_range_prep 函数。

2.4. 文件定位

iomap 实现了 llseek 系统调用的两种迭代 whence 模式。

2.4.1. SEEK_DATA

iomap_seek_data 函数实现了 llseek 的 SEEK_DATA “whence” 值。IOMAP_REPORT 将作为 flags 参数传递给 ->iomap_begin

对于未写入的映射,将搜索页缓存。页缓存中已映射 folio 且这些 folio 内文件系统块最新的区域将被报告为数据区域。

调用者通常在调用此函数之前以共享模式持有 i_rwsem

2.4.2. SEEK_HOLE

iomap_seek_hole 函数实现了 llseek 的 SEEK_HOLE “whence” 值。IOMAP_REPORT 将作为 flags 参数传递给 ->iomap_begin

对于未写入的映射,将搜索页缓存。页缓存中未映射 folio 的区域,或者 folio 中文件系统块非最新的区域将被报告为稀疏空洞区域。

调用者通常在调用此函数之前以共享模式持有 i_rwsem

2.5. 交换文件激活

iomap_swapfile_activate 函数查找文件中的所有基页对齐区域,并将它们设置为交换空间。文件在激活前将进行 fsync()IOMAP_REPORT 将作为 flags 参数传递给 ->iomap_begin。所有映射必须是已映射或未写入的;不能是脏的或共享的,也不能跨越多个块设备。调用者必须以排他模式持有 i_rwsem;这已由 swapon 提供。

2.6. 文件空间映射报告

iomap 实现了其中两个文件空间映射系统调用。

2.6.1. FS_IOC_FIEMAP

iomap_fiemap 函数以 FS_IOC_FIEMAP ioctl 指定的格式将文件区段映射导出到用户空间。IOMAP_REPORT 将作为 flags 参数传递给 ->iomap_begin。调用者通常在调用此函数之前以共享模式持有 i_rwsem

2.6.2. FIBMAP (已弃用)

iomap_bmap 实现了 FIBMAP。调用约定与 FIEMAP 相同。此函数仅为了保持与在转换前实现了 FIBMAP 的文件系统的兼容性而提供。此 ioctl 已弃用;请不要向尚未实现 FIBMAP 的文件系统添加 FIBMAP 实现。调用者在调用此函数之前应该以共享模式持有 i_rwsem,但这尚不明确。