4. 描述符生命周期管理:分配(alloc)、提交(submit)、完成(complete)、释放(free)全流程

DMA 描述符的生命周期是驱动高带宽传输的核心脉络。一个描述符从创建到销毁,依次经历 分配、提交、完成、释放 四个阶段。每个阶段都涉及特定的数据结构操作、硬件交互和内存屏障,任何环节的疏漏都可能导致数据错乱或系统崩溃。

核心原则:描述符的生命周期管理必须保证无锁或轻量锁设计,避免在数据路径上引入不必要的延迟。描述符的 ownership 在 CPU 与 DMA 引擎之间明确转移,是正确性的基石。
描述符生命周期四阶段流程图 描述符生命周期四阶段 ① 分配 dma_pool_alloc ② 提交 wmb() + 写寄存器 ③ 完成 中断 / 轮询 ④ 释放 dma_pool_free 关键转移点: • 分配 → 提交:CPU 拥有描述符,填充缓冲区地址、长度、标志。 • 提交 → 完成:ownership 转移给 DMA 引擎,CPU 只读状态。 • 完成 → 释放:ownership 归还 CPU,驱动回收描述符或归还内存池。

4.1 分配 (Alloc)

描述符的分配通常在驱动初始化阶段或运行时从 DMA 内存池 中获取。分配的核心目标是获取一块物理连续、对齐的内存,用于存放描述符结构体。

  • 内存来源:使用 dma_pool_alloc() 或 dma_alloc_coherent(),保证描述符所在内存不会被 CPU 缓存污染(coherent 映射),或通过 dma_map_single() 进行流式映射。
  • 初始化:分配后立即清零,设置默认标志位,绑定对应的 DMA 缓冲区地址。
  • 并发安全:分配操作通常受 spin_lock 保护,或使用 per-CPU 描述符缓存以减少锁竞争。
/* 描述符分配示例 */
struct dma_desc *desc;
dma_addr_t desc_dma;

desc = dma_pool_alloc(priv->desc_pool, GFP_KERNEL, &desc_dma);
if (!desc)
    return -ENOMEM;

memset(desc, 0, sizeof(*desc));
desc->buf_addr = cpu_to_le32(desc_dma);
desc->flags = 0;
/* 初始化完成回调上下文 */
desc->callback = my_complete_cb;
desc->cb_data = my_data;
性能提示:在高速传输场景(如 10G/25G 网卡),建议在 probe 阶段预分配一批描述符,放入空闲链表,运行时直接取用,避免运行时频繁调用 dma_pool_alloc。

4.2 提交 (Submit)

提交阶段将填充好的描述符“交给”DMA 引擎。驱动需要确保描述符内容对硬件可见,然后触发 DMA 开始传输。

步骤 操作 内存屏障 说明
1 填充描述符字段 无 设置缓冲区地址、长度、中断标志等
2 写内存屏障 wmb() 或 dma_wmb() 保证描述符写入完成后再更新指针
3 更新硬件指针 writel() 隐含屏障 写 DMA 引擎的 tail 寄存器或 doorbell
4 可选:读刷新 readl() 某些硬件需要读取状态寄存器确认
/* 描述符提交示例 */
static void submit_desc(struct my_priv *priv, struct dma_desc *desc)
{
    unsigned long flags;

    spin_lock_irqsave(&priv->lock, flags);

    /* 将描述符加入硬件环形缓冲区 */
    priv->ring[priv->prod_idx] = desc;
    priv->prod_idx = (priv->prod_idx + 1) & (RING_SIZE - 1);

    /* 保证描述符写入完成 */
    dma_wmb();

    /* 通知硬件:更新 tail 寄存器 */
    writel(priv->prod_idx, priv->ioaddr + DMA_TAIL_REG);

    spin_unlock_irqrestore(&priv->lock, flags);
}
常见错误:忘记在更新硬件指针前插入写屏障,导致 DMA 引擎读到未完全写入的描述符,引发数据错乱。在 ARM/PPC 等弱排序架构上尤其致命。

4.3 完成 (Complete)

DMA 引擎完成传输后,通过 中断 或 轮询 通知驱动。驱动在完成处理函数中遍历已完成的描述符,提取传输状态,并调用用户注册的回调。

  • 中断模式:在 ISR 中读取硬件状态寄存器,确定哪些描述符已完成,然后调用 napi_schedule() 或直接处理。
  • 轮询模式:驱动定时检查描述符的 done 标志位(由硬件写入),适用于低延迟或中断合并场景。
  • 状态提取:读取描述符中的传输字节数、错误标志、时间戳等。
/* 描述符完成处理示例 */
static void process_completed(struct my_priv *priv)
{
    struct dma_desc *desc;
    u32 completed_idx = readl(priv->ioaddr + DMA_HEAD_REG);

    while (priv->cons_idx != completed_idx) {
        desc = priv->ring[priv->cons_idx];

        /* 确保硬件写入对 CPU 可见 */
        dma_rmb();

        if (desc->status & DESC_ERROR) {
            dev_err(priv->dev, "DMA transfer error\n");
            /* 错误处理逻辑 */
        }

        /* 调用完成回调 */
        if (desc->callback)
            desc->callback(desc->cb_data, desc->trans_len);

        priv->cons_idx = (priv->cons_idx + 1) & (RING_SIZE - 1);
    }
}

