1. DMA搬运数据的完整过程:谁发起、谁执行、谁善后
我在Linux下被DMA真正教育,是在调一块嵌入式网卡驱动的时候。开机日志里一行failed to reset the dma,让我对着数据手册翻了一下午。后来又在串口、ADC、SD/MMC、视频采集这几类设备上跟DMA打过不少交道,发现很多人对DMA的理解还停留在“外设和内存之间直接搬运数据”这个层面,真到写驱动时,连dma_map_single和dma_alloc_coherent的区别都说不上来。这篇我只讲Linux下的DMA机制,从搬运过程、地址映射、cache一致性,到dmaengine框架和真实排查案例,尽量把一条能直接落地的路径给你讲清楚。
1.1 从“一整批数据搬运”理解DMA的存在价值
先回答一个最基本的问题:DMA到底解决了什么?
你可以把没有DMA时的数据收发想象成叫外卖。CPU是那个亲自下楼取餐的人,每来一份餐,CPU就要从工位上站起来、下楼、拿餐、上楼、放到桌上。如果外卖一分钟来一百份,CPU什么正事都不用干了,光是上下楼就累死。传统中断驱动的UART、SPI、GPIO模拟时序,本质就是这样,每个字节到达或发送完成都会触发中断,CPU在中断里读一个寄存器、写一个寄存器。数据量小的时候无所谓,一旦波特率跑高、采样率拉满、网络包疯狂进来,CPU占用率会直接被打满。
DMA把这个模型改成了快递柜。CPU只需要告诉DMA控制器:货从哪个地址来、放到哪个地址去、一共多少件。然后DMA控制器自己走总线把数据搬完,搬完了再发一个中断通知CPU“货已入库”。整个过程CPU只需要参与头尾两件事,中间的数据搬运完全不占用CPU指令周期。
在Linux驱动的视角里,一次典型DMA传输是这样的:
- 驱动分配一块内存,把虚拟地址通过DMA API转换成设备能识别的DMA地址。
- 驱动把源地址、目的地址、传输长度、突发大小、传输方向这些参数配置到DMA控制器。
- DMA控制器作为总线主设备开始搬运,搬运过程不需要CPU干预。
- 传输完成,DMA控制器触发中断,驱动在中断处理中调用回调函数消费数据。
这里有三个词很关键:总线主设备、源地址和目的地址、传输方向。DMA控制器不是从设备,它是自己去读写总线的,所以它能搬内存到外设FIFO,也能搬外设FIFO到内存,还能搬内存到内存。理解了这个模型,后面很多API设计的理由就顺了。
1.2 DMA的“一次传输”到底有多小,burst和FIFO怎么配
另一个容易让新手懵的是DMA控制器里那一堆传输参数。比如burst size、FIFO深度、数据宽度。很多人的做法是照抄参考驱动,能跑就再也不动,直到某天发现高负载下丢数据,才回头研究这些参数。
我常用一个生活化的解释方式:DMA搬运数据,像搬家公司的货车一趟一趟运箱子。货车一次装多少箱(burst),决定跑多少趟。一辆车只装一箱,虽然灵活但是效率极低;一辆车能装64箱,但需要仓库门口有足够大的暂存区(FIFO)来配合装卸。
实际配置时,burst size通常取总线位宽的一个合理倍数。比如32位总线上,8拍的burst就是32字节一次。如果外设FIFO深度不够,burst就要调小,否则FIFO会溢出。很多DMA控制器配置寄存器里都有burst length或transfer width字段,调试时可以先从最小burst开始,确认数据正确后再逐步调大观察性能,这也是验证链路稳定性的一个好方法。
2. 地址翻译与dma_mask:为什么驱动不能直接把内核地址交给DMA
我见过不少刚接触驱动的同事,试图把kmalloc返回的指针直接写进DMA描述符。这在某些x86裸机环境下能跑,在Linux上则是完全错误的做法。因为DMA控制器看到的地址空间,和CPU看到的内核虚拟地址空间根本不是一回事。
2.1 四种地址,一次说清
Linux内核里至少有四种地址概念容易混淆:
| 地址类型 | 说明 | 能否直接交给DMA |
|---|---|---|
| 内核虚拟地址 | CPU通过页表访问的地址,如kmalloc返回值 | 不能 |
| 物理地址 | CPU的物理地址空间,可能需要经过MMU转换 | 不一定能,DMA控制器不一定按物理地址寻址 |
| 总线地址/DMA地址 | 设备在总线上看到的地址,即dma_addr_t | 这就是DMA控制器能用的地址 |
| IOMMU映射地址 | 有SMMU/IOMMU时,设备看到的是IOVA,不是物理地址 | 是,但由IOMMU翻译 |
在没有IOMMU的平台上,总线地址通常等于物理地址,这也是很多人误以为“物理地址能直接给DMA”的来源。但Linux API的语义要求你必须用DMA API做转换,因为一旦硬件平台引入了IOMMU,dma_addr_t就是一个IOVA,和你想象的物理地址完全无关。驱动应该永远把dma_addr_t当作一个不透明的整数,不要自己去猜它的含义。
2.2 dma_mask到底在表达什么
dma_mask是设备结构体里的一个位掩码,表示这个设备的DMA控制器最多能寻址多少位地址。如果设备只有32位DMA地址能力,但驱动把64位物理地址直接给它,高地址部分会被截断或者丢失,轻则数据错乱,重则直接触发总线错误。
用dma_set_mask_and_coherent()设置这个掩码,通常这样写:
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) { dev_err(dev, "无法设置DMA掩码\n"); return -EIO; }有的驱动只调dma_set_mask(),没有调coherent版本,这会导致一致性映射使用的掩码没有设置,某些平台上会分配出设备无法访问的高地址。我建议一律使用dma_set_mask_and_coherent(),省得代码评审时被追问。
2.3 物理地址在DMA场景下的两个陷阱
第一个陷阱是内存类型。DMA使用的内存需要保证物理连续或者支持SG表离散传输。kmalloc分配的内存通常物理连续,但只能分配较小的块;大块连续内存要么用devm_kmalloc碰运气,要么用CMA。第二个陷阱是内存的属性。有些平台区域被标记为device memory或strongly-ordered,CPU读写行为和普通DMA缓冲不一致,容易出现性能异常。
所以Linux专门提供了一组DMA地址映射API,它负责的不仅是地址转换,还包括cache一致性和内存屏障,这才是重点。
3. cache一致性与两类映射:从“数据不对”反推API设计
“为什么我DMA收到的数据是老数据?”这个问题在Linux驱动讨论里出现的频率,高到可以单独开一个板块。大多数时候答案只有一个:cache一致性没处理好。
3.1 CPU cache和DMA之间的“数据倒挂”问题
CPU cache是CPU和内存之间的高速缓存。当CPU写内存时,数据可能还在cache里没真正落到RAM;当CPU读内存时,也可能直接把cache里的旧数据给了你,根本没去内存拿。
DMA绕过CPU cache直接读写RAM。于是经典问题来了:
- 设备往内存写了新数据,CPU读时发现cache里还是旧副本,于是读到旧数据。
- CPU刚往内存写了数据,设备读时发现数据还没从cache写回RAM,于是设备读到旧数据。
这两个问题并称为cache一致性问题。解决办法只有两种思路:一是让这段内存不走cache,每次访问都直接穿透到RAM;二是在DMA传输的关键节点手动做cache清理和失效操作。
3.2 一致性映射为什么“贵”但省心
dma_alloc_coherent()走的是第一种思路。它在分配内存时,会保证这段内存区域对CPU和DMA控制器是一致的,CPU访问它不会被cache干扰。
dma_addr_t dma_handle; void *cpu_addr; cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL); if (!cpu_addr) return -ENOMEM;这个API返回两个东西:cpu_addr是CPU访问这段内存的虚拟地址,dma_handle是DMA控制器访问这段内存用的总线地址。传输过程中不需要你手动同步cache,因为这段内存本身就用uncached或者write-through方式映射了。
代价是什么?分配和释放的开销比较大,而且有些平台上这类内存是从专用内存池里割出来的,不能无限分配。它适合CPU和设备都要频繁访问的共享结构,比如描述符环、DMA控制块、状态标记位这种“小但关键”的区域。拿它当大数据搬运缓冲,不划算。
3.3 流式映射为什么必须按方向来
dma_map_single()和dma_unmap_single()走的是第二种思路。它映射的是已经存在的一段内存,比如kmalloc出来的缓冲区,在map和unmap的边界处做cache维护。
使用流式映射时,方向不是随便填的:
| 方向枚举 | 含义 | map阶段行为 | unmap阶段行为 |
|---|---|---|---|
DMA_TO_DEVICE | CPU写,设备读 | 做cache写回,保证数据落RAM | 一般不需要特殊处理 |
DMA_FROM_DEVICE | 设备写,CPU读 | 做cache失效,丢弃旧副本 | 做cache失效,让CPU读到新数据 |
DMA_BIDIRECTIONAL | 双向 | 写回加失效 | 写回加失效 |
DMA_NONE | 非法/调试 | 不执行传输 | 不执行传输 |
这里有个很多人没想明白的点:为什么DMA_FROM_DEVICE在map阶段就要做cache失效?因为设备随时可能在map完成后开始写内存,cache里如果保留一份旧数据,后面CPU读这段内存时,mmu一查cache命中了,拿到的是设备写入之前的老数据。所以必须在DMA开始前就把cache里的旧副本丢弃。
我自己的经验是:DMA_BIDIRECTIONAL能不用就不用。它每次map/unmap都会做完整的writeback加invalidate,性能比单向映射差不少,而且双向访问的缓冲区在并发时容易出现谁也没预料到的数据竞争。
3.4 权限模型:DMA映射期间的“内存借用”
理解流式映射一定要建立一个“所有权”概念。map之后,这段内存的访问权实际上交给了DMA控制器,CPU不应该再去读写它,直到unmap完成。这不是API强制要求的,但如果不遵守,就会遇到“CPU改了数据、DMA没看到”或者“DMA写了数据、CPU读的是脏数据”这类问题。
/* 发送方向:CPU写数据到buf,然后map给设备读 */ write_data_to_buf(buf); dma_addr = dma_map_single(dev, buf, len, DMA_TO_DEVICE); /* map之后,CPU不要再去动buf */ hw_start_transmit(dma_addr, len); ... /* 发送完成中断 */ dma_unmap_single(dev, dma_addr, len, DMA_TO_DEVICE); /* 现在CPU可以重新使用buf */这类API设计很像“借车”:借给设备之前你要把车加满油(写回数据),设备使用过程中你不能抢着开,还回来之后你才能再用。理解了这个模型,很多诡异的DMA问题就都能对号入座了。
4. 流式映射和SG表:写驱动时最常用的那组DMA函数
流式映射虽然概念简单,但工程里用起来有不少细节。尤其是异步传输场景,很多人会忽略map和unmap必须严格配对这一条,最后在调试时被DMA-API: device driver tries to free DMA memory it has not allocated这种错误教做人。
4.1 一次发送的完整映射生命周期
一个典型的网络包发送路径,映射大致是:
struct sk_buff *skb = ...; dma_addr_t dma_addr; dma_addr = dma_map_single(dev, skb->data, skb->len, DMA_TO_DEVICE); if (dma_mapping_error(dev, dma_addr)) { dev_err(dev, "DMA映射失败\n"); dev_kfree_skb(skb); return NETDEV_TX_OK; } /* 把dma_addr填进发送描述符,触发硬件发送 */ tx_desc->addr = dma_addr; tx_desc->len = skb->len; wmb(); /* 确保描述符先写入内存再通知硬件 */ writel(1, dev->base + TX_START_REG);注意检查dma_mapping_error(),这在IOMMU场景下尤其重要,因为IOVA空间耗尽时它会返回一个错误地址,不检查会出大问题。
发送完成中断里,必须找到对应的skb并unmap:
dma_unmap_single(dev, tx_desc->dma_addr, tx_desc->len, DMA_TO_DEVICE); dev_kfree_skb(skb);这里有个工程细节:发送完成中断可能比下一次发送晚到很多,所以tx_desc结构里最好保存dma_addr、len、skb指针,不能靠临时变量去猜。
4.2 大块内存连续性问题与scatter-gather
连续物理内存永远是稀缺资源。系统跑久了,内存碎片化严重,kmalloc一个几MB的缓冲区都可能失败。这时候就轮到scatter-gather(SG)出场。
SG表的核心思想是:不要求一整块连续内存,而是把分散的几个内存块,通过描述符串成一个链表交给DMA控制器,让它依次把每一段搬完。
struct scatterlist sg[2]; sg_init_table(sg, 2); sg_set_buf(&sg[0], buf1, len1); sg_set_buf(&sg[1], buf2, len2); nents = dma_map_sg(dev, sg, 2, DMA_FROM_DEVICE); if (!nents) return -ENOMEM; for_each_sg(sg, s, nents, i) { dma_addr_t addr = sg_dma_address(s); unsigned len = sg_dma_len(s); /* 配置硬件描述符 */ }这里最容易犯的错误是用s->address和s->length去填描述符,而不是用sg_dma_address()和sg_dma_len()。在有IOMMU的情况下,dma_map_sg()可能会把多个物理段合并成一个IOVA段,返回的nents比传入的2更小,只有用sg_dma_*宏才能拿到真正给设备看的地址。
4.3 dma_pool和预分配描述符
对高频小对象传输,比如网络驱动里的描述符本身,每次动态分配再map的开销不可忽略。dma_pool_create()和dma_pool_alloc()可以预先创建一块DMA内存池,每次分配和释放的开销比通用kmalloc更可预期,而且能保证对齐,适合描述符环、DMA请求结构这类固定大小对象。
struct dma_pool *pool = dma_pool_create("rx_desc", dev, sizeof(struct desc), 16, 0); struct desc *d = dma_pool_alloc(pool, GFP_KERNEL, &dma_addr);我一般只在两种情况下用dma_pool:一是对象固定大小且频率极高;二是硬件对地址对齐有严格要求。如果只是单次大数据传输,直接用流式映射或者一致性映射就够了。
5. dmaengine框架:用标准API驱动DMA控制器的完整链路
前面讲的都是底层dma_map接口,属于“内存映射”层。实际写驱动时,如果硬件自带DMA控制器,更常见的是用Linux的dmaengine框架。这个框架把各种DMA控制器的差异封装成一组统一API,驱动不用关心你的DMA控制器是ARM PL330、DMA-330还是厂商私有IP。
5.1 dmaengine的几个核心对象
struct dma_device:一个DMA控制器,里面有device_prep_slave_sg、device_prep_dma_cyclic等回调。struct dma_chan:一个DMA通道,对应控制器上的一条物理DMA流。struct dma_async_tx_descriptor:一次传输描述符,类似一个“任务单”,里面保存了传输参数、回调函数和cookie。
驱动开发者一般不直接调用硬件寄存器,而是通过dma_request_chan()申请通道,然后用dmaengine_prep_xxx()系列函数创建描述符,再dmaengine_submit()提交,最后dma_async_issue_pending()让硬件动起来。
5.2 从申请通道到传输完成
一个从外设FIFO读取数据的例子:
struct dma_chan *chan = dma_request_chan(dev, "rx"); if (IS_ERR(chan)) return PTR_ERR(chan); struct dma_slave_config cfg = { .direction = DMA_DEV_TO_MEM, .src_addr = (phys_addr_t)dev->base + FIFO_DATA, .src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES, .src_maxburst = 16, }; dmaengine_slave_config(chan, &cfg); struct dma_async_tx_descriptor *desc; desc = dmaengine_prep_slave_single(chan, buf_dma, len, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT); if (!desc) { dma_release_channel(chan); return -ENOMEM; } desc->callback = my_dma_callback; desc->callback_param = priv; cookie = dmaengine_submit(desc); dma_async_issue_pending(chan);传输完成时,my_dma_callback会在DMA中断上下文里被调用。这个回调里不要做任何耗时操作,也不要调用msleep、mutex_lock这类可能睡眠的函数。如果收完数据要做协议解析,最好用tasklet或workqueue把数据丢到进程上下文处理。
5.3 循环模式cyclic:串口和音频的长期伴侣
普通的一次性DMA传输,描述符执行完就结束了。但串口、音频这类持续数据流,需要DMA一直循环搬运,这时候要使用dmaengine_prep_dma_cyclic():
desc = dmaengine_prep_dma_cyclic(chan, buf_dma, buf_size, period_size, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);它会把一块缓冲区切分成若干个period,每填满一个period就触发一次回调。驱动在回调里通过dmaengine_tx_status()查询当前传输位置,计算还有多少新数据可读,用环形缓冲区维护读写指针。
这里最常见的一个坑:cyclic模式启动后不会自己停,必须显式调用dmaengine_terminate_sync()。而且不要在回调里直接调用terminate,因为你正在使用这个通道,强行停止可能导致控制器的状态机紊乱。需要停止时,置一个标志位,让线程上下文去操作。
6. 串口、ADC、网卡:三种典型DMA场景的驱动侧实现
原理讲太多容易晕,我拿三个具体场景把前面内容串起来。
6.1 串口DMA:高波特率下CPU不再成为瓶颈
串口是很多人接触DMA的起点。115200波特率、8N1格式,每秒数据量是11520字节,CPU逐个字符中断还能扛。一旦跑到1.5Mbps甚至更高,每个字符都中断的话,CPU几乎被锁死在中断里。
标准做法是给串口驱动配置cyclic DMA环形缓冲。驱动初始化时分配一块环形DMA缓冲区,注册period回调。每当一个period的数据到达,回调里就把这段数据交给tty层。代码逻辑不复杂,但对timing要求高:如果回调处理太慢,DMA已经在覆盖老数据,就会丢数据。所以回调里绝不能再做加锁、分配内存这类耗时操作,只做数据搬运和软中断调度。
6.2 ADC多通道:扫描顺序决定数据布局
我用ADC采集多路模拟量时,第一次拿到的DMA数据怎么都对不上通道。后来看数据手册才发现问题:DMA缓冲区里的数据顺序,不是按ADC通道编号排的,而是按扫描顺序排的。
最简单的理解是:ADC配置成规则组扫描,从通道0扫到通道3,DMA buffer里的顺序就是[ch0, ch1, ch2, ch3, ch0, ch1, ...]。如果你在软件里按[ch3, ch2, ch1, ch0]去解析,结果当然是乱的。
另外还要注意ADC数据寄存器宽度和DMA搬运宽度必须匹配。很多MCU的ADC数据寄存器是16位,DMA配置成32位搬运时,一次会把两个采样数据打包搬运,软件如果不按对应宽度去取,数据就串位了。我的排查习惯是:先配置单通道,读回一组已知值确认基础通路;再扩展到多通道,逐组对比扫描顺序和DMA buffer内的排列。
6.3 网卡收包:page pool驱动的高频DMA映射
网卡是Linux里DMA最频繁的设备之一,每个包都可能涉及一次DMA映射。为了减少dma_map_single()的开销,现代驱动普遍使用page pool和NAPI配合:从page pool拿一整页内存,分割成多个packet buffer,map一次后就重复使用,只有skb被协议栈收走或释放时才unmap。
实际调驱动时,很多人忽略一个点:page pool分配出来的页,可能来自不同内存区,DMA地址范围不一定是设备支持的。网卡初始化时要正确设置dma_set_mask_and_coherent(),否则某些平台会随机出现RX DMA错误。这个问题在开启IOMMU的平台上尤其隐蔽,因为IOVA空间往往比物理内存范围大,掩盖了真实DMA位宽限制。
7. 从“failed to reset the dma”到“ADC数据错乱”:几个真实排查案例
理论说得再多,最终还是要落到“日志报错了怎么办”。这一节我按真实排查链路写几个常见问题,重点是排查思路,不是直接甩答案。
7.1 网卡报failed to reset the dma,到底先查什么
我第一次看到failed to reset the dma时,第一反应是DMA API用错了。查了半天代码发现,这行日志来自MAC驱动内部的软件复位流程,和驱动对DMA的映射完全无关。它的本质是:CPU写了一个复位寄存器,但轮询等待硬件复位完成时超时了。
碰到这类日志,按顺序查三件事:
- 时钟和电源域:DMA/MAC控制器的时钟有没有打开,电源域是否正常。寄存器访问全返回0xFF或全0,大概率是时钟没使能。
- 复位信号冲突:复位控制器里有没有别的设备正占用同一个复位域,导致MAC复位一直完成不了。
- PHY的时钟和接口时序:以太网MAC做DMA复位时,有时候依赖PHY提供的参考时钟已经稳定。RGMII情况下,PHY时钟没起来,MAC状态机无法完成初始化,就会报DMA复位超时。
如果是RK3588这类多核SoC,还要对比主线内核和厂商BSP里dts的差异。很多时候就是dts里少了assigned-clock-rates或resets属性,导致驱动probe时硬件还没准备好。
7.2 DMA continuous requests分配失败:不是所有连续内存都叫CMA
“DMA continuous requests”失败,本质是连续物理内存分配不出来。常见日志有failed to allocate coherent memory、CMA: failed to allocate等。
排查关键不是去看驱动代码,而是先确认系统里CMA区域到底有多大:
cat /proc/meminfo | grep Cma如果CmaTotal本身就只有16MB,而驱动申请8MB连续buffer,再叠加系统的正常DMA使用,很容易失败。解决方案有三种:
- 在cmdline里调大
cma=64M这样的保留区大小; - 把驱动改用SG表,让硬件支持离散缓冲;
- 使用
dma_pool按需分配,而不是一次性申请超大块。
我之前还遇过一种情况:dmesg里看不到明显CMA失败,但驱动申请就是返回NULL。原因是驱动里用了GFP_ATOMIC在中断上下文直接调dma_alloc_coherent,而该上下文不允许睡眠等待内存回收。这类问题通过增加预分配缓冲或者把分配时机提前到probe阶段解决。
7.3 ADC多通道DMA数据乱:一次和cache无关的错位
有个项目上ADC用了四个通道DMA采集,数据时而对、时而错,看起来像极了cache一致性bug。我把dma_alloc_coherent换成了普通内存加dma_map_single并手动做invalidate,问题依旧。
后来打印DMA buffer的原始字节,才发现数据确实是整齐的,只是每个通道的值整体错位了一个索引。原因出在ADC初始化时配置了“通道扫描顺序为2,1,0,3”,而DMA解析代码按“0,1,2,3”来取。这种错位问题肉眼很难发现,因为你看到的每个通道值都来自某一个真实通道,只是映射关系不对。
排查ADC类DMA问题,我总结了一个固定流程:
- 单通道模式采集一个已知电压,确认基础通路正确。
- 多通道模式下打印原始DMA buffer前N个样本,不要做任何软件重排。
- 对照数据手册里的扫描顺序,确认buffer排列和预期一致。
- 最后才怀疑cache和内存对齐问题。
按这个顺序,大概率能在十分钟内定位问题。
7.4 排查DMA问题的三板斧
第一斧是开CONFIG_DMA_API_DEBUG。它能在map/unmap不配对、invalid地址释放、错误大小释放时输出stack trace,是DMA驱动问题最直接的“裁判”。代价是性能明显下降,我只在问题复现时才开。
第二斧是看/proc/interrupts里DMA中断次数。如果中断频率远超预期,说明burst配置太小或者环形缓冲区深度不够,硬件频繁打断CPU,DMA的优势被浪费了。
第三斧是ftrace的dmaengine事件。内核有/sys/kernel/debug/tracing/events/dmaengine,可以看到描述符提交和完成的时序,诊断回调跟不跟得上硬件传输速度。
8. 调试DMA的常用手段与经验清单
写DMA驱动这么多年,我越来越觉得,DMA问题最难的不是代码逻辑,而是“地址、内存、cache、中断”这几个概念交织在一起时,现象往往指向错误的方向。所以最后分享几个我日常调DMA时积累的工具使用方法,算不上高大上,但确实管用。
内核编译时打开CONFIG_DMA_API_DEBUG后,再配合dma_debug模块参数,可以控制调试输出层级。如果怀疑映射配对问题,观察dmesg里是否有类似DMA-API: device driver tries to free DMA memory it has not allocated的输出。它明确指出“你有一次unmap提前或者重复了,并且把出错的调用栈打出来”,比人肉翻代码快得多。
/sys/kernel/debug/dma-api/目录下还有映射记录的统计信息,包括当前映射条数、错误计数。这个目录在release版内核里常常不存在,所以复现问题时要确保内核打开了相关配置。
针对中断频率过高,我会用一段简单的shell抓取:
for i in $(seq 1 5); do grep eth0 /proc/interrupts sleep 1 done看每秒新增次数是否和设备吞吐量匹配。如果发送10MB数据却产生了几万次中断,那么burst或者描述符深度一定需要调大。
我再整理一些零散但很重要的经验:
dma_map_single返回的地址,不能长期保存复用。每次map/unmap必须严格配对。如果硬件支持doorbell机制,每次提交后要smp_wmb()确保描述符写入对DMA控制器可见。- 回调函数在DMA中断上下文执行,绝对不能睡眠。如果回调里要锁,要用
spin_lock,而且要确认锁是否可替代成lockless队列。数据量大时优先用NAPI那种poll模式,把处理延后到软中断。 - 一致性映射地址
dma_alloc_coherent返回的dma_addr_t不等于物理地址,“等于”只是一种巧合。不要拿它去做ioremap或计算物理页号。 - 用户空间程序不能直接触碰
dma_alloc_coherent返回的内存,它是内核虚拟地址。如果确实需要用户态读写DMA缓冲,要么用mmap方式重新映射,要么通过dma_buf框架导出,但缓存属性必须配置正确,否则在带cache的平台上用户态读到的数据仍然可能是脏的。
最后说一个我常跟团队讲的实践:DMA驱动调试不要一口气把所有优化全加上。先保证功能正确,验证数据一致,再调burst、多描述符、环形缓冲和page pool这些性能项。否则一旦出错,你根本分不清是映射错了、cache没同步、还是burst配得太大导致的硬件状态异常。