1. 库设计

1.1. 简介

iomap 是一个用于处理常见文件操作的文件系统库。该库分为两层:

  1. 较低层提供文件偏移量范围的迭代器。此层尝试从文件系统获取每个文件范围到存储的映射,但存储信息并非必需。

  2. 较高层根据较低层迭代器提供的空间映射执行操作。

迭代可能涉及文件逻辑偏移范围到物理区段的映射,但存储层信息并非必需,例如用于遍历缓存的文件信息。该库导出了各种 API,用于实现文件操作,例如:

  • 页缓存读写

  • 页缓存的 Folio 写故障

  • 脏 folio 的回写

  • 直接 I/O 读写

  • fsdax I/O 读、写、加载和存储

  • FIEMAP

  • lseek SEEK_DATASEEK_HOLE

  • 交换文件激活

该库最初来源于 XFS 曾经使用的文件 I/O 路径;现在它已扩展到覆盖其他几个操作。

1.2. 谁应该阅读本文档?

本文档的目标读者是文件系统、存储和页缓存程序员以及代码审查员。

如果您正在从事 PCI、机器架构或设备驱动程序方面的工作,您很可能找错了地方。

1.3. 它有何改进?

与将文件 I/O 分解为小单元(通常是内存页或块)并基于该单元查找空间映射的经典 Linux I/O 模型不同,iomap 模型要求文件系统为给定文件操作创建最大的空间映射,并在此基础上启动操作。这种策略提高了文件系统对正在执行的操作大小的可见性,使其能够在可能的情况下通过更大的空间分配来对抗碎片。更大的空间映射通过将映射函数调用的成本分摊到更多数据上,从而提高运行时性能。

从高层次来看,iomap 操作如下所示

  1. 对于操作范围内的每个字节...

    1. 通过 ->iomap_begin 获取空间映射

    2. 对于每个子工作单元...

      1. 重新验证映射,并在必要时返回到上述 (1)。目前只有页缓存操作需要这样做。

      2. 执行工作

    3. 增加操作游标

    4. 如有必要,通过 ->iomap_end 释放映射

每个 iomap 操作将在下面详细介绍。该库之前已被一篇 LWN 文章KernelNewbies 页面介绍过。

本文档的目标是简要讨论 iomap 的设计和功能,然后详细介绍 iomap 提供的接口。如果您修改了 iomap,请更新此设计文档。

1.4. 文件范围迭代器

1.4.1. 定义

  • 缓冲区头(buffer head):旧缓冲区缓存的残余。

  • fsblock:文件块大小,也称为 i_blocksize

  • i_rwsem:VFS struct inode 读写信号量。进程以共享模式持有此信号量以读取文件状态和内容。某些文件系统可能允许共享模式进行写入。进程通常以独占模式持有此信号量以更改文件状态和内容。

  • invalidate_lock:页缓存 struct address_space 读写信号量,用于保护支持 EOF 以下 folio 穿孔的文件系统,防止 folio 插入和移除。希望插入 folio 的进程必须以共享模式持有此锁以防止移除,尽管允许并发插入。希望移除 folio 的进程必须以独占模式持有此锁以防止插入。不允许并发移除。

  • dax_read_lock:dax 获取的 RCU 读锁,用于防止设备预关机钩子在其他线程释放资源之前返回。

  • 文件系统映射锁(filesystem mapping lock):此同步原语是文件系统内部的,必须在采样映射时保护文件映射数据免受更新。文件系统作者必须确定如何进行这种协调;它不一定是一个实际的锁。

  • iomap 内部操作锁(iomap internal operation lock):这是 iomap 函数在持有映射时获取的同步原语的通用术语。一个具体的例子是在读写页缓存时获取 folio 锁。

  • 纯覆盖写(pure overwrite):一种在提交或完成期间不需要任何元数据或清零操作的写操作。这意味着文件系统必须已经将磁盘空间分配为 IOMAP_MAPPED,并且文件系统不得对 I/O 对齐或大小施加任何限制。I/O 对齐的唯一限制是设备级别(最小 I/O 大小和对齐,通常是扇区大小)。

1.4.2. struct iomap

文件系统通过以下结构将文件的字节范围映射到存储设备的字节范围,并将其告知 iomap 迭代器:

struct iomap {
    u64                 addr;
    loff_t              offset;
    u64                 length;
    u16                 type;
    u16                 flags;
    struct block_device *bdev;
    struct dax_device   *dax_dev;
    void                *inline_data;
    void                *private;
    const struct iomap_folio_ops *folio_ops;
    u64                 validity_cookie;
};

