MAC层调优:DMA描述符环与数据包收发路径
各位好,欢迎来到第三章。今天咱们聊聊MAC层最核心的两个东西:DMA描述符环和数据包收发路径。
说实话,很多做U-Boot移植的工程师,往往把MAC驱动调通就跑,很少有人会去深挖DMA描述符环的细节。但我个人习惯是,只要板子网络性能不达标,第一件事就是打开描述符环的配置看一眼。为什么?因为这里藏着太多性能瓶颈了。
3.1 DMA描述符环:收发数据的“传送带”
先说说DMA描述符环是什么。说白了,它就是一块内存区域,里面放了一串描述符。每个描述符指向一个数据缓冲区,告诉DMA控制器:“数据从这里拿”或者“数据放这里”。
我见过不少新手,以为描述符环就是个简单的数组。其实不然。它更像一条环形传送带——数据包从网口进来,DMA自动把数据填到描述符指向的缓冲区,然后CPU去处理。处理完再把描述符还给DMA,循环使用。
核心要点:描述符环的大小、对齐方式、缓存一致性策略,直接决定了收发包的吞吐量和延迟。
3.1.1 描述符结构
不同MAC控制器(比如DesignWare、Freescale的FEC、TI的CPSW)的描述符格式略有差异,但核心字段大同小异。我以最常见的DesignWare GMAC为例:
struct dw_desc {
u32 des0; // 控制/状态字:数据长度、错误标志、OWN位
u32 des1; // 扩展状态/控制:校验和、时间戳
u32 des2; // 缓冲区地址(低32位)
u32 des3; // 下一个描述符地址 / 缓冲区地址(高32位)
};
这里最关键的字段是OWN位(在des0的最高位)。OWN位决定了描述符归谁管:
- OWN=1:描述符归DMA控制器所有,CPU不能碰
- OWN=0:描述符归CPU所有,DMA不会动它
嗯,这里要注意。很多驱动bug就出在这个OWN位的切换时机上。我曾经调试过一个丢包问题,查了两天才发现是CPU在DMA还没写完数据时就去读了缓冲区——说白了就是OWN位没等DMA清掉就抢了。
3.1.2 环的大小与对齐
描述符环的大小,我建议至少配置64个描述符。太小了容易丢包,太大了浪费内存。对于U-Boot这种轻量级环境,64~128个是比较合理的范围。
对齐要求呢?大多数MAC控制器要求描述符环4字节对齐,有些甚至要求缓存行对齐(32字节或64字节)。你想想看,如果描述符环跨了缓存行,DMA和CPU来回刷缓存,性能直接打对折。
我的经验:分配描述符环时,用cache_aligned_alloc()或者手动做对齐。别偷懒用malloc,万一不对齐,调试起来很痛苦。
3.2 数据包收发路径:从网口到协议栈
理解了描述符环,咱们再来看数据包怎么从网口一路跑到协议栈。我画了一张流程图,把整个路径串起来:
3.2.1 接收路径详解
接收路径上,最耗时的环节其实是CPU轮询描述符。U-Boot默认用轮询模式(polling),因为中断在U-Boot里处理起来太麻烦。但轮询有个问题:CPU一直在空转,浪费电不说,还拖慢了其他任务。
我建议的做法是:在收包函数里加一个超时机制。比如轮询1000次还没收到包,就主动退出,别死等。代码大概长这样:
int eth_rx_poll(struct eth_device *dev, int timeout_ms)
{
int retry = timeout_ms * 100; // 假设每次轮询10us
while (retry--) {
if (desc_has_data(rx_ring)) {
process_packet(rx_ring);
return 0;
}
udelay(10);
}
return -ETIMEDOUT;
}
注意:轮询间隔不能太短,否则CPU占用率飙升。10~20微秒是比较合理的值。我曾经见过有人写1微秒轮询,结果整个系统卡成幻灯片。
3.2.2 发送路径详解
发送路径相对简单,但有个坑:DMA发送完成通知。有些MAC控制器在DMA发送完成后,会清掉描述符的OWN位。但有些不会,需要CPU去轮询状态寄存器。
我个人习惯是,发送前先检查描述符环里有没有空闲的描述符。如果没有,就等一会儿再试。千万别直接往满的环里塞数据,会覆盖还没发完的包。
int eth_tx_packet(struct eth_device *dev, void *buf, int len)
{
struct dw_desc *desc = get_tx_desc(tx_index);
// 检查描述符是否空闲
if (desc->des0 & OWN_BIT) {
printf("TX ring full, dropping packet\n");
return -1;
}
// 填充描述符
desc->des2 = (u32)buf;
desc->des0 = (len & LEN_MASK) | OWN_BIT;
// 触发DMA发送
writel(POLL_DEMAND, &mac_regs->tx_poll);
return 0;
}
3.3 调优实战:描述符环参数优化
好了,理论讲完了,咱们来点实战的。下面这张表是我在实际项目中总结的描述符环调优参数:
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| 描述符数量 | 32 | 64~128 | 太少丢包,太多浪费内存 |
| 缓冲区大小 | 1518字节 | 2048字节 | 留点余量给VLAN标签 |
| 描述符对齐 | 4字节 | 64字节(缓存行) | 避免伪共享问题 |
| 轮询间隔 | 1微秒 | 10~20微秒 | 平衡CPU占用和延迟 |
| OWN位检查方式 | 轮询寄存器 | 直接读描述符内存 | 减少寄存器访问开销 |
避坑指南:我曾经在一个ARM Cortex-A7平台上调U-Boot网络,发现收包速度上不去。查了半天,原来是DMA描述符环分配在DDR里,而DMA和CPU的缓存一致性没处理好。解决方案很简单:在分配描述符环时,用dma_alloc_coherent()确保缓存一致。就这么一行代码,吞吐量翻了一倍。
3.4 缓存一致性问题
说到缓存一致性,这是个绕不开的话题。CPU有缓存(L1/L2),DMA直接读写内存。如果CPU修改了描述符,但数据还在缓存里没写回内存,DMA读到的就是旧数据。反过来,DMA写完了描述符,CPU从缓存读到的也可能是旧数据。
解决办法有两种:
- 使用一致性内存:dma_alloc_coherent()分配的内存,硬件保证缓存一致。简单但性能稍差。
- 手动刷缓存:在切换OWN位前后,调用flush_dcache_range()和invalidate_dcache_range()。性能好,但代码复杂。
我个人偏向第二种。为什么?因为U-Boot里网络流量不大,手动刷缓存的开销可以忽略,而且能更精细地控制一致性。但如果你用的是多核CPU,或者DMA和CPU频繁交互,那还是用一致性内存更稳妥。
核心原则:OWN位切换前,CPU必须确保描述符数据已经写回内存;OWN位切换后,CPU必须从内存重新读取描述符,不能使用缓存中的旧副本。
3.5 小结
这一章咱们把DMA描述符环和数据包收发路径拆开揉碎了讲了一遍。你想想看,其实MAC层的调优,本质上就是在做三件事:描述符环的配置、OWN位的正确切换、缓存一致性的维护。这三件事做好了,网络性能基本不会差。