网络数据包的收发,说白了就是一场与时间的赛跑。CPU如果亲自去搬每个数据包,那基本就不用干别的了。DMA在这里扮演的角色,就是那个不知疲倦的搬运工。
我在MTK平台上调试GMAC时,遇到过一个问题:千兆以太网跑不满带宽。查来查去,发现是DMA描述符环配置不合理。嗯,今天我们就来聊聊这个。
23.1 MTK Ethernet GMAC的DMA架构
MTK的GMAC(Gigabit MAC)内部集成了专用的DMA引擎。它负责把网络数据包从FIFO搬到系统内存,或者反过来。
核心结构就两个:
- 发送描述符环(Tx Descriptor Ring):CPU准备好数据,DMA负责发送
- 接收描述符环(Rx Descriptor Ring):DMA接收数据,CPU负责处理
每个描述符里包含了:
- 数据缓冲区的物理地址
- 数据长度
- 状态标志位(是否完成、是否有错误)
我个人习惯把描述符环设计成256个条目。太少容易丢包,太多浪费内存。你想想看,256个刚好能扛住突发流量。
关键点:描述符环必须放在非Cacheable的内存区域。否则CPU和DMA看到的数据不一致,你会疯掉的。
23.2 WiFi模块的DMA传输机制
MTK的WiFi芯片(比如MT7668)内部也有DMA。但和GMAC不同,WiFi的DMA要处理更多的协议头。
WiFi数据包的DMA流程大致是这样:
- WiFi固件把收到的数据写到内部SRAM
- DMA把数据从SRAM搬到主机内存
- 驱动解析802.11头,转换成802.3格式
- 交给网络协议栈
我曾经踩过一个坑:WiFi的DMA描述符里有个「数据偏移」字段。默认值是0,但WiFi数据包前面有24字节的802.11头。如果不设置偏移,DMA会把头和数据一起搬,导致协议栈解析失败。
注意:WiFi DMA的缓冲区大小要预留足够的头部空间。我一般设置2048字节,够用。
23.3 NAPI与DMA的配合机制
NAPI(New API)是Linux网络子系统的中断缓解机制。它的核心思想是:
- 有数据来时,先触发中断
- 中断处理函数关闭中断,切换到轮询模式
- 轮询完所有数据后,再开启中断
为什么要配合DMA?因为DMA已经把数据搬好了。NAPI只需要去描述符环里取就行了。
在MTK平台上,典型的NAPI轮询函数长这样:
static int mtk_napi_poll(struct napi_struct *napi, int budget) { struct mtk_eth *eth = container_of(napi, struct mtk_eth, napi); int rx_done = 0; // 遍历接收描述符环 while (rx_done < budget) { struct mtk_rx_desc *desc = ð->rx_ring[rx_done]; // 检查DMA是否完成 if (!(desc->status & MTK_RX_DMA_DONE)) break; // 处理数据包 skb = build_skb(desc->data, desc->len); napi_gro_receive(napi, skb); // 重新填充描述符 desc->status = 0; rx_done++; } // 如果没处理完,继续轮询 if (rx_done == budget) return budget; // 处理完了,重新开启中断 napi_complete_done(napi, rx_done); mtk_irq_enable(eth, MTK_RX_DONE_INT); return rx_done; }这段代码里有个细节:每次处理完一个包,要立即重新填充描述符。否则DMA没地方放新数据,会丢包。
我的经验:budget值不要设太大。我一般设64,既能保证吞吐量,又不会让CPU饿死其他任务。
23.4 性能调优实战
在实际项目中,DMA和NAPI的配合有几个关键参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 描述符环大小 | 256 | 平衡内存和吞吐量 |
| NAPI budget | 64 | 单次轮询最大包数 |
| DMA burst size | 8 | 一次DMA传输的字节数 |
| 中断合并阈值 | 4 | 攒够4个包再触发中断 |
我曾经在MT7621平台上调试WiFi吞吐量。默认配置下,2.4G只能跑到80Mbps。后来我把DMA burst size从4改成8,NAPI budget从32改成64,直接飙到150Mbps。
为什么会这样?因为burst size大了,DMA每次搬运的数据更多,减少了总线占用。NAPI budget大了,一次轮询能处理更多包,减少了中断次数。
核心原则:让DMA多干活,让CPU少中断。NAPI就是那个「攒一波再处理」的机制。
23.5 避坑指南
最后分享几个我踩过的坑:
- Cache一致性:DMA缓冲区一定要用dma_alloc_coherent()分配,或者手动做cache flush。否则你会看到数据错乱。
- 描述符环溢出:如果CPU处理速度跟不上DMA,描述符环会满。这时候要丢包还是反压?我建议丢包,反压会导致DMA卡死。
- 中断亲和性:多核CPU上,把网络中断绑定到同一个核。否则cache来回迁移,性能下降30%。
嗯,DMA和网络子系统的配合,说白了就是「让硬件干硬件的活,让软件干软件的活」。NAPI就是那个协调者,告诉DMA「你先搬,搬完了叫我」。搞懂了这一点,MTK平台的网络驱动就没什么秘密了。