4、DMA与Ring Buffer:DMA引擎初始化、Ring Buffer管理、描述符链与数据收发流程
DMA 和 Ring Buffer,说白了就是网络驱动里最核心的数据搬运机制。没有 DMA,CPU 得亲自去搬每个字节,那性能就彻底完蛋了。我刚开始接触网络驱动时,总觉得 DMA 是个黑盒子,后来踩了几个坑才真正搞明白。
4.1 DMA引擎初始化
DMA 引擎的初始化,通常是在网络设备驱动的 probe() 函数里完成的。我个人习惯把它分成三步:
- 分配 DMA 缓冲区:用
dma_alloc_coherent()或dma_alloc_noncoherent()分配内存。 - 设置 DMA 掩码:告诉硬件它能访问的地址范围。
- 注册中断处理:DMA 传输完成后,硬件会触发中断。
核心代码片段:DMA 初始化
/* 分配 DMA 描述符内存 */
priv->desc_base = dma_alloc_coherent(dev,
sizeof(struct my_desc) * NUM_DESC,
&priv->desc_dma, GFP_KERNEL);
if (!priv->desc_base)
return -ENOMEM;
/* 设置 DMA 掩码,支持 64 位地址 */
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
dev_err(dev, "DMA mask 64-bit failed, try 32-bit\n");
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
if (ret)
goto err_free_desc;
}
/* 注册 DMA 完成中断 */
ret = request_irq(priv->irq, my_dma_irq_handler, 0,
"my_net_dma", priv);
避坑指南:我曾经在一个项目里忘了检查 dma_set_mask_and_coherent() 的返回值,结果硬件在 64 位模式下访问了 32 位地址,导致数据错乱。嗯,从那以后我再也不敢忽略这个检查了。
4.2 Ring Buffer 管理
Ring Buffer,也叫环形缓冲区,是 DMA 描述符的存放场所。它本质上就是一个固定大小的数组,配合两个指针(生产者指针和消费者指针)来管理。
你想想看,为什么用环形而不是链表?因为环形缓冲区在硬件层面更容易实现,而且内存是连续的,DMA 引擎可以直接通过地址偏移来访问。
4.2.1 发送 Ring Buffer
发送方向,CPU 是生产者,DMA 引擎是消费者。CPU 把待发送的数据描述符写入 Ring Buffer,DMA 引擎逐个取出并发送。
| 字段 | 说明 | 我常用的值 |
|---|---|---|
tx_ring_size |
发送描述符数量 | 256 或 512 |
tx_cur |
当前可用的描述符索引 | 初始为 0 |
tx_dirty |
已完成的描述符索引 | 初始为 0 |
注意:Ring Buffer 的大小必须是 2 的幂次方,这样可以用位运算代替取模运算。我见过有人用 idx % size,性能差了一大截。
4.2.2 接收 Ring Buffer
接收方向正好反过来。DMA 引擎是生产者,它把收到的数据写入缓冲区,然后更新描述符状态。CPU 是消费者,它轮询或通过中断知道有新数据到了。
/* 初始化接收 Ring Buffer */
static int rx_ring_init(struct net_device *ndev)
{
struct my_priv *priv = netdev_priv(ndev);
int i;
for (i = 0; i < RX_RING_SIZE; i++) {
struct my_desc *desc = &priv->rx_ring[i];
desc->buf_addr = dma_map_single(ndev->dev.parent,
priv->rx_buf[i],
RX_BUF_SIZE, DMA_FROM_DEVICE);
desc->len = RX_BUF_SIZE;
desc->flags = DESC_OWN_DMA; /* 所有权交给 DMA */
}
priv->rx_cur = 0;
priv->rx_dirty = 0;
return 0;
}
4.3 描述符链
描述符链,就是把一个个描述符串起来。每个描述符里有一个 next 指针,指向下一个描述符。最后一个描述符的 next 指向第一个,形成闭环。
我记得有一次调试一个千兆网卡驱动,发现数据总是丢包。查了半天,原来是描述符链的最后一个节点没有正确回环到第一个,DMA 引擎走到头就停了。
描述符结构体示例
struct my_desc {
__le32 buf_addr_low; /* 缓冲区地址低32位 */
__le32 buf_addr_high; /* 缓冲区地址高32位 */
__le16 len; /* 数据长度 */
__le16 flags; /* 状态标志 */
__le32 next_desc; /* 下一个描述符的 DMA 地址 */
};
/* 初始化描述符链 */
for (i = 0; i < NUM_DESC; i++) {
desc[i].next_desc = priv->desc_dma +
((i + 1) % NUM_DESC) * sizeof(struct my_desc);
}
4.4 数据收发流程
整个数据收发流程,我习惯用一张图来说明。你看下面这个 SVG,它把发送和接收的路径都画清楚了。
4.4.1 发送流程详解
- CPU 准备数据:把要发送的数据放到 DMA 缓冲区里。
- 构造描述符:填写缓冲区地址、长度,并把 OWN 位设为 DMA 所有。
- 触发发送:写发送控制寄存器,告诉 DMA 引擎可以开始了。
- DMA 搬运:DMA 引擎从 Ring Buffer 里取出描述符,把数据从内存搬到 MAC 层。
- 完成中断:发送完成后,DMA 引擎更新描述符状态,触发中断。
- CPU 回收:在中断处理里,CPU 回收已完成的描述符和缓冲区。
个人经验:发送流程里最容易出问题的是第 6 步。我曾经在中断处理里直接调用 dev_kfree_skb(),结果导致死锁。后来改用 NAPI 机制,把回收操作放到软中断里做,问题就解决了。
4.4.2 接收流程详解
- 预分配缓冲区:在初始化时,CPU 把空的缓冲区交给 DMA。
- DMA 写入:数据到达时,DMA 引擎直接把数据写入预分配的缓冲区。
- 更新描述符:DMA 引擎把描述符的 OWN 位还给 CPU,并记录数据长度。
- 触发中断:DMA 引擎通知 CPU 有新数据到了。
- CPU 处理:CPU 从 Ring Buffer 里取出描述符,把数据交给协议栈。
- 重新填充:CPU 把新的空缓冲区挂回 Ring Buffer,等待下一次接收。
注意:接收流程里有一个常见的坑——缓冲区泄漏。如果 CPU 处理速度跟不上 DMA 写入速度,Ring Buffer 会被填满,新数据就会丢失。我一般会在驱动里加一个统计计数器,监控接收丢包的情况。
4.5 性能优化要点
| 优化点 | 做法 | 效果 |
|---|---|---|
| 描述符预取 | 使用 prefetch() 提前加载描述符 |
减少 Cache Miss |
| 批量处理 | 一次中断处理多个描述符 | 降低中断开销 |
| NAPI 轮询 | 用轮询代替中断 | 高负载下性能提升明显 |
| DMA 对齐 | 缓冲区地址按 Cache Line 对齐 | 避免伪共享 |
嗯,这里要注意一点:优化不是越多越好。我见过有人把 Ring Buffer 设成 4096 个描述符,结果内存占用太大,反而导致性能下降。一般来说,256 到 512 个描述符是比较合理的范围。
DMA 和 Ring Buffer 这块,说白了就是「谁拥有描述符,谁就能操作数据」。CPU 和 DMA 引擎通过 OWN 位来交接控制权,只要这个机制不出问题,数据收发就能顺畅运行。