block: introduce dma map backed bio type

块层 · dma-buf 直通块设备 · io_uring 大系列 v5 07/16

💡 一句话总结

在把 GPU/设备内存(dma-buf)直接读写到 NVMe 裸块设备(raw bdev)的场景下,块层原来必须为每次 I/O 用页描述符数组(bio_vec)描述数据、并现场做 DMA 映射(IOMMU 下开销巨大);本补丁把 bio 里存页数组的指针槽改造成联合体,让它改存一个"已经映射好的 dma-buf 映射"(dma_buf_io_map),于是块层和驱动可以跳过逐页描述与逐次 DMA 映射,直接下发预建好的 SGL/PRP。系列级基准(写入 cover letter,合作者 Anuj 早前用 udmabuf 测得,未独立验证):STRICT 模式从 570 KIOPS 提到 5.01 MIOPS(约 8.8×),LAZY 模式从 1.93 MIOPS 提到 5.01 MIOPS(约 2.6×),追平 PASSTHROUGH 上限。

📋 补丁基本信息

项目内容
补丁类型新特性(feature,为 dma-buf 直通块设备铺路的基础设施)
状态In Review(v5 大系列 07/16,未合入主线)
当前版本v5 · 补丁链接(07/16)
版本演进 RFC v2(2026-01,00/11)→ v3(2026-04/05,00/10)→ v4(2026-07-29,00/14)→ v5(2026-08-01,00/16)
作者机构Pavel Begunkov(io_uring 维护者)
提交日期2026-08-01
改动范围7 文件(block/bio.c、block/blk-merge.c、block/fops.c、include/linux/bio.h、blk-mq.h、blk_types.h、bvec.h),+78/-9 行
核心函数struct bio 联合体 / bio_iov_iter_set() / bio_split_io_at_dmabuf() / __bio_clone()
灵感来源Suggested-by: Keith Busch(2022 年的早期尝试 20220805162444.3985535-1-kbusch@fb.com)

📊 速览卡片

核心机制
bio 联合体
优化目标
免 DMA 重映射
适用场景
dma-buf→NVMe
实测提升
见 cover 基准

🎯 解决什么问题

背景 / 原始动机
本补丁属于 Pavel Begunkov 的 「Add dmabuf read/write via io_uring」16-patch 大系列(v5)的一部分(cover letter:cover.1785596451.git.asml.silence@gmail.com)。该系列的目标:允许把一个 dma-buf(例如 GPU 显存、其他设备分配的共享内存)注册到 io_uring 实例,然后作为普通 registered buffer,用 IORING_OP_{READ,WRITE}_FIXED 直接对指定文件(首批为 NVMe 裸块设备)做读写——即"GPU 数据不经主机内存中转、直接与 NVMe 交换"。作者明确说基础设施不绑死 io_uring,未来可有更多使用者。类似思路最早由 Keith Busch 在 2022 年提出(作者借用了不少改动),后被 Intel 的 Tushar Gohad / Vishal Verma 重新拾起,再到现在由 Pavel 主导推进。
系统层面:块层 bio 的"数据描述"模型
块层 I/O 的最小单位是 bio,它通过 bi_io_vec 指向一段 struct bio_vec 数组——每个元素描述"一个页 + 偏移 + 长度",即把数据切成一页页交给设备和驱动去 逐段 DMA 映射(dma_map_sg)后再提交。这套模型对"普通用户页/页缓存"是合适的;但对"已经是某个设备预先映射好的 dma-buf"来说就是浪费:数据早就映射好了,根本不需要逐页 bio_vec,也不需要每次 I/O 重新映射。 本补丁正是打破这套假设:当 bio 的数据是一个预映射 dma-buf 时,把 bi_io_vec 这个指针槽复用作 bi_dmabuf_map,让 bio 直接携带"驱动专用的 dma 映射"。
场景层面:谁会遇到、为什么痛
典型场景是 GPU 训练/推理的 checkpoint 落盘、GPU↔NVMe 直接对拷、以及任何"另一台设备已把内存映射好、只想让块设备消费"的零拷贝数据通路。这类负载的共同特征:数据体量大(几百 MB 到数 GB)、内存是设备侧分配(不是普通用户页)、IOMMU 开启时每次 I/O 的 iommu_map/unmap 是纯开销。现状下要么经主机 RAM 中转(多一次全量拷贝),要么走普通 registered buffer 仍要逐页 pin+映射。本补丁所在系列让 NVMe 直接吃下"别人映射好的 DMA 地址表",把中转拷贝和重复映射都省掉。