字段如下:

  • offsetlength 描述此映射覆盖的文件偏移量范围(以字节为单位)。这些字段必须始终由文件系统设置。

  • type 描述空间映射的类型:

    • IOMAP_HOLE:未分配存储空间。此类型绝不能响应 IOMAP_WRITE 操作返回,因为写入必须分配和映射空间,并返回映射。addr 字段必须设置为 IOMAP_NULL_ADDR。iomap 不支持向空洞写入(无论是通过页缓存还是直接 I/O)。

    • IOMAP_DELALLOC:承诺稍后分配空间(“延迟分配”)。如果文件系统在此处返回 IOMAP_F_NEW 并且写入失败,->iomap_end 函数必须删除预留。addr 字段必须设置为 IOMAP_NULL_ADDR

    • IOMAP_MAPPED:文件范围映射到存储设备上的特定空间。设备在 bdevdax_dev 中返回。设备地址(以字节为单位)通过 addr 返回。

    • IOMAP_UNWRITTEN:文件范围映射到存储设备上的特定空间,但该空间尚未初始化。设备在 bdevdax_dev 中返回。设备地址(以字节为单位)通过 addr 返回。从此类映射读取将向调用者返回零。对于写入或回写操作,ioend 应将映射更新为 MAPPED。有关详细信息,请参阅关于 ioend 的部分。

    • IOMAP_INLINE:文件范围映射到由 inline_data 指定的内存缓冲区。对于写入操作,->iomap_end 函数大概会处理数据持久化。addr 字段必须设置为 IOMAP_NULL_ADDR

  • flags 描述空间映射的状态。这些标志应由文件系统在 ->iomap_begin 中设置:

    • IOMAP_F_NEW:映射下的空间是新分配的。未写入的区域必须清零。如果写入失败且映射是空间预留,则必须删除该预留。

    • IOMAP_F_DIRTY:inode 将包含访问任何写入数据所需未提交的元数据。fdatasync 需要提交这些更改到持久存储。这需要考虑 I/O 完成时可能发生的元数据更改,例如直接 I/O 导致的文件大小更新。

    • IOMAP_F_SHARED:映射下的空间是共享的。必须进行写时复制以避免损坏其他文件数据。

    • IOMAP_F_BUFFER_HEAD:此映射需要使用缓冲区头进行页缓存操作。请勿增加此用途。

    • IOMAP_F_MERGED:多个连续块映射被合并到此单个映射中。这仅对 FIEMAP 有用。

    • IOMAP_F_XATTR:此映射用于扩展属性数据,而非常规文件数据。这仅对 FIEMAP 有用。

    • IOMAP_F_BOUNDARY:这表示 I/O 及其完成不得与任何其他 I/O 或完成合并。文件系统在向无法处理跨越某些 LBA(例如 ZNS 设备)的 I/O 的设备提交 I/O 时必须使用此标志。此标志仅适用于缓冲 I/O 回写;所有其他函数都会忽略它。

    • IOMAP_F_PRIVATE:此标志保留用于文件系统私有用途。

    • IOMAP_F_ANON_WRITE:表示(写入)I/O 尚未分配目标块,文件系统将在 bio 提交处理程序中完成此操作,并根据需要拆分 I/O。

    • IOMAP_F_ATOMIC_BIO:这表示写入 I/O 必须在 bio 中设置 REQ_ATOMIC 标志进行提交。文件系统需要设置此标志以通知 iomap 写入 I/O 操作需要基于硬件卸载机制的撕裂写保护。它们还必须确保在 I/O 完成后,映射更新必须在单个元数据更新中执行。

    这些标志可以由 iomap 在文件操作期间自行设置。如果文件系统需要观察这些标志,则应提供一个 ->iomap_end 函数:

    • IOMAP_F_SIZE_CHANGED:文件大小因使用此映射而更改。

    • IOMAP_F_STALE:发现映射已过期。iomap 将对此映射调用 ->iomap_end,然后调用 ->iomap_begin 以获取新映射。

    目前,这些标志仅由页缓存操作设置。

  • addr 描述设备地址,以字节为单位。

  • bdev 描述此映射的块设备。仅在映射或未写入操作时需要设置此项。

  • dax_dev 描述此映射的 DAX 设备。仅在映射或未写入操作时需要设置此项,并且仅适用于 fsdax 操作。

  • inline_data 指向一个内存缓冲区,用于涉及 IOMAP_INLINE 映射的 I/O。此值对于所有其他映射类型均被忽略。

  • private 是指向文件系统私有信息的指针。此值将保持不变地传递给 ->iomap_end

  • folio_ops 将在页缓存操作部分中介绍。

  • validity_cookie 是文件系统设置的一个神奇的新鲜度值,应使用它来检测过期的映射。对于页缓存操作,这对于正确操作至关重要,因为可能发生页故障,这意味着在 ->iomap_begin->iomap_end 之间不应持有文件系统锁。具有完全静态映射的文件系统不需要设置此值。只有页缓存操作会重新验证映射;有关详细信息,请参阅关于 iomap_valid 的部分。

1.4.3. struct iomap_ops

每个 iomap 函数都要求文件系统传递一个操作结构以获取映射并(可选地)释放映射:

