block: introduce dma map backed bio type
💡 一句话总结
在把 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) |
📊 速览卡片
🎯 解决什么问题
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,它通过 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 映射"。
iommu_map/unmap 是纯开销。现状下要么经主机 RAM 中转(多一次全量拷贝),要么走普通 registered buffer 仍要逐页 pin+映射。本补丁所在系列让 NVMe 直接吃下"别人映射好的 DMA 地址表",把中转拷贝和重复映射都省掉。
🧩 核心机制
简化理解(类比):普通 bio 相当于"快递单上写着每一页的地址,快递员按页去取";本补丁则允许"寄件方直接把已经打包好、贴好快递单(DMA 地址)的一整包(dma-buf map)递过来,快递员拿走就送"。
| 模块 / 文件 | 改了什么 | 为什么这么改 |
|---|---|---|
include/linux/blk_types.h | struct 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.h | bio_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。
📈 性能影响
cover.1785596451.git.asml.silence@gmail.com,作者标注为合作者 Anuj 早前测试,未独立验证):
| IOMMU 模式 | before(每次 I/O 现映射) | after(预建 dma-buf 映射) | 提升 |
|---|---|---|---|
| STRICT | 570 KIOPS | 5.01 MIOPS | ≈ 8.8× |
| LAZY | 1.93 MIOPS | 5.01 MIOPS | ≈ 2.6× |
| PASSTHROUGH(无 IOMMU 映射) | 5.01 MIOPS | 5.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 | 2026-01 · 00/11 | 不传裸 DMA 地址,包成驱动专用对象;拆成 token 与 map 两个对象;实现 move_notify |
| v3 | 2026-04/05 · 00/10 | 重做 io_uring 注册;把 token/map 基础设施移出 blk-mq;简化回调(去掉一层转发表);不按请求方向跳过 dma sync;修若干 hang;s/dma/dmabuf/ 改名 |
| v4 | 2026-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/16 | raw 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 回复;但系列跨版本有真实、且与本补丁机制相关的维护者讨论,如实呈现如下。
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)。
UBLK_IO_F_SHMEM_ZC,可对用户对齐缓冲区维护长期 vfio dma 映射(Message-ID: afxgc4hizusnAA26@fedora、afi7c-VUJWOLlC1m@fedora)。作者反问是否想让普通 registered buffer 也走"驱动预注册"路线,双方就统一接口的必要性展开探讨。
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)。截至归档,作者尚未公开回应。
⚠️ 风险与局限
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,对用户侧对齐有隐性约束。
🔗 交叉引用
✅ 关键洞察
- 解决:块层 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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。