🧩 核心机制

简化理解(类比):普通 bio 相当于"快递单上写着每一页的地址,快递员按页去取";本补丁则允许"寄件方直接把已经打包好、贴好快递单(DMA 地址)的一整包(dma-buf map)递过来,快递员拿走就送"。

系统层面解读(哪个模块改了啥、为什么改)
模块 / 文件改了什么为什么这么改
include/linux/blk_types.hstruct bio 的 bi_io_vec 指针改成与 bi_dmabuf_map 的联合体;新增 REQ_DMABUF 请求标志 + op_is_dmabuf()一个 bio 要么是普通 bvec 型、要么是 dma-buf 型,两者互斥,可共用同一指针槽(省内存);用标志区分,让各路径按类型分派
block/bio.c(bio_iov_iter_set())允许用 dmabuf 迭代器直接填充 bio;命中时置 REQ_NOMERGE | REQ_DMABUF;加 static_assert 保证联合体成员偏移一致这是 block/fops 异步直通 IO 的快路径:从迭代器直接取数据指针、省掉 bio_iov_iter_get_pages() 的取页逻辑;dma-buf 是单一连续预映射区间,禁止与相邻 bio 合并
block/blk-merge.c(bio_split_io_at_dmabuf())bio 超限拆分时,dma-buf 型 bio 走专用拆分逻辑dma-buf 是"一整块连续映射",无法像普通 bio 那样按页细分段;只能按 max_segments << seg_shift(每个 DMA 段覆盖 2^seg_shift 字节)切成整段,并校验对齐
block/bio.c(__bio_clone())克隆 bio 时按类型复制 bi_dmabuf_map 或 bi_io_vec联合体不能盲目整拷:克隆前必须先知道当前是哪种指针
include/linux/bio.hbio_no_advance_iter() 与 bio_iov_vecs_to_alloc() 对 dmabuf 型生效dma-buf 型 bio 不按 bvec 方式推进迭代器;且不需要额外分配 bio_vec 数组(复用既有 dma 映射)
include/linux/blk-mq.h新增 blk_mq_rq_is_dmabuf() 辅助函数给驱动(NVMe)提供一个"这个请求是不是 dma-buf 型"的检查入口,受 CONFIG_DMA_SHARED_BUFFER 保护

一句话机制:把"bio 必须用页数组描述数据"改成"bio 可以携带一个驱动专用的预建 DMA 映射",配合新标志位让克隆/拆分/推进/分配各路径都按类型分派。

🔬 关键代码

① bio 指针槽改造成联合体 + 新标志位(数据表示层的核心)

@@ include/linux/blk_types.h
 	struct bio {
 		...
 		/* The actual vec list, preserved by bio_reset() */
-		struct bio_vec		*bi_io_vec;
+		union {
+			struct bio_vec		*bi_io_vec;
+			/* Driver specific dma map, valid IFF REQ_DMABUF is set */
+			struct dma_buf_io_map	*bi_dmabuf_map;
+		};
 		struct bvec_iter	bi_iter;
 		...
 	};

 	enum req_flag_bits {
 		...
 		__REQ_ATOMIC,		/* for atomic write operations */
+		__REQ_DMABUF,		/* Using premmaped dma buffers */
 		...
 	};
+	#define REQ_DMABUF	(__force blk_opf_t)(1ULL << __REQ_DMABUF)

+	static inline bool op_is_dmabuf(blk_opf_t op)
+	{
+		return op & REQ_DMABUF;
+	}

▲ 关键一行:联合体注释写明 valid IFF REQ_DMABUF is set——语义约束靠 REQ_DMABUF 标志维持。这是把"两种互斥数据表示塞进同一个指针槽"的典型 union 手法:dma-buf 型 bio 不再需要逐页 bio_vec,因为它指向的是驱动专用、已建好的 dma_buf_io_map(含 seg_shift 描述 DMA 段大小)。

② 快路径 bio_iov_iter_set:从迭代器直接取 dma 映射

