DMA传输核心三要素:描述符、环形缓冲、中断机制
搞PCIe DMA,说到底就是三个东西在跳舞:描述符、环形缓冲、中断机制。这三者缺一个,传输就玩不转。我刚入行那会儿,总觉得描述符就是个结构体,环形缓冲就是个队列,中断就是发个信号——简单嘛。结果第一个板卡调了整整两周才跑通,就是因为对这三个角色的配合关系没吃透。
这一章咱们就把它们拆开揉碎,看看每个到底干嘛的,怎么配合才能让数据飞起来。
1. 描述符——传输的“任务单”
描述符(Descriptor)说白了就是一张任务单。CPU往这张单子上写:从哪取数据、写到哪、传多少字节、传完要干嘛。然后DMA控制器照着单子干活。
我习惯把它看成“文件传输请求”的精简版。每个描述符通常包含这些字段:
| 字段 | 说明 | 常见位宽 |
|---|---|---|
| 源地址 | PCIe设备侧的内存地址或BAR空间 | 64位 |
| 目的地址 | 主机侧物理地址(HPA) | 64位 |
| 传输长度 | 一次DMA搬运的字节数 | 16/32位 |
| 控制标志 | 中断使能、链式使能、方向等 | 16位 |
| 状态字段 | 传输完成后硬件回写的完成状态 | 32位 |
| 下一个描述符指针 | 链式DMA时指向下一个描述符 | 64位 |
描述符的存放位置也很关键。大部分方案让驱动程序在主机内存里分配一块连续的区域,把描述符数组放在那,然后通过寄存器把基地址告诉DMA控制器。控制器通过PCIe读描述符,拿到任务信息,再启动数据搬运。
// 典型的描述符结构体(C语言)
struct dma_descriptor {
uint64_t src_addr; // 源地址(设备侧)
uint64_t dst_addr; // 目的地址(主机侧)
uint32_t length; // 传输长度(字节)
uint16_t control; // 控制标志位
uint16_t status; // 硬件回写状态
uint64_t next_desc; // 链式下一个描述符地址
} __attribute__((packed));
2. 环形缓冲——描述符的“传送带”
单个描述符太慢了。你想,每传一笔都中断一次CPU,让CPU重新写描述符再启动,那跟PIO拷贝有啥区别?
所以就有了环形缓冲(Ring Buffer)。它本质是一个固定大小的描述符数组,头和尾指针在里边循环转。CPU在尾部插入新任务,DMA从头部取任务,处理完把头指针往前挪。就像工厂的传送带——CPU往传送带上放零件,DMA机械手拿走加工。
为什么叫“环形”?因为数组是循环使用的,写到底了就绕回开头。这样做最大的好处是:减少CPU和硬件的交互次数。CPU可以一次塞一批描述符进去,然后DMA自己挨个处理,处理完一根,自动取下一根——这个机制就是后面要讲的“链式DMA”的雏形。
环形缓冲还有两个重要寄存器:生产索引(Producer Index)和消费索引(Consumer Index)。CPU更新生产索引,DMA更新消费索引。怎么判断队列满了还是空了?有很多种实现,我最常用的是“留一个空位”法:当消费索引+1 == 生产索引时,队列满;当两者相等时,队列空。
// 判断环形缓冲状态(假设深度为N)
#define RING_SIZE 128
int ring_full(unsigned int prod, unsigned int cons) {
return ((prod + 1) % RING_SIZE) == cons;
}
int ring_empty(unsigned int prod, unsigned int cons) {
return prod == cons;
}
3. 中断机制——传输的“完成通知”
描述符和环形缓冲搞定了怎么干活,但CPU怎么知道活干完了?总不能一直轮询状态寄存器吧?那块PCIe带宽就浪费了。
这时候就得靠中断。DMA传输完一个描述符或一批描述符后,向CPU发送一个MSI/MSI-X中断。中断处理程序里,驱动更新消费索引,把完成的数据交给上层协议栈或用户态程序。
中断有两种常见模式:
- 每描述符中断:每个描述符完成都发一次中断。响应最快,但中断频率太高,CPU扛不住。适合低带宽、低延迟的场景,比如控制类消息。
- 批量中断:完成一批描述符(比如消费索引跳过N个)才发一次中断。高吞吐场景的首选,我做过NVMe SSD的驱动,就是批量中断配合中断合并(Interrupt Coalescing),吞吐提升30%以上。
中断的另一个重要细节是中断状态清除。硬件在描述符的状态字段里写一个完成标志,驱动读到这个标志后,再写寄存器清除中断。如果时序没处理好——比如驱动还没读完状态,硬件就把中断清掉了——那数据就可能丢失。
三要素的协同工作流
为了让你更直观地理解这三者怎么配合,我画了一张图。它把描述符、环形缓冲、中断的交互流程串了起来:
实践经验:把三者串起来
你看这张图,从左到右,从CPU到DMA到中断,形成了一个闭环。实际调试时,我最常用的方法是:先确认描述符的格式对不对(拿逻辑分析仪抓一次PCIe读写),再看环形缓冲的索引是否按预期增减,最后挂个示波器看中断引脚(或者用PCIe tracer看MSI-X包)。
嗯,这一章内容比较多,但你只要记住这三个概念以及它们之间的交互,后面讲链式DMA、多队列、原子操作的时候,你就不会觉得乱了。