内核的文件系统实现常常会涉及一个名为 "iomap" 的层,但很少有人能准确说出 iomap 究竟是什么。这正是 LWN 旨在填补的空白。简而言之,iomap 处理的是文件系统空间中的数据(由特定的文件和文件内偏移量标识)与存储空间(可能是内存位置,或存储设备上的一组块)之间的映射关系。利用这种映射,iomap 可以处理一大堆常见的、与文件系统相关的任务,从而允许从各个文件系统实现中移除大量样板代码。
一、 前世今生:从缓冲区头到 iomap 的演进
1. 引入 iomap 前的旧时代:缓冲区头(Buffer Heads)
在 Linux 的早期,文件空间与存储空间的映射是由 缓冲区头(Buffer Heads,即 struct buffer_head) 来表示的。然而,它们存在许多致命的问题:
-
设计不适应现代文件系统: 作为磁盘块与等大小内存区域之间的直接映射,缓冲区头并不是为当今的大型文件和基于区段(Extent)的文件系统设计的。
-
内存开销与碎片化: 当涉及的数据由缓冲区头表示时,很难生成高效的 I/O 操作。磁盘上的单个连续区段可能需要数百个缓冲区头,每个缓冲区头管理一个块,这些缓冲区头必须重新组合成少量的 I/O 操作,导致了严重的性能损耗与内存碎片。
2. iomap 的历史与演进
-
诞生: iomap 代码最初由 Christoph Hellwig 在 2016 年底发布的 4.8 内核版本 中作为独立层引入,但其大部分功能基于 Dave Chinner 在 XFS 文件系统中的早期实现。
-
成长: 随着文件系统的不断转换和新功能的加入,它多年来一直在发展。当前的实现由
fs/iomap目录中的大约一打文件组成。它被实现为两个广泛的层:文件与其后备存储之间的底层映射,以及高层代码用来实现文件系统所需的大部分功能的上层逻辑。 -
现状: 绝大多数积极维护的文件系统都已至少部分过渡到 iomap。
二、 存储映射的核心:struct iomap 与回调机制
每一个文件系统实现的记录文件,本质上都是存储在某个持久介质上的一系列字节的表示。文件系统的大部分工作归根结底是通过在内存和持久介质之间移动数据,来执行以文件和偏移量表示的操作。
有了 iomap,原本可能涉及大量缓冲区头的映射,现在可以用一个单一的 iomap 结构来表示:
struct iomap {
u64 addr; /* 磁盘映射偏移量,字节 */
loff_t offset; /* 文件映射偏移量,字节 */
u64 length; /* 映射长度,字节 */
u16 type; /* 映射类型 */
u16 flags; /* 映射标志 */
struct block_device *bdev; /* 用于 I/O 的块设备 */
struct dax_device *dax_dev; /* 用于 DAX 操作的 dax_dev */
void *inline_data;
void *private; /* 文件系统私有数据 */
u64 validity_cookie; /* 与 .iomap_valid() 一起使用 */
};
1. 映射类型(Type)
type 字段描述了实际表示的映射类型:
-
IOMAP_MAPPED:表示从文件到持久存储设备上空间的常规映射。 -
IOMAP_INLINE:表示文件与inline_data指向的内存之间的映射。这种映射可用于微型文件,其文件数据可以直接存储在文件索引节点(inode)本身中。 -
IOMAP_HOLE:意味着底层存储尚未分配------根本没有映射。它仅对读操作有效,其结果是从该范围读取将返回零。 -
IOMAP_DELALLOC:同样表示映射不存在,但它将在未来某个时间点被创建(延迟分配)。 -
IOMAP_UNWRITTEN:表示映射存在,但后备存储尚未被写入,可能包含随机(或敏感)数据。从该范围读取将返回零,而不是访问后备存储。
2. 文件系统回调:iomap_ops
这些 iomap 结构必须由文件系统在响应 iomap 层的请求时进行填充。这些请求通过文件系统提供的回调函数接收:
struct iomap_ops {
/*
* 返回 pos 处的现有映射,或者从 pos 开始为最多 length 长度预留空间,
* 条件是我们能将其作为一个单一映射来完成。
* 实际长度在 iomap->length 中返回。
*/
int (*iomap_begin)(struct inode *inode, loff_t pos, loff_t length,
unsigned flags, struct iomap *iomap,
struct iomap *srcmap);
/*
* 提交和/或取消预留先前使用 iomap_begin 分配的空间。
* written 表示需要提交的成功写操作的长度,其余部分需要取消预留。
* 如果没有写入数据,written 可能为零。
*/
int (*iomap_end)(struct inode *inode, loff_t pos, loff_t length,
ssize_t written, unsigned flags, struct iomap *iomap);
};
iomap_begin() 中的常用 flags 包括 IOMAP_WRITE(写入)、IOMAP_ZERO(清零)和 IOMAP_DIRECT(直接 I/O)。
三、 文件系统 I/O 的实现与简化
映射只有在内核能用它们执行 I/O 时才有价值。以缓冲读(Buffered Reads)为例,当应用程序调用 read() 时,需要完成页缓存分配、磁盘定位、数据读取和用户空间复制。
对于缓冲 I/O,文件系统必须提供 struct address_space_operations,其中的 read_folio() 回调用于读操作:
int (*read_folio)(struct file *file, struct folio *folio);
通过配合 iomap_read_ops 和 iomap_read_folio_ctx 结构,文件系统可以极大地简化这一过程。例如:
struct iomap_read_ops {
int (*read_folio_range)(const struct iomap_iter *iter,
struct iomap_read_folio_ctx *ctx, size_t len);
void (*submit_read)(const struct iomap_iter *iter,
struct iomap_read_folio_ctx *ctx);
/* ... */
};
struct iomap_read_folio_ctx {
const struct iomap_read_ops *ops;
struct folio *cur_folio;
struct readahead_control *rac;
void *read_ctx;
loff_t read_ctx_file_offset;
};
许多文件系统甚至无需自定义 iomap_read_ops,直接使用 iomap 层自带的实现(如 iomap_bio_read_ops)。最终,read_folio() 的实现只需归结为对 iomap_read_folio() 的调用:
void iomap_read_folio(const struct iomap_ops *ops,
struct iomap_read_folio_ctx *ctx,
void *private);
上述调用序列的时序关系如下所示:

除了缓冲读,iomap 还广泛支持缓冲写(通过 writepages())、直接 I/O、DAX I/O、文件内寻址、缺页异常处理、文件截断和交换文件激活等。
四、 iomap 的未来展望
iomap 子系统远未完成,几乎在每个开发周期都有新的工作被合并进来:
-
新特性支持: 最近的更改包括对具有
fs-verity完整性保护的文件的支持、生成和验证T10保护信息的能力、更好的预读支持以及安全修复。 -
架构改进: 目前正在进行中的工作包括将
iomap_begin()和iomap_end()回调替换为基于迭代器的 API,并进一步改善直接 I/O 性能。 -
文件系统转换: 转换工作仍在继续,目前正在考虑将
exfat和minix文件系统转换为使用 iomap。
总而言之,实现一个文件系统涉及到一定程度无法完全抽象掉的复杂性。不过,iomap 层确实处理了每个文件系统都必须处理的许多底层细节。使用 iomap 还使得像大 folio(large-folio)支持这样的功能只需付出很少的额外努力即可工作,使其成为了 Linux 内核中一个极其关键且繁忙的现代核心子系统。
