1. 库设计¶
1.1. 简介¶
iomap 是一个用于处理常见文件操作的文件系统库。该库有两层:
一个底层,提供文件偏移范围的迭代器。该层尝试从文件系统获取每个文件范围到存储的映射,但存储信息并非必须。
一个上层,它作用于底层迭代器提供的空间映射。
迭代可能涉及文件逻辑偏移范围到物理区段的映射,但存储层信息并非必须,例如,用于遍历缓存的文件信息。该库导出了各种用于实现文件操作的 API,例如:
页面缓存读取和写入
页面缓存的 Folio 写错误
脏 Folio 的回写
直接 I/O 读取和写入
fsdax I/O 读取、写入、加载和存储
FIEMAP
lseek
SEEK_DATA
和SEEK_HOLE
交换文件激活
该库起源于 XFS 曾经使用的文件 I/O 路径;现在它已被扩展以覆盖其他几种操作。
1.2. 谁应该阅读本文?¶
本文档的目标受众是文件系统、存储和页面缓存程序员以及代码审查人员。
如果您正在从事 PCI、机器架构或设备驱动程序方面的工作,那么您很可能来错了地方。
1.3. 这有什么改进?¶
与经典的 Linux I/O 模型将文件 I/O 分解为小单元(通常是内存页或块)并根据该单元查找空间映射不同,iomap 模型要求文件系统为给定的文件操作创建最大的空间映射,并在此基础上启动操作。这种策略提高了文件系统对正在执行的操作大小的可见性,使其能够在可能的情况下通过更大的空间分配来对抗碎片。更大的空间映射通过在更大的数据量上分摊映射函数调用的成本来提高运行时性能。
在较高层面上,iomap 操作看起来像这样:
对于操作范围内的每个字节...
通过
->iomap_begin
获取空间映射对于每个工作子单元...
重新验证映射,如有必要,返回上面的 (1)。到目前为止,只有页面缓存操作需要这样做。
执行工作
递增操作游标
如有必要,通过
->iomap_end
释放映射
每个 iomap 操作将在下面更详细地介绍。该库以前由LWN 文章和KernelNewbies 页面涵盖。
本文档的目标是简要讨论 iomap 的设计和功能,然后更详细地列出 iomap 提供的接口。如果您更改 iomap,请更新此设计文档。
1.4. 文件范围迭代器¶
1.4.1. 定义¶
buffer head:旧缓冲区缓存的零星残余。
fsblock
:文件的块大小,也称为i_blocksize
。
i_rwsem
:VFSstruct inode
读写信号量。进程以共享模式持有它以读取文件状态和内容。某些文件系统可能允许共享模式进行写入。进程通常以独占模式持有它以更改文件状态和内容。
invalidate_lock
:页面缓存struct address_space
读写信号量,用于防止对于支持在 EOF 下方穿透 Folio 的文件系统插入和移除 Folio。希望插入 Folio 的进程必须以共享模式持有此锁以防止移除,但允许并发插入。希望移除 Folio 的进程必须以独占模式持有此锁以防止插入。不允许并发移除。
dax_read_lock
:dax 获取的 RCU 读取锁,以防止设备预关闭钩子在其他线程释放资源之前返回。文件系统映射锁:此同步原语是文件系统内部的,必须保护文件映射数据在采样映射时的更新。文件系统作者必须确定如何进行此协调;它不需要是实际的锁。
iomap 内部操作锁:这是 iomap 函数在持有映射时获取的同步原语的通用术语。一个具体的例子是在读取或写入页面缓存时获取 Folio 锁。
纯覆盖:写入操作,在提交或完成期间,不需要执行任何元数据或清零操作。这意味着文件系统必须已将磁盘上的空间分配为
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;
};
字段如下:
offset
和length
描述此映射覆盖的文件偏移范围(以字节为单位)。这些字段必须始终由文件系统设置。
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:文件范围映射到存储设备上的特定空间。设备在
bdev
或dax_dev
中返回。设备地址(以字节为单位)通过addr
返回。IOMAP_UNWRITTEN:文件范围映射到存储设备上的特定空间,但该空间尚未初始化。设备在
bdev
或dax_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_PRIVATE:从该值开始,高位可以由文件系统出于自身目的设置。
这些标志可以在文件操作期间由 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
的 pos
和 length
指定的字节范围的一个文件映射。此映射应通过 iomap
指针返回。该映射必须覆盖所提供的文件范围的至少第一个字节,但不需要覆盖整个请求的范围。
每个 iomap 操作都通过 flags
参数描述请求的操作。flags
的确切值将在下面特定于操作的部分中进行说明。这些标志至少在原则上可以普遍应用于 iomap 操作。
当调用者希望向块存储发出文件 I/O 时,设置
IOMAP_DIRECT
。当调用者希望向类内存存储发出文件 I/O 时,设置
IOMAP_DAX
。当调用者希望尽力避免任何会导致提交任务阻塞的操作时,会设置
IOMAP_NOWAIT
。这在目的上类似于网络 API 的O_NONBLOCK
- 它旨在用于异步应用程序,使其可以继续执行其他工作,而不是等待特定的不可用文件系统资源变为可用。实现IOMAP_NOWAIT
语义的文件系统需要使用 trylock 算法。它们需要能够使用单个 iomap 映射来满足整个 I/O 请求范围。它们需要避免同步读取或写入元数据。它们需要避免阻塞内存分配。它们需要避免等待事务预留以允许进行修改。它们可能不应该分配新空间。等等。如果文件系统开发人员对其任何特定的IOMAP_NOWAIT
操作是否最终可能会阻塞有任何疑问,那么他们应该尽早返回-EAGAIN
,而不是启动操作并强制提交任务阻塞。IOMAP_NOWAIT
通常代表IOCB_NOWAIT
或RWF_NOWAIT
设置。
如果需要从设备上的 不同设备或地址范围读取现有文件内容,则文件系统应通过 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_write
和iomap_file_unshare
之前使用i_rwsem
,以防止这两个文件操作互相干扰。页缓存回写可能会锁定 folio,以防止其他线程在回写进行时访问该 folio。
文件系统在
->iomap_begin
和->iomap_end
函数中使用较低级别的原语来协调对文件空间映射信息的访问。在持有此原语时,应填充 iomap 对象的字段。在获取较低级别同步原语时,如果存在较高级别的同步原语,则仍会保持持有状态。例如,XFS 在采样映射时采用ILOCK_EXCL
,而 ext4 采用i_data_sem
。具有不可变映射信息的文件系统可能不需要在此处进行同步。操作原语由 iomap 操作使用,以协调对其自身内部数据结构的访问。在获取此原语时,如果存在较高级别的同步原语,则仍会保持持有状态。在获取此原语时,不会保持较低级别的原语。例如,pagecache 写操作将获取一个文件映射,然后抓取并锁定一个 folio 以复制新内容。它还可能锁定一个内部 folio 状态对象以更新元数据。
确切的锁定要求特定于文件系统;对于某些操作,可以省略其中一些锁。所有进一步提及的锁定都是建议,而不是强制要求。每个文件系统作者都必须自己确定锁定方案。
1.7. 错误和限制¶
不支持 fscrypt。
不支持压缩。
尚不支持 fsverity。
强烈假设 IO 应该像在 XFS 上那样工作。
iomap 实际上是否适用于非普通文件数据?
欢迎提交补丁!