上一篇把 I/O 提交说成「把调用方缓冲交给设备 DMA」。本篇钉这句话的前提:什么样的指针合法、对齐与 PRP 如何约束、mempool 解决什么、何时不得不拷贝。
常见误区:posix_memalign +
mlock 就以为可以扔给
spdk_nvme_ns_cmd_write;或把「零拷贝」理解成「从网卡包头到盘片全程从不
memcpy」。官方 DMA From User Space
把第一点判死刑;第二点在工程上往往是分层目标,而不是单函数保证。
本文是「SPDK / 用户态存储栈」系列第 5 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 4 篇 · NVMe 驱动 提交/完成与零拷贝声明 第 5 篇 · DMA / mempool 分配、对齐、池化、拷贝边界 第 6 篇 · bdev 核心 块层如何携带缓冲 第 3 篇 · 环境与绑盘 大页与 VFIO 原料
版本锚定:SPDK v26.01 Direct Memory Access (DMA) From User Space;env/DPDK 内存与
spdk_dma_malloc族 / mempool API @ tag v26.01。不粘贴伪造分配失败栈或 fio 零拷贝对比。
一、问题:设备看见的地址不是
void *
用户态指针是虚拟地址。NVMe 设备(无 IOMMU 时)在 PCI 上发起的 DMA 使用物理地址;有 IOMMU 时使用经翻译的总线地址 / IOVA。无论哪条路,运行时都必须能在提交命令时填入设备能懂的地址,并保证传输期间映射不变。
操作系统不保证普通匿名页的虚拟→物理映射永恒不变:换页、compaction、透明压缩、热加内存重平衡都可能移动页框。内核驱动用内核 DMA API 处理钉页与映射;用户态驱动必须走另一套契约。
POSIX mlock
只保证「有物理页backing、不被换出」,不保证页框号不变,也不是可移植的
pin API。因此「先 malloc 再 mlock」不是 SPDK
认可的数据面分配路径。
二、官方结论:用
spdk_dma_malloc() 族
DMA From User Space 的收束句:
所有传给 SPDK 的数据缓冲,都必须用
spdk_dma_malloc()或其兄弟分配;SPDK 依赖 DPDK 已验证的底座做内存管理。(与 DPDK 合用时,DPDK mbuf 也是安全的。)
含义分解:
| 要求 | 原因 |
|---|---|
| 经 SPDK/DPDK 分配器 | 登记到可翻译的内存映射表;已知物理/IOVA |
| 钉住 / 大页 | 降低映射被内核挪走的风险 |
| 可与 IOMMU 配合 | VFIO 下用进程映射做设备侧翻译(长期正确路径) |
setup.sh 预留的 hugepage
是原料;业务缓冲仍要显式从 DMA
分配器取出。把大页耗尽与「偶发 I/O
失败」联系起来时,先查池与分配,而不是先怪盘。
flowchart LR
HP["Hugepages / VFIO IOVA"]
DMA["spdk_dma_malloc family"]
POOL["spdk_mempool"]
IO["NVMe PRP/SGL + DMA"]
HP --> DMA
DMA --> POOL
DMA --> IO
POOL --> IO
BAD["malloc / new"] -.->|"not valid"| IO
三、PRP:规范如何约束物理布局
在 IOMMU 普及前(以及设备仍报 PRP 时),NVMe 1.0 要求内存能被 PRP list 描述。官方文档概括的性质包括:
- 按设备页(已知实现多为 4KiB)切开;
- 第一页可以是从任意 4 字节对齐地址开始的部分页,但不可跨过当前物理页末尾;
- 若有多页,第一页必须结束在物理 4KiB 边界;
- 最后一页从物理 4KiB 边界开始,结尾可不对齐到页末。
NVMe 1.1 起有更灵活的 SGL,但是可选;文档写明写作时多数设备仍弱于「任意 scatter」。工程后果:
- 随意拼起来的多段缓冲可能根本无法生成合法 PRP。
- 对齐失败的故障可能出现在提交期(命令构造失败)或表现为设备级错误——取决于路径,但根因常在缓冲布局。
- 即使用 IOMMU,「乱切的用户态 iovec」仍可能在驱动/传输层被拒绝或被迫拷贝——不要假设 IOMMU 赦免所有布局罪。
数学上,一次传输触及的物理页集合必须可被 PRP/SGL 规则枚举;分配器通过大页与对齐的返回块,把「偶然合法」变成「默认合法」。
四、IOMMU:长期地基,而不是可选美化
同一文档把 IOMMU
写成未来可证明正确的方向:设备看到的是总线地址,经 IOMMU
落到进程映射,内核可移动底层物理页而不拆掉进行中的
DMA(在映射仍有效的前提下)。Linux vfio-pci
是用户态配置入口。
与第 3 篇对齐:
- 推荐:VFIO + IOMMU 开启;
- 大页:今日仍广泛依赖其稳定行为,但文档已提示「非设计上的永恒保证」;
- 权限:IOMMU 就绪时,示例可能降权运行;否则常需 root。
排障口诀:分配器报错先看大页与 EAL;能分配但 I/O 怪错再看对齐/IOMMU 组/设备是否仍在 VFIO。
五、Mempool:复用、局部性与生命周期
热路径上每次 spdk_dma_malloc /
free 都能工作,但对小块高 IOPS
不友好:分配器锁/跨核、缓存冷、失败路径复杂。SPDK(经
DPDK)提供 mempool:预分配一堆同尺寸 DMA
安全对象,运行时 get/put。
典型用法(语义级,符号以 v26.01 头文件为准):
- 创建池时指定元素大小、数量、socket/NUMA、缓存行为;
- I/O 提交前
spdk_mempool_get(或封装名)取缓冲; - 完成回调里再
put回池——生命周期必须盖过 DMA 进行中的区间。
/* Illustrative shape only — verify prototypes against include/ @ v26.01 */
void *buf = spdk_dma_malloc(4096, 0x1000, NULL);
if (buf == NULL) {
/* handle allocation failure — do not fall back to malloc for DMA I/O */
}
/* ... submit NVMe I/O using buf ... */
/* ... after completion callback ... */
spdk_dma_free(buf);池化时把 malloc/free 换成
get/put,不变量不变:飞行中的 I/O
不得回收缓冲。过早 put 是经典
use-after-free/DMA 踩踏;过晚 put
则表现为池耗尽、提交失败——表面像「盘队列满」。
与 bdev 的衔接(第 6 篇预告):块层请求携带的 iov 必须已经是 DMA 安全,或由模块在边界上显式拷到安全缓冲。拷贝发生在哪一层,决定延迟账记在谁头上。
六、零拷贝 vs 显式拷贝:一张边界表
| 场景 | 更可能零拷贝 | 更可能要拷贝 |
|---|---|---|
应用缓冲本身来自 spdk_dma_* /
mempool,并满足 PRP |
是 | |
缓冲来自 malloc、语言运行时堆、部分
mmap |
是(先搬到 DMA 缓冲) | |
| 前端是 socket 读入的普通缓冲,后端是 SPDK NVMe | 仅当缓冲分配策略从一开始就按 DMA 设计,或走特殊跨栈机制 | 常见路径:拷一次 |
| Admin 命令某些 API | 视 API | 文档已提示可能有拷贝 |
| 为了对齐/合并碎片 iov | 驱动或上层可能 bounce |
所以:驱动文档的 zero-copy = 「驱动不藏载荷 bounce」;系统是否端到端零拷贝,取决于你有没有在边界上引入拷贝。站内 Zero Copy 的肮脏真相 讨论的是 socket/文件路径的类似账本——本系列把同一怀疑态度用在块/NVMe 上。
不写未实测的「拷贝掉百分之多少延迟」;只要求把拷贝标成显式层,以便第 13–14 篇归因。
七、失败模式与观测直觉
| 症状(语义) | 优先怀疑 |
|---|---|
| 初始化或首次 I/O 分配失败 | 大页不足、HUGEMEM、cgroup
hugetlb、池尺寸 |
| 提交返回错误 / 无法构造命令 | 对齐、长度跨页非法、非法指针来源 |
| 随机数据损坏 / 机器 check | 飞行中回收缓冲、错误 IOVA、设备被错误解绑(灾难级) |
| 池「慢慢漏光」 | 完成路径未
put、错误路径泄漏、回调未注册 |
| 换核后变慢 | 跨 NUMA 取缓冲;池 socket 与 reactor 不一致 |
无本机直通 NVMe 时,不拿伪造日志证明上述条目;它们来自机制推演与官方约束,供实机时对照。
八、设计谱系与开放问题
谱系:内核 DMA API → 用户态 hugepage 实用主义 → IOMMU/VFIO 正规化。DPDK 先在网卡侧把这条路走通;SPDK 把同一内存故事接到 NVMe PRP/SGL。
争论与开放问题:
- 大页依赖 vs IOMMU 纯净模型:今日部署常两者叠用;内核演进若改变大页可迁移性,IOMMU 路径的权重会上升(官方已预警)。
- 池尺寸与内存税:预留过多大页损害共置密度;过少则在突发时分配失败——没有无负载的通用公式。
- 跨组件零拷贝:NVMe-oF、vhost、压缩/加密叠层时,每一跳都可能逼出拷贝;「全链路 ZC」是产品叙事还是可检验不变量,要在第 8–11 篇逐跳核对。
- 与
O_DIRECT缓冲:内核 Direct I/O 也有对齐约束,但检查与 bounce 策略在内核;SPDK 把检查与失败更多暴露给应用——灵活与锋利并存。
九、把约束写成「分配策略」而不是「每次祈祷」
可维护的用户态存储进程通常把内存策略收成三条厂规:
- 数据面只许一种分配入口(
spdk_dma_*/ 指定 mempool);禁止业务模块私自malloc后「碰巧对齐」。 - 按 NUMA / reactor 建池:哪个核提交,就优先用哪个 socket 的池,避免跨节点拉缓冲。
- 错误路径也要还池:提交失败、超时回调、热拔错误完成——每条路径审查是否
put/free,用计数器盯泄漏。
与编程语言运行时交互时(Go/Java/Rust 自管堆),边界更刺眼:要么在 FFI 侧预先准备 DMA 池并由原生层持有,要么接受显式拷贝层。试图把托管堆对象「固定后直接 DMA」需要极强的运行时支持与审核,默认应视为不安全幻想,除非有专门机制与测试。
相对内核
O_DIRECT:应用同样要对齐,但内核可以在部分失败路径上
bounce 或返回清晰 errno;SPDK
把更多布局责任前移——这是性能来源之一,也是新人脚枪来源之一。教学与
code review
应把「缓冲出身」当成一等注释,而不是假设读者记得第 5
篇。
若必须对接「外来缓冲」(例如从内核 aio
bdev、socket、或托管运行时划入的内存),把
显式拷贝模块画进架构图:一端
memcpy/spdk_dma
填充,一端只认池内指针。隐藏在中间某层的静默 bounce
会让延迟账对不上,也会让「我们已经零拷贝」的表述失去可检验性。能证明的零拷贝,才配写进设计文档。
十、参考资料
规范 / 官方文档(A)
- SPDK, Direct Memory Access (DMA) From User
Space @
v26.01:
spdk_dma_malloc、hugepage、PRP、IOMMU。 - SPDK, NVMe Driver @ v26.01:零拷贝声明、内部 tracker 内存(非载荷缓冲)。
- SPDK, Getting Started @
v26.01:
HUGEMEM/setup.sh。 - NVMe Base Specification:PRP / SGL 章(与 storage/03 交叉)。
源码(A)
spdk/spdktag v26.01:include/spdk/env.h(DMA 分配族)、mempool 相关头文件、lib/env_dpdk/(目录级)。
站内对照
实验台账
- 无分配器基准测试;无伪造输出。
十一、小结
- DMA
安全分配是硬前提;
malloc/mlock不能替代spdk_dma_malloc族。 - PRP/SGL 约束物理布局;对齐与分段错误是一类独立失败模式。
- IOMMU+VFIO 是地址翻译的长期正确故事;大页是今日部署的重要支柱。
- Mempool 服务热路径复用,但生命周期必须覆盖 DMA 飞行区间。
- 零拷贝是「驱动不藏 bounce」;端到端是否拷贝要按边界逐层记账。
- 厂规化分配入口与还池路径,比在出故障后争论「是不是零拷贝」更有用。
→ 上一篇:用户态 NVMe 驱动 · 系列目录 · 下一篇:bdev 核心
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【SPDK 用户态存储】环境与绑盘:Hugepage、CPU 亲和与 VFIO/UIO
说明 SPDK 运行前 env:大页分配、CPU 绑核语义、setup.sh 将 NVMe 从内核驱动解绑到 VFIO/UIO;钉住设备独占代价与 reset 回退路径。
【SPDK 用户态存储】用户态 NVMe 驱动:Controller、Qpair 与 Poll Group
拆解 SPDK 被动式 NVMe 库:probe/attach、admin 与 I/O qpair、完成轮询与 poll group;说明单线程独占 qpair、零拷贝提交路径及相对内核驱动的边界。
【SPDK 用户态存储】用户态存储全景:从 Reactor 到 NVMe-oF 的坐标系
定位 SPDK 相对内核块层、O_DIRECT+io_uring 与 NVMe 协议百科的生态位;钉住 I/O 路径、线程模型、设备独占、块抽象、远端传输五条坐标系,并给出 16 篇阅读路线。
【SPDK 用户态存储】Reactor 与线程模型:Event、Poller 与 Message Passing
钉住 SPDK event framework:每核 reactor、事件队列、poller 轮询与跨核消息;说明 shared-nothing 边界,以及在 reactor 上阻塞等于饿死同核所有工作。