Lazy loaded image
Words 0Read Time 1 min
Invalid Date
🗺️
核心判断
写 DMA 驱动之前,最应该先看清的不是 API,而是数据路径和控制路径到底怎么分层。很多实现之所以写着写着就乱,不是因为函数不会调,而是因为脑子里根本没有一张全链路图。

这一章解决什么问题

你后面会碰到一堆名词:
  • buffer
  • descriptor
  • doorbell
  • completion
  • ring
  • mmap
  • ioctl
  • 中断
  • poll
 
如果没有全景图,这些词就会像一堆散装零件;有了全景图,它们才会变成一台机器。
 
本章的任务很简单:
先把一次 DMA 传输从设备到用户态的完整链路画出来。

一张总图先压住全场

这张图里,最核心的不是“设备连内存”,而是下面三件事:
  1. 数据面:真正承载 payload 的 buffer
  1. 控制面:提交、完成、回收、错误处理
  1. 同步边界:CPU、设备、用户态之间何时能看见同一份最新数据

先分清数据面和控制面

数据面:真正承载数据的地方

数据面通常包括:
  • 设备读的发送 buffer
  • 设备写的接收 buffer
  • ring buffer 中每个 slot 的 payload
  • 用户态 mmap 看到的那块共享区
 
数据面的目标通常是:
  • 高吞吐
  • 尽量少拷贝
  • 可持续流转

控制面:保证这条通路不翻车

控制面通常包括:
  • descriptor
  • head / tail
  • valid / done / owner 状态位
  • ioctl / poll / 中断
  • timeout / reset / reclaim
 
控制面的目标不是快,而是:
  • 明确谁能提交
  • 明确谁已完成
  • 明确谁可以回收
  • 明确异常怎么收口
 
一句话:
数据面负责搬东西,控制面负责防止大家同时动同一块东西。

一次 DMA 传输,从设备视角看是什么

场景 A:设备从内存读数据(TX)

比如 CPU/用户态准备好一块发送数据,让设备拿去发。
在这个路径里,设备的角色像一个消费者:
  • CPU/用户先生产数据
  • 驱动完成提交
  • 设备消费 buffer

场景 B:设备把数据写入内存(RX)

比如音频采样、网络收包、ADC 连续采集。
在这个路径里,设备的角色像一个生产者:
  • 驱动先准备空 buffer
  • 设备往里写
  • CPU/用户再来消费
 
这类 RX 场景,也是本专题的主线。因为它天然会引出 ring buffer、ownership、completion、零拷贝这些核心问题。

descriptor、doorbell、completion 到底各管什么

组件
作用
本质问题
descriptor
描述某个 buffer 的地址、长度、控制位
“设备该处理哪块内存?”
doorbell
通知设备有新任务可处理
“现在可以开始干活了吗?”
completion
表示一次处理已完成
“干完了吗?结果现在归谁?”

descriptor 不是数据本身,而是任务说明书

descriptor 常常包含:
  • DMA 地址
  • 长度
  • 控制标志
  • ownership / valid / done 之类的状态位
 
别把 descriptor 理解成“附属结构”。很多 DMA 驱动真正的状态机,其实就藏在 descriptor 和 ring 推进语义里。

doorbell 是交接动作,不是业务逻辑

doorbell 可能是:
  • 写某个寄存器
  • 推进 producer index
  • 设置某个位
 
它的语义通常是:
“驱动这边准备好了,你可以开始看了。”

completion 是所有收尾逻辑的起点

completion 可能来自:
  • 硬件中断
  • 状态寄存器轮询
  • descriptor done bit
  • DMAEngine 回调
 
completion 不是“整个系统结束”,而只是:
设备阶段结束,下一阶段可以开始交接。

从驱动视角看,一次传输究竟做了什么

驱动做的第一件事:组织内存

驱动需要先准备:
  • DMA buffer
  • descriptor/ring
  • 同步原语
  • 中断和完成通知路径
 
说白了,设备擅长搬,驱动擅长组织。

驱动做的第二件事:完成 ownership 交接

这一步包括:
  • 什么时候 buffer 从 CPU 交给设备
  • 什么时候设备交回给 CPU
  • 什么时候 CPU 再交给用户态
 
这也是为什么后面的重头戏不是“分配内存”,而是“定义生命周期”。

驱动做的第三件事:把异常路径写完整

真实系统里,驱动必须准备面对:
  • completion 丢失
  • timeout
  • reset
  • 用户态不消费
  • 进程提前退出
 
如果没有这些设计,你的 happy path 越漂亮,翻车时越壮观。

从用户态视角看,全链路意味着什么

当用户态介入时,系统会多出一层协作关系:
  • 用户态可能通过 mmap 直接看到数据面
  • 用户态通过 ioctl / poll 与驱动走控制面
  • 用户态不应直接绕过协议修改正在被设备使用的 buffer
 
所以用户态不是“旁观者”,而是全链路里的第三个参与方。
 
从这一刻起,系统里至少有三方:
  • 设备
  • 驱动 / CPU
  • 用户态
 
三方都能看到某些资源,但不是三方都能在任意时刻改它。这个限制,就是后面 ownership 状态机存在的理由。

一条典型的流式采集全链路

这条链路里,最值钱的观察不是“发生了几步”,而是:
  • 数据面只在必要时被访问
  • 控制面负责推进状态
  • 每次推进都在做 ownership 切换

写驱动时最容易丢掉的全局视角

只盯着 DMA API,不看系统边界

结果就是:
  • buffer 有了
  • sync 也调了
  • 但 nobody knows 下一步该谁动
 
这类代码最容易出现“偶发旧数据”“重复提交”“早回收”这类半鬼不鬼的问题。

只盯着设备,不看用户态接口

一旦需要零拷贝,用户态是否知道:
  • 什么时候能写
  • 什么时候能读
  • 什么时候必须停止碰 buffer
 
如果答案含糊,那问题已经埋好了。

只盯着 happy path,不看回收路径

全链路图里最不该省略的,就是:
  • completion 后谁接手
  • 超时后谁清场
  • reset 后旧 buffer 如何作废
 
这些看起来“像细节”,其实决定系统有没有资格上线。

本章结论

  1. 一次 DMA 传输不是一个 API 调用,而是一条跨设备、内存、驱动、用户态的完整链路。
  1. 这条链路里必须分清数据面控制面
  1. descriptor、doorbell、completion 分别解决“处理谁”“何时开始”“何时结束”的问题。
  1. 一旦用户态进入链路,DMA 问题就升级成多方协作协议问题。

下一章要干什么

下一章开始打地基:
  • 地址空间
  • cache 一致性
  • sync / barrier
  • IOMMU
  • DMA mask
 
因为如果你连“设备看到的地址”和“CPU 看到的地址”都没分清,后面所有生命周期设计都会建立在幻觉上。
上一篇
Data Structure and Algorithm
下一篇
用面试拷问嵌入式技术栈

Comments
Loading...