@@ block/bio.c
 	bool bio_iov_iter_set(struct bio *bio, const struct iov_iter *iter)
 	{
-		if (!iov_iter_is_bvec(iter))
+		if (!iov_iter_is_bvec(iter) && !iov_iter_is_dmabuf_map(iter))
 			return false;

 		WARN_ON_ONCE(bio->bi_max_vecs);

+		static_assert(offsetof(struct bio, bi_io_vec) ==
+			      offsetof(struct bio, bi_dmabuf_map));
+		static_assert(offsetof(struct iov_iter, bvec) ==
+			      offsetof(struct iov_iter, dmabuf_map));
+
 		bio->bi_io_vec = (struct bio_vec *)iter->bvec;
 		bio->bi_iter.bi_idx = 0;
 		bio->bi_iter.bi_offset = iter->iov_offset;
 		bio->bi_iter.bi_size = iov_iter_count(iter);
 		bio_set_flag(bio, BIO_CLONED);
+		if (iov_iter_is_dmabuf_map(iter))
+			bio->bi_opf |= REQ_NOMERGE | REQ_DMABUF;
 		return true;
 	}

▲ 这是 block/fops.c 异步直通 IO 的"零取页"快路径:普通 bvec 迭代器时它直接把 iter->bvec 指针塞进 bio,跳过 bio_iov_iter_get_pages()。现在 dma-buf 迭代器也走这里(iov_iter_is_dmabuf_map()),且 static_assert 在编译期证明联合体成员与迭代器联合体成员偏移一致,保证把 dmabuf_map 当 bvec 赋值是安全的。REQ_NOMERGE 表示这块预映射区间不可与相邻 bio 合并。

③ dma-buf 型 bio 的拆分逻辑:一整块连续映射怎么切

@@ block/blk-merge.c
+static inline int bio_split_io_at_dmabuf(struct bio *bio,
+		const struct queue_limits *lim, unsigned *segs,
+		unsigned max_bytes, unsigned len_align_mask,
+		unsigned start_align_mask)
+{
+	unsigned bytes = min(bio->bi_iter.bi_size, max_bytes);
+	unsigned seg_shift = bio->bi_dmabuf_map->seg_shift;
+	unsigned offset = bio->bi_iter.bi_offset & ((1U << seg_shift) - 1);
+
+	if ((bio->bi_iter.bi_offset & start_align_mask) ||
+	    (bio->bi_iter.bi_size & len_align_mask))
+		return -EINVAL;
+
+	/* single contiguous range into the dma-buf */
+	*segs = 1;
+
+	bytes = min(bytes, ((unsigned)lim->max_segments << seg_shift) - offset);
+	if (bytes != bio->bi_iter.bi_size)
+		return bytes;
+	return 0;
+}
+
 	int bio_split_io_at(struct bio *bio, const struct queue_limits *lim, ...)
 	{
 		...
+		if (op_is_dmabuf(bio->bi_opf)) {
+			ret = bio_split_io_at_dmabuf(bio, lim, &nsegs, ...);
+			if (ret < 0)
+				return ret;
+			if (!ret)
+				goto out;
+			bytes = ret;
+			goto split;
+		}
 		bio_for_each_bvec(bv, bio, iter) { ... }

▲ 普通 bio 拆分成"按页/段遍历、检查每个 bvec 的对齐与缝隙";dma-buf 型 bio 不能这么做——它没有逐页描述,只有一整块连续映射。所以这里把整块视为 1 个 segment,用 lim->max_segments << seg_shift(每个 DMA 段占 2^seg_shift 字节)反算最多能装多少字节,减掉段内偏移即得拆分点;开头还要校验起止对齐,不合法直接 -EINVAL。

📈 性能影响

本补丁是否自带基准
本补丁未提供独立 benchmark。它是系列中的一块拼图(07/16),性能收益必须放在整个「dmabuf read/write via io_uring」系列里看。cover letter 给出了系列级、且直指本机制价值的基准(数据源:v5 cover letter cover.1785596451.git.asml.silence@gmail.com,作者标注为合作者 Anuj 早前测试,未独立验证):
udmabuf 读取 · 不同 IOMMU 模式(Anuj 早前测试,写入 cover letter)
IOMMU 模式before(每次 I/O 现映射)after(预建 dma-buf 映射)提升
STRICT570 KIOPS5.01 MIOPS≈ 8.8×
LAZY1.93 MIOPS5.01 MIOPS≈ 2.6×
PASSTHROUGH(无 IOMMU 映射)5.01 MIOPS5.01 MIOPS无差异(已达硬件上限)

解读(AI 分析):这张表最说明问题——STRICT/LAZY 是 IOMMU 每次 I/O 都要 map/unmap 的模式,正是本补丁想消灭的开销;"after"在所有模式下都撞到 PASSTHROUGH 的天花板(5.01 MIOPS),说明预建 dma-buf 映射把 IOMMU 开销彻底摊掉了。注意:这是 on-CPU 侧的收益(省掉了 CPU 在提交路径上做的 iommu_map/dma_map_sg 计算);off-CPU 侧(等待设备/锁)无相关数据,属逻辑推断。

🔄 方案演进

RFC v2 → v5 演进脉络(依据 v5 cover letter changelog)
版本日期 / 规模要点
RFC v22026-01 · 00/11不传裸 DMA 地址,包成驱动专用对象;拆成 token 与 map 两个对象;实现 move_notify
v32026-04/05 · 00/10重做 io_uring 注册;把 token/map 基础设施移出 blk-mq;简化回调(去掉一层转发表);不按请求方向跳过 dma sync;修若干 hang;s/dma/dmabuf/ 改名
v42026-07-29 · 00/14加入 Anuj 的 SGL 支持;整体挪到 drivers/dma-buf/ 并改名;修尺寸分配错误;修 io_uring re-import 处理;task work 前 drop map;blk-mq 回调移到 block_device_operations;bio 标志改造成 REQ_OP 体系
v5(本篇)2026-08-01 · 00/16raw bdev 上拒绝 dma-buf + buffered IO;新增 lim->max_segments bio 拆分;新增 bio_iov_iter_set() 辅助;修 io_uring uapi 校验;NVMe 侧:nvme_pci_sgl_set_data 改名并拆成 prep 补丁、段遍历改 do-while、去掉相邻段合并、去掉 first_dma/first_len、>NVME_MAX_SEGS 的 bailout 移到块层、SGL/PRP 决策抽成辅助函数

与本文补丁直接相关的演进:v4 把 bio 标志"改造成 REQ_OP 体系"、v5"新增 lim->max_segments bio 拆分"——正是本补丁里 REQ_DMABUF 标志和 bio_split_io_at_dmabuf() 的来历;bio_iov_iter_set() 是系列第 6 篇单独引入的辅助函数,本篇第 7 篇在其上叠加 dma-buf 分支。系列规模从 11 → 10 → 14 → 16 篇,说明方案在持续吸收 review 反馈(尤其 v4 加 SGL、移驱动回调)。

💬 讨论焦点

本补丁(07/16)自身在 v5 下暂无直接 review 回复;但系列跨版本有真实、且与本补丁机制相关的维护者讨论,如实呈现如下。

1) RFC v2:接口该放 file_operations 还是做 nvme 专用?(架构之争)
Christian König(dma-buf 维护者) 曾问:为何不做成一个子系统接口(如 nvme ioctl),这样整体实现能简化很多?Christoph Hellwig(hch) 反驳:该特性"绝不是 nvme 专用",nvme 只是首个底层驱动,任何高性能块设备都该支持;且"nvme 子系统不是一个 actor,向子系统注册不可行——真正做 DMA 映射的是 nvme 控制器,这个接口对控制器正好"(Message-ID: 20260107061851.GA15324@lst.de)。
Ming Lei(块层维护者) 则坚持担忧:把 dma-buf 预映射回调挂在 struct file_operations 上,很难穿过 device mapper / raid 等叠层块设备;FS 根本不需要关心 dma buffer 附着,附着点应是"高性能主机控制器"(Message-ID: aV8UJvkt7VGzHjxS@fedora)。