struct iomap_ops {
    int (*iomap_begin)(struct inode *inode, loff_t pos, loff_t length,
                       unsigned flags, struct iomap *iomap,
                       struct iomap *srcmap);

    int (*iomap_end)(struct inode *inode, loff_t pos, loff_t length,
                     ssize_t written, unsigned flags,
                     struct iomap *iomap);
};

1.4.3.1. ->iomap_begin

iomap 操作调用 ->iomap_begin,以获取文件 inode 指定的 poslength 字节范围的一个文件映射。此映射应通过 iomap 指针返回。该映射必须至少覆盖所提供文件范围的第一个字节,但不需要覆盖整个请求范围。

每个 iomap 操作通过 flags 参数描述请求的操作。 flags 的确切值将在下面的操作特定部分中说明。这些标志原则上可以普遍应用于 iomap 操作:

  • IOMAP_DIRECT 在调用者希望向块存储发出文件 I/O 时设置。

  • IOMAP_DAX 在调用者希望向类内存存储发出文件 I/O 时设置。

  • IOMAP_NOWAIT 在调用者希望尽力避免任何导致提交任务阻塞的操作时设置。其意图类似于网络 API 的 O_NONBLOCK - 它旨在让异步应用程序继续执行其他工作,而不是等待特定的不可用文件系统资源变为可用。实现 IOMAP_NOWAIT 语义的文件系统需要使用 trylock 算法。它们需要能够用单个 iomap 映射满足整个 I/O 请求范围。它们需要避免同步读写元数据。它们需要避免阻塞内存分配。它们需要避免等待事务预留以允许修改发生。它们可能不应该分配新空间。等等。如果文件系统开发者对任何特定的 IOMAP_NOWAIT 操作是否可能最终阻塞有任何疑问,那么他们应该尽早返回 -EAGAIN,而不是启动操作并强制提交任务阻塞。IOMAP_NOWAIT 通常代表 IOCB_NOWAITRWF_NOWAIT 进行设置。

  • IOMAP_DONTCACHE 在调用者希望执行缓冲文件 I/O,并希望内核在 I/O 完成后丢弃页缓存(如果它未被其他线程使用)时设置。

如果有必要从不同设备或设备上的地址范围读取现有文件内容,文件系统应通过 srcmap 返回该信息。只有页缓存和 fsdax 操作支持从一个映射读取并写入另一个映射。

1.4.3.2. ->iomap_end

操作完成后,如果存在,将调用 ->iomap_end 函数,以表示 iomap 已完成对映射的操作。通常,实现会使用此函数来拆除在 ->iomap_begin 中设置的任何上下文。例如,写入可能希望提交对操作字节的预留,并取消预留未操作的任何空间。written 如果没有触及任何字节,可能为零。flags 将包含传递给 ->iomap_begin 的相同值。读取的 iomap 操作不太可能需要提供此函数。

两个函数在出错时应返回负的 errno 代码,成功时返回零。

1.5. 文件操作准备

iomap 仅处理映射和 I/O。文件系统在启动 I/O 操作之前,仍必须调用 VFS 来检查输入参数和文件状态。它不处理获取文件系统冻结保护、更新时间戳、剥夺权限或访问控制。

1.6. 锁定层次结构

iomap 要求文件系统提供自己的锁定模型。就 iomap 而言,同步原语分为三类:

  • 上层原语由文件系统提供,用于协调对不同 iomap 操作的访问。确切的原语特定于文件系统和操作,但通常是 VFS inode、页缓存失效或 folio 锁。例如,文件系统可能在调用 iomap_file_buffered_writeiomap_file_unshare 之前获取 i_rwsem,以防止这两个文件操作相互覆盖。页缓存回写可能会锁定 folio,以防止其他线程在回写进行期间访问 folio。

    • 下层原语由文件系统在 ->iomap_begin->iomap_end 函数中获取,用于协调对文件空间映射信息的访问。iomap 对象的字段应在此原语被持有时填充。上层同步原语(如果有)在获取下层同步原语时仍被持有。例如,XFS 在采样映射时获取 ILOCK_EXCL,ext4 获取 i_data_sem。具有不可变映射信息的文件系统可能不需要在此处进行同步。

    • 操作原语由 iomap 操作获取,用于协调对其内部数据结构的访问。上层同步原语(如果有)在获取此原语时仍被持有。下层原语在获取此原语时不被持有。例如,页缓存写入操作将获取文件映射,然后抓取并锁定 folio 以复制新内容。它也可能锁定内部 folio 状态对象以更新元数据。

确切的锁定要求特定于文件系统;对于某些操作,可以省略其中一些锁。所有关于锁定的进一步提及都是建议,而非强制。每个文件系统作者都必须自行确定锁定机制。

1.7. 缺陷和限制

  • 不支持 fscrypt。

  • 不支持压缩。

  • 尚不支持 fsverity。

  • 强烈假定 I/O 应该像在 XFS 上那样工作。

  • iomap 真的适用于非常规文件数据吗?

欢迎补丁!