4、DMA与Ring Buffer:DMA引擎初始化、Ring Buffer管理、描述符链与数据收发流程

DMA 和 Ring Buffer,说白了就是网络驱动里最核心的数据搬运机制。没有 DMA,CPU 得亲自去搬每个字节,那性能就彻底完蛋了。我刚开始接触网络驱动时,总觉得 DMA 是个黑盒子,后来踩了几个坑才真正搞明白。

4.1 DMA引擎初始化

DMA 引擎的初始化,通常是在网络设备驱动的 probe() 函数里完成的。我个人习惯把它分成三步:

  1. 分配 DMA 缓冲区:用 dma_alloc_coherent() 或 dma_alloc_noncoherent() 分配内存。
  2. 设置 DMA 掩码:告诉硬件它能访问的地址范围。
  3. 注册中断处理: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,它把发送和接收的路径都画清楚了。

DMA 数据收发流程 CPU 构造描述符 写入 Ring Buffer DMA 引擎读取 发送完成中断 DMA 接收数据 更新描述符状态 接收完成中断 CPU 处理数据 关键点:描述符的 OWN 位用于切换 CPU 和 DMA 的控制权,必须严格按顺序操作。

4.4.1 发送流程详解

  1. CPU 准备数据:把要发送的数据放到 DMA 缓冲区里。
  2. 构造描述符:填写缓冲区地址、长度,并把 OWN 位设为 DMA 所有。
  3. 触发发送:写发送控制寄存器,告诉 DMA 引擎可以开始了。
  4. DMA 搬运:DMA 引擎从 Ring Buffer 里取出描述符,把数据从内存搬到 MAC 层。
  5. 完成中断:发送完成后,DMA 引擎更新描述符状态,触发中断。
  6. CPU 回收:在中断处理里,CPU 回收已完成的描述符和缓冲区。

个人经验:发送流程里最容易出问题的是第 6 步。我曾经在中断处理里直接调用 dev_kfree_skb(),结果导致死锁。后来改用 NAPI 机制,把回收操作放到软中断里做,问题就解决了。

4.4.2 接收流程详解

  1. 预分配缓冲区:在初始化时,CPU 把空的缓冲区交给 DMA。
  2. DMA 写入:数据到达时,DMA 引擎直接把数据写入预分配的缓冲区。
  3. 更新描述符:DMA 引擎把描述符的 OWN 位还给 CPU,并记录数据长度。
  4. 触发中断:DMA 引擎通知 CPU 有新数据到了。
  5. CPU 处理:CPU 从 Ring Buffer 里取出描述符,把数据交给协议栈。
  6. 重新填充: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 位来交接控制权,只要这个机制不出问题,数据收发就能顺畅运行。