解读(AI 分析):Ming Lei 的"叠层设备穿透"担忧是真实痛点,作者在 v4 把回调从 blk-mq 移到 block_device_operations,正是向"更贴近块设备、便于叠层传递"的方向妥协;但 dm/raid 的全支持仍未在本系列覆盖(首批仅 NVMe)。

2) v3:普通 registered buffer 也是长期映射——要不要统一机制?
Ming Lei 提出:普通 io_uring registered buffer 同样是长期存在的映射,驱动在意的正是"长期 dma 映射",为何不给驱动预注册普通 registered buffer 的能力、把机制统一起来?他并透露受此启发,在 ublk 加了 UBLK_IO_F_SHMEM_ZC,可对用户对齐缓冲区维护长期 vfio dma 映射(Message-ID: afxgc4hizusnAA26@fedora、afi7c-VUJWOLlC1m@fedora)。作者反问是否想让普通 registered buffer 也走"驱动预注册"路线,双方就统一接口的必要性展开探讨。
3) v3/v4:流程性反馈 + 社区需求信号
hch 两次指出系列缺少"基于哪个分支"的说明(v4:20260729065421.GA9468@lst.de;v3:20260512070045.GA32030@lst.de),作者答复并给出 git 分支。v3 后期 hch 主动问"你打算重发吗?很多人急切等它落地",Christian König 附议:"我们这边也有大把人眼巴巴等着"(Message-ID: 49b4c4e0-d0a0-4301-afc6-5f6acf5e9424@amd.com)——这是很强的社区需求信号。
4) v5:Anuj 在 02/16(iov_iter)上发现真实 bug
Anuj Gupta 审阅系列第 2 篇(iov_iter dmabuf 迭代器)时指出:iov_iter_alignment() 把 iov_iter_is_dmabuf_map(i) 并进 iter_is_ubuf(i) 分支,但 dmabuf_map 与 ubuf 共用同一个 union 槽,等于把 map 指针当用户地址读——虽然 iov_iter_gap_alignment() 已正确返回 0,这里应当同样处理。他补充核对了该补丁其余路径(advance/revert/restore),均不 dereference union 指针,问题只此一处,且"当前系列暂无路径会让 dmabuf iter 走到这,故暂不影响"(Message-ID: CACzX3AvwBF32_ODomei3XHSZ=KtRaWLN-_Tfkwv7sV3Ssc3wmw@mail.gmail.com)。截至归档,作者尚未公开回应。

