2. 支持的文件操作

以下是关于 iomap 实现的高级文件操作的讨论。

2.1. 缓冲 I/O

缓冲 I/O 是 Linux 中的默认文件 I/O 路径。文件内容缓存在内存(“页面缓存”)中以满足读取和写入操作。脏缓存将在某个时间点写回磁盘,这可以通过 fsync 及其变体强制执行。

iomap 实现了文件系统在传统的 I/O 模型下必须自己实现的几乎所有 folio 和页面缓存管理。这意味着文件系统不需要知道分配、映射、管理最新和脏状态或页面缓存 folio 回写的细节。在传统的 I/O 模型下,这是非常低效地使用缓冲区头的链表而不是 iomap 使用的每个 folio 位图进行管理的。除非文件系统显式选择加入缓冲区头,否则它们将不会被使用,这使得缓冲 I/O 更加高效,并且页面缓存维护者更加满意。

2.1.1. struct address_space_operations

以下 iomap 函数可以直接从地址空间操作结构中引用

  • iomap_dirty_folio

  • iomap_release_folio

  • iomap_invalidate_folio

  • iomap_is_partially_uptodate

以下地址空间操作可以很容易地包装

  • 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 中。如果发现 cookie 中的值与文件系统在映射传递回 ->iomap_valid 时持有的值不同,则应认为 iomap 过期且验证失败。

这些 struct kiocb 标志对于使用 iomap 的缓冲 I/O 非常重要

  • IOCB_NOWAIT:启用 IOMAP_NOWAIT

2.1.3. 内部每个 Folio 状态

如果 fsblock 大小与页面缓存 folio 的大小匹配,则假定所有磁盘 I/O 操作都将在整个 folio 上进行。在这种情况下,只需要 folio 的最新状态(内存内容至少与磁盘上的内容一样新)和脏状态(内存内容比磁盘上的内容更新)。

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

iomap 内部跟踪每个 fsblock 的两个状态位

  • uptodate:iomap 将尝试使 folio 完全最新。如果存在读取(预读)错误,则这些 fsblock 将不会被标记为最新。当 folio 内的所有 fsblock 都是最新时,folio 本身将被标记为最新。

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

iomap 还跟踪正在进行的读取和写入磁盘 I/O 的数量。此结构比 struct buffer_head 轻得多,因为每个 folio 只有一个,并且每个 fsblock 的开销是 2 位 vs. 104 字节。

希望在页面缓存中启用大型 folio 的文件系统应在初始化 incore 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_end 函数调用 iomap_write_delalloc_release 来查找缓存新 (IOMAP_F_NEW) 延迟分配映射的所有干净区域。它需要 invalidate_lock

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

2.1.5.3. 文件操作的清零

对于未与 fsblock 大小对齐的非截断文件操作,文件系统可以调用 iomap_zero_range 来执行页缓存的清零。IOMAP_ZERO 将作为 flags 参数传递给 ->iomap_begin。调用方通常在调用此函数之前以独占模式持有 i_rwseminvalidate_lock

2.1.5.4. 取消共享重新链接的文件数据

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

2.1.6. 截断

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

2.1.7. 页缓存回写

文件系统可以调用 iomap_writepages 来响应将脏页缓存页写入磁盘的请求。应保持 mappingwbc 参数不变。 wpc 指针应由文件系统分配,并且必须初始化为零。

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

即使回写失败,通过下面描述的 ->map_blocks 机制运行的所有页的脏位都将被清除。这是为了防止存储设备发生故障时出现脏页凝块;为用户空间记录一个 -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 (*prepare_ioend)(struct iomap_ioend *ioend, int status);
    void (*discard_folio)(struct folio *folio, loff_t pos);
};

字段如下

  • map_blocks:将 wpc->iomap 设置为 offsetlen 给出的文件范围(以字节为单位)的空间映射。 iomap 为每个脏页中的每个脏 fs 块调用此函数,但它将重用映射,用于页内连续脏 fs 块的运行。此处不要返回 IOMAP_INLINE 映射; ->iomap_end 函数必须处理持久化写入的数据。此处不要返回 IOMAP_DELALLOC 映射; iomap 当前需要映射到已分配的空间。如果映射没有更改,文件系统可以跳过可能代价高昂的映射查找。此重新验证必须由文件系统开放编码;目前尚不清楚是否可以将 iomap::validity_cookie 重用于此目的。此函数必须由文件系统提供。

  • prepare_ioend:使文件系统能够在提交回写 I/O 之前转换回写 ioend 或执行任何其他准备工作。这可能包括预写空间会计更新,或者为内部目的安装自定义 ->bi_end_io 函数,例如将 ioend 完成推迟到工作队列,以便从进程上下文中运行元数据更新事务。此函数是可选的。

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

2.1.7.2. 页缓存回写完成

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

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

一些文件系统可能希望通过批量处理来摊销回写后更新的运行元数据事务的成本。它们还可能需要事务从进程上下文中运行,这意味着将批次转移到工作队列。 iomap ioend 包含一个 list_head 以启用批处理。

给定一批 ioend,iomap 有一些辅助工具来帮助摊销

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

  • iomap_ioend_try_merge:给定一个不在任何列表中的 ioend 和一个单独的已排序 ioend 列表,将列表中尽可能多的 ioend 从头部合并到给定的 ioend 中。只有在文件范围和存储地址是连续的;未写入和共享状态相同;并且写入 I/O 结果相同的情况下,才能合并 ioend。合并的 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:确保设备在完成调用之前已将数据持久化到磁盘。对于纯覆盖,可以使用 FUA 启用 I/O。

  • 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。要写入的文件范围必须对齐以满足文件系统和底层块设备原子提交功能的要求。如果需要文件系统元数据更新(例如,未写入范围转换或写入时复制),则必须以原子方式提交整个文件范围的所有更新。每个未撕裂写入只允许一个空间映射。未撕裂写入必须与单个文件块对齐,且不得长于单个文件块。

调用者通常在调用此函数之前以共享或独占模式持有 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 读取执行从存储设备到调用者缓冲区的 memcpy。 ->iomap_beginflags 值将为 IOMAP_DAX,并具有以下任何组合的增强功能:

  • IOMAP_NOWAIT,如前所述。

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

2.3.2. fsdax 写入

fsdax 写入启动从调用者缓冲区到存储设备的 memcpy。 ->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 截断、预分配和取消共享

对于 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 中的 fsblock 为最新的区域将被报告为数据区域。

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

2.4.2. SEEK_HOLE

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

对于未写入的映射,将搜索页缓存。页缓存中没有映射 folio 的区域,或者 folio 中的 fsblock 不是最新的区域将被报告为稀疏空洞区域。

调用者通常在调用此函数之前以共享模式持有 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 实现。调用者可能应该在调用此函数之前以共享模式持有 i_rwsem,但这并不明确。