4. Linux中的DMA实现:DMA控制器工作原理、DMA引擎驱动、dmaengine框架
聊到回测系统的性能优化,DMA是个绕不开的话题。我最早接触DMA是在做高频行情解析的时候,那时候CPU占用率居高不下,后来发现大量时间都花在内存拷贝上。嗯,DMA就是来解决这个问题的。
4.1 DMA控制器工作原理
DMA控制器,说白了就是一个专门负责搬运数据的硬件模块。CPU告诉它「把A地址的数据搬到B地址」,然后就可以去干别的事了。搬运工作由DMA控制器独立完成,搬完了再通知CPU一声。
为什么会这样设计?你想想看,如果每次数据拷贝都要CPU亲自下场,那CPU就成了搬运工,而不是指挥官了。尤其是在回测系统里,大量历史数据需要在内存和磁盘之间来回倒腾,CPU要是每次都亲自拷贝,那计算性能就别想要了。
DMA控制器的核心工作流程其实很简单:
- 请求阶段:外设或软件发起DMA传输请求
- 仲裁阶段:多个DMA请求同时到来时,控制器按优先级裁决
- 传输阶段:DMA控制器接管总线,直接进行数据传输
- 完成阶段:传输结束,DMA控制器发送中断通知CPU
关键点:DMA传输期间,CPU可以并行执行其他任务。这才是性能提升的本质。
我在项目中遇到过一个问题:DMA传输的数据量太小,反而比CPU直接拷贝还慢。为什么?因为DMA的初始化开销是固定的,数据量太小的话,这个开销就摊不薄了。一般来说,传输长度超过几十个字节,DMA才开始有优势。
4.2 DMA引擎驱动
Linux内核里,DMA引擎驱动负责管理DMA控制器的硬件资源。它提供了一套统一的接口,让上层模块不用关心具体的DMA硬件实现。
我记得第一次看DMA引擎驱动的代码时,被各种回调函数搞得头晕。后来理清了思路,其实核心就几个东西:
- DMA通道:每个DMA控制器有多个通道,可以并行处理多个传输任务
- DMA描述符:描述一次传输任务的数据结构,包含源地址、目的地址、长度等信息
- DMA传输类型:内存到内存、内存到外设、外设到内存
在回测系统里,我们最常用的是内存到内存的DMA传输。比如把一大块历史行情数据从缓冲区拷贝到计算区,用DMA来做,CPU就可以腾出手来算策略信号。
避坑指南:我曾经在调试DMA驱动时,发现传输完成中断一直不来。查了半天,原来是DMA描述符的地址没有做缓存一致性处理。在ARM架构下,DMA和CPU看到的缓存可能不一致,需要显式地flush cache。
4.3 dmaengine框架
Linux内核从2.6版本开始引入了dmaengine框架,目的是统一各种DMA控制器的驱动接口。这个框架把DMA操作抽象成了几个核心概念:
| 概念 | 说明 | 对应数据结构 |
|---|---|---|
| DMA设备 | 代表一个DMA控制器硬件 | struct dma_device |
| DMA通道 | 设备上的一个传输通道 | struct dma_chan |
| DMA描述符 | 描述一次传输任务 | struct dma_async_tx_descriptor |
| 传输操作 | 启动、暂停、停止等控制接口 | struct dma_device 中的回调函数 |
使用dmaengine框架的典型流程是这样的:
// 1. 请求DMA通道
struct dma_chan *chan = dma_request_channel(mask, filter_fn, NULL);
// 2. 准备传输描述符
struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_dma_memcpy(chan, dst_addr, src_addr, len, flags);
// 3. 设置完成回调
tx->callback = my_dma_callback;
tx->callback_param = &my_data;
// 4. 提交传输任务
dmaengine_submit(tx);
// 5. 启动传输
dma_async_issue_pending(chan);
// 6. 等待完成(在回调中处理)
void my_dma_callback(void *param) {
// 传输完成,处理数据
}
我个人习惯把DMA传输封装成一个异步接口,上层调用者只需要传入源地址、目的地址和长度,剩下的交给DMA引擎去处理。这样代码结构清晰,也方便后续替换成其他传输方式。
注意:DMA传输的内存需要保证物理地址连续。如果使用vmalloc分配的内存,需要先转换成物理地址连续的缓冲区。我在回测系统里用的是dma_alloc_coherent来分配DMA缓冲区,省去了很多麻烦。
4.4 知识体系结构图
下面这张图展示了DMA在Linux系统中的整体架构,从硬件到应用层的层次关系:
从这张图可以看得很清楚:应用层通过系统调用与dmaengine框架交互,框架向下调用具体的DMA引擎驱动,驱动再操作DMA控制器硬件。每一层都有明确的职责划分。
实战建议:在回测系统里,我通常会在应用层和dmaengine之间加一层抽象,叫做「数据传输管理器」。这样上层代码只需要调用transfer(start, dest, len)这样的接口,底层用DMA还是用CPU拷贝,对上层完全透明。方便调试,也方便后期优化。
嗯,DMA这部分内容就讲到这里。理解DMA控制器的工作原理、掌握dmaengine框架的使用,对于做高性能回测系统来说,是基本功。尤其是当你的回测数据量达到TB级别时,DMA带来的性能收益会非常明显。
公众号:蓝海数据掘金营,微信deep3321