核心判断
写 DMA 驱动时,最蠢也最常见的死法,就是还没把骨架立住,就急着写数据路径。正确顺序恰恰相反:先把 probe、资源、状态、接口、清理路径搭成一套能自洽的最小骨架,再往里塞 DMA 逻辑。
这一章解决什么问题
这一章不追求“功能豪华”,只追求一件事:
搭出一套最小可运行、可扩展、可收尾的 DMA 驱动骨架。
最小骨架至少要回答:
- probe 时初始化什么
- remove 时清理什么
- 哪些资源跟设备生命周期绑定
- 哪些资源跟文件句柄或用户上下文绑定
- 中断、wait queue、
mmap、ioctl的位置怎么预留
最小驱动骨架长什么样
这图看起来朴素,但它给出一个核心顺序:
先有资源与状态,后有运行态逻辑。
设备私有结构体:别把状态撒得到处都是
一个最小骨架,建议先收敛到一个私有结构体里。比如:
这里最重要的不是字段名,而是观念:
- DMA 资源要集中管理
- ring 状态要集中管理
- 运行态标志要集中管理
别把状态散在全局变量、回调上下文和神秘静态数组里。那不叫灵活,那叫给未来的自己挖坑。
probe 阶段最少要做哪些事
1. 识别并保存设备上下文
- 绑定
struct device
- 初始化私有数据
- 建立必要的锁和等待队列
2. 声明 DMA 地址能力
这一步的核心不是形式,而是确认设备能看多大地址空间。
如果硬件支持 64-bit,就别装 32-bit 设备;如果只支持 32-bit,也别假装系统会替你兜底。
3. 分配 DMA 相关资源
最小集合通常包括:
- 数据 buffer
- descriptor 或 slot 元数据
- ring 管理结构
这里先不用追求极致抽象,先把生命周期边界画清楚:
- 谁分配
- 谁释放
- 失败时如何回滚
4. 初始化状态机和 ring 元数据
比如:
- 每个 slot 初始状态
prod_idx/cons_idx
- in-flight 计数
- completion 标志
5. 注册中断与完成路径
至少要明确:
- IRQ 入口在哪里
- 完成后唤醒谁
- 是在中断里直接收尾,还是交给下半部/线程化处理
6. 暴露用户态入口
哪怕你还没把所有语义写完,也应该先把这些位置留出来:
open
release
unlocked_ioctl
mmap
poll
别等数据路径写完才发现:原来用户态接口没地儿挂,或者清理路径根本没设计。
一个非常典型的最小骨架:coherent ring
如果你一开始只是想先把控制面骨架站住,一个很常见的起手式就是:
- ring / descriptor 用 coherent
- payload 暂时也先简单处理
它的优点不是“最强”,而是:
- 地址和一致性语义更稳
- 容易验证 probe / remove / irq / ring 结构是否正确
- 适合第一版把系统先跑起来
这个模式特别适合:
- ring buffer
- descriptor 区
- 长生命周期控制结构
如果 descriptor 很多又很小,早点考虑 DMA pool
小块高频对象如果直接反复
dma_alloc_coherent(),往往既笨又重。更合理的方式是:
- 大块 payload 和 ring 分开
- 小 descriptor / command buffer 用
dma_pool
这不是“高级优化”,而是骨架阶段就该有的资源模型意识。
一个最小的 probe 骨架
这段代码最值钱的部分不是函数名,而是回滚结构:
每成功一步,就必须知道失败时怎么退回来。
remove 路径为什么不能敷衍
很多人把 remove 当成“收尾代码”,实际上它是检验骨架是否成熟的照妖镜。
remove 至少要处理:
- 停止硬件 DMA
- 禁止新请求进入
- 等待或强制回收 in-flight buffer
- 注销字符设备/接口
- 释放 IRQ
- 释放 DMA 资源
如果 remove 写不干净,说明你整个生命周期其实都没设计完整。
open / release:文件句柄不是摆设
open 阶段要想什么
- 是共享设备上下文,还是为每个 fd 建独立 session?
- 设备忙时是否允许多开?
- 是否要记录当前 owner?
release 阶段要想什么
- 这个 fd 是否仍持有未 consume 的 slot?
- 是否需要强制归还
mmap相关上下文?
- 是否需要在最后一个 fd 关闭时停机或清理 ring?
很多 DMA 驱动不是死在传输逻辑,而是死在用户态关闭文件后,驱动还假装一切岁月静好。
中断与完成路径先占位,不要后补
一个最小完成路径,至少要做到:
- 确认哪一个 slot / descriptor 完成
- 更新状态到
READY_FOR_CPU或完成队列
- 唤醒等待者
- 必要时安排后续处理
一个常见的坑是:
- 中断先来了
- 但软件侧没有清晰的 slot 状态机
- 于是完成到了,谁也不知道该交给谁
ioctl / mmap / poll 的角色预留
即使这章还不展开,也必须先给它们定位:
接口 | 主要职责 |
ioctl | 提交、查询、消费、控制命令 |
mmap | 暴露数据面共享区 |
poll | 等待完成事件或可读状态 |
如果你不提前给这三者分工,后面很容易出现一个经典惨案:
mmap被拿来偷渡状态
ioctl被拿来传数据
poll只剩装饰意义
最小资源检查清单
probe 成功前必须确认
- DMA mask 设置成功
- buffer / descriptor 分配成功
- ring 元数据初始化完成
- 中断注册成功
- 接口节点注册成功
remove 前必须确认
- 新请求已阻止进入
- 硬件不再继续 DMA
- in-flight buffer 已回收或作废
- 等待队列与会话已释放
- 资源释放顺序正确
本章结论
- DMA 驱动先搭骨架,再填数据路径;别反过来。
- probe / remove 不是模板活,而是生命周期治理的起点和终点。
- coherent ring 是一个很适合起步的骨架形态,但别误以为它能替你解决后面所有数据面问题。
- 设备私有结构体、ring 元数据、中断完成路径、用户态入口都应在最小骨架阶段就站住位置。
- 回滚路径写不顺,说明资源模型还没设计好。
配套阅读
如果你正在把骨架真正落成代码,建议和下面几章配套看:
- 04|Ownership、协议与生命周期:DMA 设计的中轴:先把 slot 交接规则钉死。
- 06|内核态 DMA 收发实战:先跑通一条闭环:把骨架推进成真正可运行闭环。
- 09|常见坑、异常路径与调试验证:别把 DMA 写成玄学:防止 probe / remove 写得像样,运行态却死得难看。
- 12|API、模板与快查附录:写代码时拿来当检查表。
下一章要干什么
下一章开始把骨架跑起来:
- 内核态收发闭环
- prepare / submit / complete / reclaim
- timeout / reset / error path
也就是说,从下一步开始,系统不只是“站起来”,而是要开始“跑起来”。






