4. 描述符生命周期管理:分配(alloc)、提交(submit)、完成(complete)、释放(free)全流程
DMA 描述符的生命周期是驱动高带宽传输的核心脉络。一个描述符从创建到销毁,依次经历 分配、提交、完成、释放 四个阶段。每个阶段都涉及特定的数据结构操作、硬件交互和内存屏障,任何环节的疏漏都可能导致数据错乱或系统崩溃。
核心原则:描述符的生命周期管理必须保证无锁或轻量锁设计,避免在数据路径上引入不必要的延迟。描述符的 ownership 在 CPU 与 DMA 引擎之间明确转移,是正确性的基石。
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;
}
}
}
生命周期检查清单:
- 分配时是否设置了正确的 DMA 方向和缓冲区大小?
- 提交前是否插入了
dma_wmb()? - 完成处理中是否使用了
dma_rmb()保证数据可见性? - 释放前是否确认描述符不在硬件飞行队列中?
- 环形缓冲区场景下,是否正确处理了 prod_idx 和 cons_idx 的 wrap-around?
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。