核心判断
在真正进入
mmap 零拷贝之前,最该先验证的是:单纯在内核态里,这条 DMA 生命周期能不能完整闭环。 如果连 prepare、submit、complete、reclaim 这条链都没跑顺,就别急着把复杂度再拉进用户态。这一章解决什么问题
这一章的目标很明确:
先在内核态跑通一条完整 DMA 收发闭环。
先把这四件事钉死:
- 数据如何进入可提交状态
- 设备如何开始处理
- 完成后软件如何接手
- 结果如何被安全回收
只有这条闭环成立,后面谈零拷贝、用户态协议、性能优化才有资格。
一条最小闭环长什么样
这张图对应的核心问题只有一个:
这块 buffer 在每一步到底归谁。
从发送路径看闭环:CPU 准备 → 设备读取
典型场景
- 内核准备一块要发给设备的数据
- 驱动把它交给设备 DMA 读取
- 设备处理完成后返回完成事件
典型时序
一个很典型的 TX 骨架
发送路径的关键点
- CPU 写完后,不能立刻假设设备就能看到最新版本
- descriptor 写完后,不能让设备过早看到半成品
- 设备完成后,slot 才能回到可复用状态
很多所谓“偶发发错数据”,本质上都是提交和回收边界不清。
从接收路径看闭环:设备写入 → CPU 消费
典型场景
- 驱动预先准备空 buffer
- 设备把结果 DMA 写进去
- 完成中断到来后,CPU 再读取结果
典型时序
接收路径的关键点
completion到来不等于“所有人都能立刻安全读”
- 在 CPU 真正接手前,必须先把可见性问题处理好
- consume 不是装饰动作,而是回收语义的边界点
如果你的数据不是天然连续:SG 闭环怎么理解
当单块 buffer 不适合真实场景时,就会进入 SG:
- 多段内存被映射成一条 DMA 事务
- 完成后统一回收 / unmap
最关键的不是 API 数量变多,而是:
生命周期从“单 slot”扩展成“一个 SG 列表也有完整 owner 与回收边界”。
一个最小 SG 思维骨架
这里真正重要的不是会不会写
for_each_sg,而是你要始终记住:- map 的对象是一整组分段内存,不是某一个幸运 buffer
- 回收时必须按 SG 事务整体 unmap,而不是只盯某一段
- descriptor 链只是硬件视角的遍历结构,生命周期边界仍然由驱动来定义
再看两类最常见的实战骨架
1. coherent ring 骨架更适合作为第一版闭环
很多初版驱动最稳的起点,其实不是一上来就 streaming,而是先把:
- ring metadata
- descriptor
- 状态页
- producer / consumer 管理结构
放进 coherent 内存。
这类骨架的价值在于:
- 一致性语义简单
- 更适合先把生命周期和状态机跑通
- 方便先验证硬件接口、寄存器和完成路径
典型心智模型就是:
这类写法最适合回答一个问题:
我能不能先别急着追极限性能,而先把闭环秩序跑通?
2. streaming TX / RX 骨架更像性能版闭环
一旦进入高吞吐路径,常见形态就是:
- 提交前
dma_map_single()
- 完成后
dma_unmap_single()
- 中间 descriptor / irq / reclaim 把一整条生命周期串起来
比如 TX 路径你真正该盯的是:
- 映射是否成功
- skb / buffer 的生命周期是否和 DMA 生命周期绑定
- completion 后是否立即 unmap + free
这类骨架比 coherent 更接近真实网卡、块设备、采集链路的数据面。
代码层面最小应该拆成哪些函数
建议先把核心路径拆成下面几块:
这套拆法的价值在于:
- prepare 管分配与前置状态
- submit 管交接给设备
- irq/wait 管完成通知
- consume/reclaim 管回收
不要把这几步揉成一个大函数。揉在一起时,表面上省事,实际是在把状态机埋进 spaghetti code。
一个最小 slot 状态定义
然后配套约束:
prepare只能从FREE进入CPU_OWNED
submit只能从CPU_OWNED进入DEVICE_OWNED
irq complete只能从DEVICE_OWNED进入READY_FOR_CPU
consume只能从READY_FOR_CPU回到FREE
如果这些约束没落到代码里,后面任何“规范文档”都只是许愿。
prepare 阶段要做什么
- 找到空闲 slot
- 标记为
CPU_OWNED
- 返回 buffer 位置或上下文
对 TX 路径来说,此后 CPU/内核可以写数据;
对 RX 路径来说,此后驱动可以把这个 slot 交给硬件准备接收。
submit 阶段要做什么
- 检查 slot 当前状态是否合法
- 做
sync_for_device
- 填写 descriptor
- 保证 descriptor 与 doorbell 顺序正确
- 通知设备开始处理
- 状态切到
DEVICE_OWNED
submit 的本质不是“调一个 API”,而是:
把控制权正式从软件交给设备。
completion 阶段要做什么
在中断里至少要完成这些动作
- 确认是哪一个 slot 完成
- 从
DEVICE_OWNED迁移到下一阶段
- 若设备写了数据,则执行
sync_for_cpu
- 把结果放入可消费队列或唤醒等待者
一条建议
别在中断里写太多业务逻辑,但也别让中断只打印一句 “DMA done”。
最低限度也要推进 slot 状态,否则完成事件到了也等于没到。
wait / consume / reclaim 分别干嘛
wait
- 阻塞或轮询,直到有完成 slot
- 获取当前可消费结果
consume
- 表示“上层已读完这份结果”
- 是逻辑上的确认动作
reclaim
- 把 slot 重新放回空闲池
- 恢复到下一轮可复用状态
很多代码最大的问题是把 consume 和 reclaim 混成一件事,甚至干脆省略 consume。结果就是:
- 上层是否真正用完数据,无人知道
- buffer 可能被过早复用
最小接收闭环伪代码
这段伪代码真正表达的是:
一个 slot 不只是“用一下”,而是完整经历一次受控生命周期。
timeout 和 reset 必须怎么接进闭环
timeout
如果等太久没完成,至少要决定:
- 当前 slot 标记成
ERROR还是继续等待
- 是否触发设备重置
- 是否通知上层这次事务失败
reset
reset 后必须重新同步软件和硬件世界:
- 清理 in-flight 状态
- 作废旧 descriptor/completion
- 重建可提交队列
别把 reset 想成“再试一次”。对 DMA 路径而言,reset 本质上是在宣布旧生命周期结束并建立新世界。
一组最重要的调试日志
建议优先打印状态迁移,而不是只打印函数名:
这比打印:
有用得多。后者只是热闹,前者才有证据链。
本章结论
- 在进入用户态零拷贝前,必须先在内核态跑通完整 DMA 闭环。
- prepare、submit、complete、consume、reclaim 这五步最好显式拆开。
- TX、RX、SG 这些路径虽然形态不同,但本质都在回答同一个问题:这次 DMA 生命周期现在归谁。
- slot 状态迁移必须落到代码约束里,而不是只留在脑子里。
- timeout 和 reset 不是边角料,而是闭环的一部分。
配套阅读
这一章最适合和下面几章一起看:
- 05|最小 DMA 驱动骨架:从 probe 到 remove:先确认资源模型和生命周期壳子是稳的。
- 04|Ownership、协议与生命周期:DMA 设计的中轴:所有闭环步骤,本质上都在执行 owner 切换。
- 07|mmap 零拷贝与用户态协作:接口协议比映射更重要:把当前两方闭环扩成三方系统。
- 09|常见坑、异常路径与调试验证:别把 DMA 写成玄学:闭环一旦不稳,优先去那里查异常边界。
下一章要干什么
下一章开始把内核闭环扩展到用户态:
mmap数据面
ioctl/poll控制面
- 用户态 submit / wait / consume 协议
因为一旦把用户态拉进来,系统就从“两方交接”升级成“三方协作”。复杂度会立刻上台阶。