⚠️ 风险与局限

潜在回归 / 边界
union 误读风险(本补丁核心风险):bi_io_vec 与 bi_dmabuf_map 共用同一指针槽,任何未随本系列更新的代码若在 dma-buf 型 bio 上读 bi_io_vec,会把映射指针当页数组指针解引用。作者同步更新了克隆/拆分/推进/分配四条路径,但块层外部(驱动/上层)遗漏即崩——依赖 REQ_DMABUF 标志纪律。
REQ_NOMERGE 禁用合并:dma-buf 型 bio 强制禁止合并,在"大量小 I/O 可合并"的负载上可能损失合并吞吐(解读(AI 分析):换取的是预映射的整体性,属有意取舍)。
首批仅 NVMe:叠层块设备(dm/raid)与文件系统路径尚未支持,Ming Lei 的穿透担忧未完全解决。
对齐限制:bio_split_io_at_dmabuf() 对起止地址有严格对齐要求,不满足直接 -EINVAL,对用户侧对齐有隐性约束。
严重度:MAJOR(union 误读为结构性风险,需整系列配套合入)· review 质疑已部分在 v4 演进中回应

🔗 交叉引用

Message-ID: cover.1785596451.git.asml.silence@gmail.com · 系列总览 + 全部版本 changelog + 基准数据出处
Message-ID: e26aefbcb383be7092dc76343ce745013e671334.1785596451.git.asml.silence@gmail.com
Message-ID: cover.1785274111.git.asml.silence@gmail.com · v4:加 SGL 支持、回调移入 block_device_operations、bio 标志改 REQ_OP 体系
Message-ID: 20260107061851.GA15324@lst.de · RFC v2 接口之争中的关键辩护
Message-ID: aV8UJvkt7VGzHjxS@fedora · RFC v2 中对架构的实质质疑
Message-ID: CACzX3AvwBF32_ODomei3XHSZ=KtRaWLN-_Tfkwv7sV3Ssc3wmw@mail.gmail.com · v5 唯一实质性代码 review
Message-ID: 20220805162444.3985535-1-kbusch@fb.com · 作者在 cover letter 中注明"借用了大量改动"

✅ 关键洞察

  • 解决:块层 bio 只能"逐页数组描述数据"的模型,无法表达"别人已映射好的 dma-buf"——本补丁用 union + 标志位让 bio 直接携带预建 DMA 映射,是 io_uring dma-buf 直通块设备系列的块层地基(07/16)。
  • 证据:cover letter 载明的系列级基准(Anuj 早前用 udmabuf 测,未独立验证):STRICT 570 KIOPS → 5.01 MIOPS(≈8.8×)、LAZY 1.93 → 5.01 MIOPS(≈2.6×),追平 PASSTHROUGH 上限——直观证明"预建映射消除 IOMMU 逐 I/O 开销"。
  • 演进:RFC v2 → v5,系列从 11 篇扩到 16 篇;v4 加 SGL、把回调移进 block_device_operations 正面回应了 Ming Lei 的叠层质疑,v5 新增 max_segments 拆分与 bio_iov_iter_set。
  • 风险:union 误读是结构性风险,必须整系列配套合入;首批仅 NVMe,dm/raid 与 FS 路径未覆盖;dma-buf 型 bio 强制 REQ_NOMERGE。
  • 建议:等待 v6 及维护者(尤其 hch/Ming Lei)对 union 表示与叠层穿透的最终定论;Anuj 在 02/16 指出的 alignment 误读待作者修复。
⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。