资讯动态

NVMe CMB与DMA-BUF:内核设备内存共享接口之争

发布时间:2026/10/1 12:56:18 来源:尧图企业网站定制
1. 从第1个NVMe硬盘的第5个分区说到控制器里的那小块神秘内存最近后台总能刷到这么几个搜索词/dev/nvme0n1p5是第 1 个 NVMe 硬盘的第 5 个分区吗、E5 平台配 NVMe 跑 Win10 启动一般要多久、用 NTLite 往老镜像里塞 USB3.0 和 NVMe 驱动、NVMe 固态怎么格式化。乍看都是普通用户问题但它们其实指向同一个内核话题NVMe 设备在操作系统里到底是怎么被暴露、被使用、被共享的。这个话题往下挖一层就是标题里那个很硬核的争论——NVMe CMB 和 DMA-BUF 之间到底该用哪种内核接口。1.1 /dev/nvme0n1p5 拆开是什么先把用户热词解决掉。/dev/nvme0n1p5拆开是三层nvme0代表第 0 个 NVMe 控制器controllern1代表这个控制器上的第 1 个 Namespace命名空间p5代表这个 Namespace 上的第 5 个分区。所以严格来说它不是第 1 个硬盘的第 5 个分区而是第 1 个控制器的第 1 个 Namespace 的第 5 个分区。消费级盘通常一个控制器一个 Namespace简化成第 1 块盘的第 5 个分区问题不大但理解成三层结构后面聊内核就顺了。块设备层把 Namespace 抽象成一个块设备分区工具再在上面切分区。这一层用户能看见也能操作。但 NVMe 控制器还有一层块设备完全看不见的东西叫作 CMBController Memory Buffer控制器内存缓冲区。CMB 既不是nvme0n1也不是nvme0n1p5它是控制器内部的一小块内存通过 PCIe BAR 映射给主机访问。它离控制器核心最近读写延迟比走系统总线的普通内存路径更可控NVMe 规范甚至允许拿它放提交队列、完成队列、Shadow Doorbell 甚至数据缓冲。问题来了内核里那套专门用来在不同设备和用户态之间共享缓冲区的 DMA-BUF 接口能不能也用来暴露这块 CMB这个问题在 linux-nvme 和 dri-devel 两个圈子都吵过吵的不仅是能不能更是该用哪种内核接口。我先把技术底子铺开再把我自己的取舍讲清楚。1.2 从块设备往下看CMB 一直没被用户态看见你可能会有疑问既然 CMB 这么好为什么我用了这么多年 NVMe 盘从来没见过它因为 Linux 的块设备层只把 Namespace 呈现给文件系统控制器内部的寄存器、BAR、队列内存这些都是驱动私有的。CMB 的映射和使用从struct nvme_dev到你手上的应用中间隔了好几个抽象层。普通用户感知不到不代表它不重要。实际上 Linux nvme 驱动很早就支持 CMB 了只是使用方式非常克制。控制器 reset、队列分配、命令提交这些路径上CMB 一直在参与只是从来不出现在/sys/class/block/里。如果你想深挖一块 NVMe 盘到底有没有 CMB、CMB 多大、能拿来干什么得去读控制器寄存器。这也是后面接口之争的第一个前提CMB 是设备资源不是通用内存暴露它必须非常小心。2. CMB 是什么Linux 又拿它做了什么2.1 规范视角CMBLOC 与 CMBSZ 决定一切从 NVMe 规范看CMB 是控制器地址空间里的一段内存通过 PCIe BAR 暴露。主机在 BAR 里有两个寄存器描述它CMBLOC和CMBSZ。CMBLOC的低 3 位是 BIRBAR Indicator Register告诉系统 CMB 落在哪个 BAR更高位给的是 CMB 在 BAR 内的偏移按 4KB 对齐。CMBSZ的低 32 位是 SZ真实大小按公式(SZ 1) × 4KB计算。CMBSZ 里还有一堆能力位。SDB 位标记这块 CMB 能不能放 Shadow Doorbell BufferSQR/CQR 位标记能不能放 Submission Queue 和 Completion Queue 的队列项DTO 位标记是否定义了数据缓冲区域的偏移。这些能力位很重要因为不是每块盘的 CMB 都支持全部功能。有些盘 CMB 只够放 Shadow Doorbell有些盘 CMB 又能放队列又能放数据保底能力完全不同。Linux 的 nvme 驱动会尝试把 CMB 映射成 CPU 可访问的虚拟地址老版本用devm_ioremap新版本在部分平台上会选devm_ioremap_wc。映射完之后驱动内部的队列分配、门铃处理就可以基于 CMB 做文章。但这里有一个非常现实的问题把队列放到 CMB 并不一定快。2.2 驱动现状队列能用数据缓冲基本没人用历史上 nvme 驱动有一段时间会尝试把 Submission Queue 的队头项放到 CMB也就是use_cmb_sqes这个模块参数。后来因为部分控制器的 CMB 读写性能反而比系统内存还差加上 CMB 和 CPU 的预取、写合并行为配合不好这个功能默认就被关掉了。你现在翻drivers/nvme/host/pci.c能看到 CMB 的映射逻辑也能看到队列分配时对 CMB 的引用但绝大多数情况下它只是声明支持并不会主动把所有队列都挪进去。数据缓冲更是基本没人用。规范里 CMB 的 DTO 能力允许把一段 CMB 划成数据中转区让控制器做 DMA 的跳板。但现实里数据缓冲动辄几 MB 甚至几十 MB大部分盘的 CMB 总共才 4~64MB控制器自己还要留一部分给门铃和队列剩下的空间小得可怜。更关键的是数据放 CMB 并没有带宽优势数据还是要在 PCIe 上跑控制器离得近不等于数据搬得快。2.3 一块只有几兆字节的控制器私房钱我用控制器私房钱这个词是想强调一个容易被忽略的事实CMB 是控制器自己的资源不是系统的。它不像 system RAM 那样可以被页分配器随便切也不像 CMA 那样能按需回收。控制器把这块 BAR 空间暴露给主机主机可以用它但所有权和生命周期都绑定在控制器上——控制器一 resetCMB 里的内容全部作废。这也决定了 CMB 和 DMA-BUF 的结合点如果你想在多个设备之间共享一段 CMB比如让 GPU 直接读 CMB 里的数据或者让另一个加速器写 CMB 里的门铃就必须有一套安全、可控、能管理生命周期的接口。DMA-BUF 恰好是这套接口最成熟的候选但它的契约远比想象中要重。3. DMA-BUF 不是一个普通内核接口它是一整套快递规则3.1 dma-buf 的生命周期和关键回调DMA-BUF 是内核给硬件加速器准备的缓冲共享机制。它解决的痛点很直接同一块内存摄像头要写、GPU 要读、显示控制器要扫描、CPU 也可能要碰。大家拿着同一个 fd各取所需谁也不把别人冲掉。导出一块 dma-buf 不是创个对象就完事。Exporter 要提供struct dma_buf_ops里面最重要的几个回调attach/detach让 importer 设备绑定上来map_dma_buf返回sg_table给 importer 提供可 DMA 的地址mmap让用户态能 CPU 映射begin_cpu_access/end_cpu_access处理缓存同步release在最后一个引用释放时收尸。这条链路的精髓在于用户态拿到的是一个 fd不是裸指针。fd 背后有引用计数有 exporter 的完整回调有 attach 语义。GPU 要访问这块 buffer得先dma_buf_attach把自己挂上去再dma_buf_map_attachment拿到 DMA 地址。这块地址在设备眼里长什么样完全由 exporter 决定。CMB 想进来就要接受这一整套规则。3.2 heap 框架解决的问题用户态最容易触达 dma-buf 的入口是 heap 框架。内核里有system_heap、cma_heap用户态open(/dev/dma_heap/system)然后ioctl(DMA_HEAP_IOCTL_ALLOC)拿一个 dma-buf fd。这个 fd 可以传给 DRM、V4L2、RDMA所有 importer 一视同仁。heap 框架解决的问题是谁来分配、怎么分配。System heap 从页分配器拿内存CMA heap 从连续内存区拿内存。它对上层屏蔽了分配细节只留下一个干净的用户态接口。这也是很多人一看到 CMB 就想到 heap 的原因如果 CMB 也能注册成一个 heap用户态就能像分配系统内存一样分配控制器内存然后顺手把 fd 丢给 GPU。听起来很完美但问题恰恰出在像分配系统内存这六个字上。3.3 CMB 和 dma-buf 契约的三个根本冲突第一个冲突是分配模型。dma-buf 的底层思维是从某个分配器拿出一块内存而 CMB 不是能随便切的页。它总量小控制器自己要用分配粒度、对齐、可映射长度都受 BAR 布局限制。你想做一个 CMB heap必须先解决控制器内部队列和用户态分配之间的资源互斥。第二个冲突是 CPU 访问类型。系统内存默认是 Write-Back 缓存mmap 之后随便读写。CMB 一般要用 Write-Combining 映射CPU 写有合并效应读却非常慢。dma-buf 的begin_cpu_access/end_cpu_access语义是为普通缓存内存设计的放到 WC 映射上会变得很别扭——你让 importer 做一次同步它可能付出的代价远超想象。第三个冲突是 DMA 地址语义。map_dma_buf返回的sg_table是要给 importer 设备做 DMA 用的。CMB 的地址是 PCIe BAR 上的地址不是系统内存地址。如果 importer 在 IOMMU 后面系统地址和总线地址之间还有一层翻译CMB 这种 BAR 地址要额外处理否则 DMA 会直接撞墙。这三个冲突就是NVMe CMB 到 DMA-BUF 内核接口之争的真正内核。4. 接口之争四条路线谁更适合 CMB4.1 路线一给 CMB 做一个专用 dma-buf heap最直观的方案是在drivers/dma-buf/heaps/里加一个nvme-cmb-heap。每个支持 CMB 的控制器注册一个 heap用户态open(/dev/dma_heap/nvme0cmb)申请 1~2MB拿 fd 传给 GPU。dma-buf 框架负责 fd 生命周期exporter ops 负责 mmap 和 DMA 地址看起来顺理成章。但很快你会撞上一堵墙heap 是通用分配器概念要求内存可拆分、可回收、可统计。CMB 不是。CMB 总量小控制器自己还要用不可能把整个 CMB 都交给 heap。剩下那点空间怎么和驱动内部队列分配器互斥你需要专门写一个资源管理器。为几 MB 内存写资源管理器还要支持热插拔、控制器 reset、多 namespace维护成本非常高。这还不算完。heap 框架假设内存可以被任意设备 attach 并做 DMA但 CMB 的 DMA 地址只在 PCIe 拓扑允许的范围内有效。有些 importer 设备在另一个 PCIe 域里或者被 IOMMU 挡着你导出的 buffer 它根本用不了。heap 的通用性在这里反而成了包袱。4.2 路线二从 NVMe ioctl 直接吐一个 dma-buf fd另一派人认为别搞通用 heap直接在 nvme 的 ioctl 里加一个命令返回 dma-buf fd。这样更直接想要哪段就导哪段只暴露 SDB 区域或者只暴露 DTO 数据区。权限也可以挂在文件节点上root 或特定 cgroup 才拿得到。这个方案的好处是边界清晰。NVMe 驱动对自己设备上的 CMB 有完全的控制力导出的每一段都经过了能力位校验。exporter 的 ops 不需要做通用分配器只需要管理哪段 BAR 内存被谁拿走了。缺点是 ioctl 是块设备 ABI往里面塞 DRM 圈子的概念有点突兀。以后每个想用这个能力的内核模块都得先学会解析一个块设备 ioctl 返回的 fd。而且 dma-buf 的价值恰恰在于通用性你把它绑死在 nvme 上等于把快递柜做成了只有一把钥匙的保险柜。将来换了别的设备这套接口没法复用。4.3 路线三先做通用设备内存 heap抽象还有人提出更宏大的方案既然 CMB 算设备内存不如先把 dma-buf heap 框架扩展成支持设备内存的通用框架再让 CMB、GPU VRAM、FPGA SRAM 都往里塞。这个方向在邮件列表上吵得最凶。问题在于设备内存不像 system RAM 那样有统一语义。有的只能 CPU 读有的要 WC 映射有的根本不能让 CPU 碰有的还分 read-only/write-only 区域。要把这些差异全塞进一个通用 heap 框架就必须定义一堆属性缓存模式、对齐、是否可回收、是否可 CPU 映射、是否需要 IOMMU 绕过。定义到最后DMA-BUF 的契约会膨胀得让所有人头疼。我见过太多为了通用性先搭抽象层、最后被现实打脸的例子。设备内存的抽象不是不能做而是应该等真的有五六个不同形态的消费方出现再做。现在只有一个 CMB就急着造通用框架大概率是过度设计。4.4 路线四什么都不做把 P2P 用起来第四条路线最反直觉别做 CMB dma-buf 了把 P2P DMA 用起来。现有pci_p2pdma机制已经允许一个设备的 DMA 直接访问另一个设备暴露的内存。NVMe 和 GPU 的场景里正确的做法是让 NVMe 控制器直接把数据 DMA 到 GPU 显存而不是绕道 CMB。原因很简单CMB 是控制器自己的内存数据要先从 SSD 闪存搬到 CMB再从 CMB 搬到 GPU两次 PCIe 事务。P2P 只需要一次。带宽上 CMB 方案没有任何优势延迟上也不占理因为 CMB 离控制器近不等于离 GPU 近。那 CMB dma-buf 什么时候还有价值我想到三个场景。平台不支持 PCIe P2P比如 IOMMU 挡路、PCIe 拓扑不允许需要把 Shadow Doorbell 或门铃区域暴露给另一个 agent做极低延迟命令提交虚拟化场景里想把 CMB 映射给 VM。这些场景很窄但真实存在。所以什么都不做也不是什么都不做而是不做数据路径保留窄场景的旁路。4.5 四条路线对照路线用户态体验内核复杂度资源冲突适合场景专用 CMB heap最标准fd 即拿即用高要写资源管理器严重和队列分配冲突通用共享、长期演进NVMe ioctl 直出 dma-buf一般依赖驱动接口中但 ABI 耦合可控按段导出窄场景、特权侧、门铃旁路通用设备内存 heap 抽象最标准但契约膨胀极高抽象成本大可控依赖框架定义多种设备内存都出现之后先做 P2P依赖 importer 支持中无数据路径绕开 CMB大带宽数据路径、零拷贝5. 我的取舍与踩坑记录一个驱动工程师的实操注记5.1 怎么确认你手上这块盘到底有没有 CMB先说排查方法。最省事的是看 dmesg 或 sysfs但路径和格式因内核版本而异不算稳定 ABI。排障时我会直接读 BAR 寄存器先用lspci -vvv找 BAR 的物理地址再用工具读偏移0x8和0xC。下面是一个典型的操作流程# 查看指定 NVMe 控制器这里假设 01:00.0的 BAR lspci -vvv -s 01:00.0 | grep -A5 Region 0 # 假设 BAR0 物理地址是 0x91000000用 lspci 看到的实际值替换 pcimem 0x91000000 0x8 w # 读 CMBLOC pcimem 0x91000000 0xc w # 读 CMBSZCMBLOC的 BIR 位告诉你 CMB 落在哪个 BARCMBSZ的低 32 位算出容量。比如CMBSZ 0xFFFCMB 大小就是(0xFFF 1) × 4KB 16MB。注意pcimem是直接操作设备寄存器的调试工具权限校验、总线锁定一概没有搞错了轻则读回垃圾重则把控制器弄挂。我建议只在专门用来折腾的调试盘上这么玩别拿生产环境的盘试。5.2 我踩过的坑dto、wc 映射、生命周期、sg 表第一个坑是能力位不看全。不是所有盘的 DTO 都置位很多消费级盘的 CMB 只支持 SDB不支持数据缓冲。你拿这样的 CMB 去做 dma-buf只能当门铃旁路用不能当数据中转区。上层如果不查能力位就直接导入 buffer跑起来就是各种诡异的 readback 错误。第二个坑是 WC 映射。CMB 如果用devm_ioremap_wc映射CPU 读性能极差多次写之间还有合并窗口。dma-buf 的mmap一旦暴露给用户态用户态随便做一次读循环就能把整个子系统的性能拖垮。必须把映射属性写进 dma-buf 的导出信息里让 importer 知道别当普通系统内存用。第三个坑是生命周期。dma-buf fd 的生命周期和控制器 reset 是不对齐的。nvme 控制器一 resetCMB 里的内容全部失效但用户态手里的 fd 可能还开着。你必须让所有已导出的 dma-buf 在 reset 路径上统一失效或者至少在 exporter ops 里把后续访问变成错误码。这个坑在普通 heap 里根本不存在因为系统内存不会被 reset。第四个坑是 sg_table 的 DMA 地址。map_dma_buf返回的 sg 不能走dma_map_page那套逻辑因为 CMB 没有struct page。你需要手动构造 sg把dma_address填成 BAR 物理地址dma_length填成块长度。示意代码如下static struct sg_table *nvme_cmb_map_dma_buf(struct dma_buf_attachment *attach, enum dma_data_direction dir) { struct nvme_cmb_buf *buf attach-dmabuf-priv; struct sg_table *sgt; sgt kzalloc(sizeof(*sgt), GFP_KERNEL); if (!sgt) return ERR_PTR(-ENOMEM); if (sg_alloc_table(sgt, 1, GFP_KERNEL)) { kfree(sgt); return ERR_PTR(-ENOMEM); } sg_init_table(sgt-sgl, 1); sgt-sgl-length buf-size; sg_dma_mark_bus_addr(sgt-sgl, buf-bar_dma_addr); return sgt; }这段只是示意真正提交补丁还要处理 attach 语义、地址重映射和 IOMMU 情况。但核心思想是明确的CMB 的 sg 是总线地址直填不是从struct page映射出来的。我见过不少实现把sg_set_page硬套在 CMB 上最后只在特定平台上没崩换一台机器就露馅。5.3 现阶段我的方案和 checklist我自己的立场很明确现阶段不支持为了 CMB 去造通用 device memory heap。数据路径走 P2PCMB 最多做一个门铃/命令队列的低延迟旁路而且只通过窄接口暴露。如果你也在调研这个方向我强烈建议按下面的 checklist 走一遍先确认硬件的 DTO/SDB 能力位没有能力位就放弃对应用途算清可用容量CMB 总大小减去控制器保留部分剩下的才是能导出的明确使用模型门铃旁路还是数据中转不要混用确认 CPU 映射属性WC、UC 还是 Cacheable写进导出信息设计生命周期策略控制器 reset 路径上所有已导出 dma-buf 必须统一失效想清楚 IOMMU 策略绕过、还是走pci_p2pdma辅助函数在真实平台上测带宽和延迟而不是只看 sysfs 数字这套流程能帮你过滤掉大部分纸上谈兵的设计。我也见过不少团队在第一步就倒下——他们手里的盘根本没有可用的数据型 CMB却已经写了三页设计文档。6. 热词背后用户看到的速度内核欠的账6.1 启动时间不是一块盘的独角戏回到开头那些热词。有人搜 E5 平台 NVMe 跑 Win10 启动要多久有人搜怎么往旧镜像里塞驱动有人搜固态格式化。这些问题的共同点是用户把快简单地等同于盘快。实际上启动时间有一大半花在 UEFI 固件、设备初始化、驱动加载和 PCIe 链路训练上NVMe 的裸顺序读速度早在启动阶段就用不着。内核里 CMB 到 DMA-BUF 的接口之争也是一样技术方案好不好由整个链路决定而不由某一个寄存器决定。你可能会问这和 CMB dma-buf 有什么关系关系大了。CMB 如果做得好可以把命令提交的门铃延迟压下去让控制器响应更快做得不好反而会让驱动为了兼容 CMB 多绕几层跳转整体变慢。用户感知到的快是无数个这样的小决策共同堆出来的。6.2 格式化、驱动注入和命名都是接口的表象再往深一层/dev/nvme0n1p5、NTLite 注入驱动、格式化对齐这些都是接口的表象。命名是内核 block 层和 nvme 驱动把控制器、Namespace、分区三层结构翻译给用户驱动注入是操作系统在引导早期找到存储设备之前的鸡生蛋问题格式化对齐是文件系统和闪存页、块之间的对齐问题。CMB 到 DMA-BUF 的接口之争本质上也是同一件事怎么把控制器的能力安全、高效、可维护地交到上层手里。如果你只是普通用户那这三个热词对应的操作照着做就行。但如果你也写内核驱动或者做上层存储架构我建议你多想想接口设计的共性问题谁拥有资源、谁分配、谁回收、生命周期怎么对齐、安全边界怎么画。CMB 只是这些问题的又一个具体样本。6.3 这场接口之争最终会以什么形式落到你手上以后你在内核邮件列表里看到nvme: add cmb dma-buf support这类补丁标题先别急着夸按我上面那套 checklist 过一遍。至少要问三个问题它导出的到底是门铃区域还是数据缓冲它怎么处理控制器 reset它有没有绕过 IOMMU这三个问题能过滤掉九成看着漂亮、落地就翻车的方案。我自己在实验室里折腾过一段 CMB 做门铃旁路的原型。结论是门铃旁路确实能把命令提交延迟压下去几十纳秒但那点收益在真实业务里几乎被驱动栈其他部分的开销稀释干净。反而是 P2P 数据路径带来的吞吐提升一测就看得见。所以我的建议很明确数据走 P2PCMB 留给控制器自己别没事找事把它包装成通用 dma-buf。如果哪天你的场景真需要控制器内存出现在 importer 的地址空间里再回来把那条窄接口做扎实你绕过的每一个通用抽象都是将来替你省掉的一次维护噩梦。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