4.4 释放 (Free)

描述符使用完毕后,必须归还给内存池或空闲链表。释放操作必须确保 DMA 引擎已经不再访问该描述符,否则可能造成 use-after-free 错误。

场景 释放方式 注意事项
正常完成 归还空闲链表 检查描述符是否被硬件重新使用(环形缓冲区场景)
超时/错误 强制回收 需要先重置 DMA 通道,确保硬件停止
驱动卸载 批量释放 遍历所有描述符,调用 dma_pool_free
/* 描述符释放示例 */
static void free_desc(struct my_priv *priv, struct dma_desc *desc, dma_addr_t dma)
{
    /* 确保描述符不再被硬件使用 */
    WARN_ON(desc->flags & DESC_IN_FLIGHT);

    /* 归还给 DMA 内存池 */
    dma_pool_free(priv->desc_pool, desc, dma);
}

/* 批量释放所有描述符(驱动卸载时) */
static void free_all_descs(struct my_priv *priv)
{
    int i;
    for (i = 0; i < RING_SIZE; i++) {
        if (priv->ring[i]) {
            dma_pool_free(priv->desc_pool,
                          priv->ring[i],
                          priv->ring_dma[i]);
            priv->ring[i] = NULL;
        }
    }
}
生命周期检查清单:
  1. 分配时是否设置了正确的 DMA 方向和缓冲区大小?
  2. 提交前是否插入了 dma_wmb()?
  3. 完成处理中是否使用了 dma_rmb() 保证数据可见性?
  4. 释放前是否确认描述符不在硬件飞行队列中?
  5. 环形缓冲区场景下,是否正确处理了 prod_idx 和 cons_idx 的 wrap-around?
描述符环形缓冲区状态转移图 描述符环形缓冲区状态转移 空闲池 Free List alloc 硬件环形缓冲区 (Ring) 槽0 槽1 槽2 槽3 ... ← cons_idx (硬件已消费) prod_idx (驱动待提交) → complete 完成队列 Done List 释放 (free) 回空闲池 状态转移说明: • 空闲池 → 环形缓冲区:驱动分配描述符并填充数据。 • 环形缓冲区 → 完成队列:DMA 引擎处理完毕,驱动在中断中回收。

4.5 生命周期中的内存屏障策略

描述符管理中的内存屏障是保证数据一致性的关键。下表总结了各阶段所需的屏障类型及位置:

阶段 屏障类型 放置位置 作用
提交前 dma_wmb() 填充描述符之后,写 tail 寄存器之前 保证描述符内容对 DMA 引擎可见
完成时 dma_rmb() 读取硬件更新的 head 指针之后,读取描述符之前 保证硬件写入的描述符状态对 CPU 可见
释放前 smp_mb() 修改描述符状态(如清除 IN_FLIGHT)之后 保证状态修改在其他核可见,防止并发释放
最佳实践:使用 dma_wmb() 和 dma_rmb() 而非通用的 wmb()/rmb(),因为 DMA 屏障在支持非一致性内存架构(如 ARM)的平台上会被优化为更轻量的指令。

4.6 异常路径处理

在实际系统中,描述符生命周期可能因错误而中断。驱动必须处理以下异常场景:

  • 描述符分配失败:返回 -ENOMEM,停止提交新描述符,等待已完成描述符释放后再重试。
  • DMA 超时:重置 DMA 通道,回收所有飞行中的描述符,调用错误回调。
  • 描述符泄漏:在环形缓冲区中,如果 prod_idx 追上 cons_idx,驱动必须停止提交,等待硬件消费。
  • 中断丢失:使用 watchdog 定时器定期检查描述符完成状态,防止死锁。
/* 超时恢复示例 */
static void dma_timeout_recovery(struct my_priv *priv)
{
    unsigned long flags;

    spin_lock_irqsave(&priv->lock, flags);

    /* 停止 DMA 引擎 */
    writel(0, priv->ioaddr + DMA_CTRL_REG);

    /* 回收所有飞行中的描述符 */
    while (priv->cons_idx != priv->prod_idx) {
        struct dma_desc *desc = priv->ring[priv->cons_idx];
        desc->flags &= ~DESC_IN_FLIGHT;
        if (desc->error_callback)
            desc->error_callback(desc->cb_data, -ETIMEDOUT);
        priv->cons_idx = (priv->cons_idx + 1) & (RING_SIZE - 1);
    }

    /* 重置硬件指针 */
    writel(0, priv->ioaddr + DMA_TAIL_REG);
    writel(0, priv->ioaddr + DMA_HEAD_REG);

    spin_unlock_irqrestore(&priv->lock, flags);
}
注意:在超时恢复中,必须确保所有 DMA 传输已经终止,否则回收描述符后硬件可能仍在写入内存,导致数据损坏。建议在重置前先读取硬件状态寄存器确认 idle。