4. DMA传输描述符:描述符生命周期、描述符池管理、async_tx与同步机制

DMA传输描述符(Descriptor)是驱动与DMA引擎之间的核心契约。每个描述符封装了一次DMA传输所需的全部信息:源地址、目的地址、传输长度、传输方向、中断标志以及链接指针。本章深入剖析描述符从创建到释放的完整生命周期,并重点讲解高性能驱动中不可或缺的描述符池管理策略,以及async_tx框架下的异步提交与同步等待机制。

核心观点:描述符管理是DMA驱动性能的分水岭。动态分配/释放描述符在高频传输场景下会引入不可接受的延迟和碎片;描述符池预分配+异步提交+批量回收是工业级驱动的标准实践。

4.1 描述符生命周期:从创建到回收

一个典型的DMA描述符在驱动中经历以下五个阶段:

  1. 分配(Allocation):从内存中分配描述符对象,通常使用dma_pool_alloc()或kmem_cache_alloc()从预分配池中获取。
  2. 初始化(Init):填充描述符字段:源/目的地址、长度、方向、DMA通道句柄、回调函数等。
  3. 提交(Submit):将描述符挂入DMA引擎的待处理队列,调用tx_submit()返回一个dma_cookie_t。
  4. 执行与完成(Execution & Completion):DMA引擎硬件执行传输,完成后触发中断或轮询更新状态。
  5. 回收(Reclaim):驱动确认传输完成后,将描述符归还给池或释放内存。
生命周期关键点:描述符在提交后至完成前,驱动不得修改其内容;完成回调中应尽快回收描述符,避免阻塞后续传输。
描述符生命周期状态机 分配 初始化 提交 执行/完成 回收 归还池中 描述符生命周期:分配 → 初始化 → 提交 → 执行/完成 → 回收 → 归还池中

4.2 描述符池管理:预分配与零碎片

在高速数据路径(如网络、存储、视频)中,每个数据包或I/O请求都需要一个或多个描述符。如果每次传输都通过kmalloc动态分配,会带来以下问题:

  • 分配延迟:内存分配器可能触发回收或 compaction,导致微秒级延迟。
  • 内存碎片:频繁分配/释放小对象会加剧内核内存碎片。
  • Cache抖动:描述符对象分散在不同内存页,降低CPU缓存命中率。

解决方案是使用描述符池(Descriptor Pool)。Linux内核提供两种池化机制:

机制 API 适用场景 特点
DMA Pool dma_pool_create() / dma_pool_alloc() 描述符需要DMA一致性内存(非coherent或coherent) 保证描述符在DMA地址空间连续;自动对齐
SLAB Cache kmem_cache_create() / kmem_cache_alloc() 描述符仅需常规内核内存,无需DMA一致性 对象大小固定;分配/释放速度快;CPU缓存友好
避坑指南:不要混用DMA Pool和SLAB Cache。如果描述符需要被DMA引擎直接访问(硬件读取描述符),必须使用dma_pool_alloc()并获取物理地址;如果描述符仅由软件管理,使用kmem_cache_alloc()更轻量。

描述符池的典型初始化流程:

/* 使用DMA Pool创建描述符池 */
struct dma_pool *desc_pool;
desc_pool = dma_pool_create("my_dma_desc_pool",
                            &pdev->dev,   /* 设备 */
                            sizeof(struct my_desc), /* 描述符大小 */
                            32,            /* 对齐字节 */
                            0);            /* 边界,0表示无限制 */

/* 从池中分配一个描述符 */
struct my_desc *desc;
dma_addr_t desc_dma;
desc = dma_pool_alloc(desc_pool, GFP_KERNEL, &desc_dma);
if (!desc) {
    dev_err(&pdev->dev, "Failed to allocate descriptor from pool\n");
    return -ENOMEM;
}
/* desc_dma 是描述符的DMA总线地址,供硬件使用 */
描述符池管理架构 描述符池 (dma_pool / kmem_cache) 描述符 #0 (空闲) 描述符 #1 (使用中) 描述符 #2 (空闲) 驱动代码 dma_pool_alloc() dma_pool_free() DMA引擎硬件 读取描述符 DMA地址 描述符池预分配固定数量描述符,驱动从池中获取/归还,避免动态分配开销

4.3 async_tx与同步机制:异步提交与完成通知

Linux内核的async_tx框架(include/linux/async_tx.h)为DMA引擎提供了统一的异步操作接口。其核心思想是:驱动提交描述符后立即返回,不阻塞当前执行上下文;传输完成时通过回调或轮询通知驱动。

4.3.1 异步提交流程

/* 1. 准备描述符 */
struct dma_async_tx_descriptor *tx;
tx = chan->device->device_prep_dma_memcpy(chan, dst, src, len, flags);
if (!tx) {
    dev_err(dev, "Failed to prepare DMA memcpy descriptor\n");
    return -EIO;
}

/* 2. 设置完成回调 */
tx->callback = my_dma_callback;
tx->callback_param = my_private_data;

/* 3. 提交描述符到DMA引擎 */
dma_cookie_t cookie;
cookie = dmaengine_submit(tx);
if (dma_submit_error(cookie)) {
    dev_err(dev, "Failed to submit DMA descriptor\n");
    return -EIO;
}

/* 4. 启动DMA通道(可选,取决于硬件) */
dma_async_issue_pending(chan);

4.3.2 同步等待机制

虽然async_tx是异步框架,但驱动有时需要等待传输完成(例如在关闭设备或卸载驱动时)。内核提供两种同步方式:

方式 API 行为 适用场景
忙等待轮询 dma_sync_wait() 循环检查cookie状态,直到传输完成 中断上下文或不允许睡眠的路径
睡眠等待 wait_for_completion() 在完成回调中调用complete(),唤醒等待进程 进程上下文,允许睡眠
最佳实践:在中断上下文中使用dma_sync_wait()时,注意轮询时间不宜过长;在进程上下文中优先使用wait_for_completion(),避免浪费CPU。

使用wait_for_completion()的典型模式:

/* 定义完成量 */
struct completion tx_done;

/* 回调函数 */
void my_dma_callback(void *param) {
    struct completion *done = (struct completion *)param;
    complete(done);
}

/* 提交并等待 */
init_completion(&tx_done);
tx->callback = my_dma_callback;
tx->callback_param = &tx_done;
dmaengine_submit(tx);
dma_async_issue_pending(chan);

/* 等待传输完成(超时保护) */
unsigned long timeout = msecs_to_jiffies(5000);
if (!wait_for_completion_timeout(&tx_done, timeout)) {
    dev_err(dev, "DMA transfer timed out\n");
    /* 处理超时:重置通道、清理描述符 */
    dmaengine_terminate_sync(chan);
}
async_tx 异步提交与同步等待流程 驱动代码 1. prep_dma_memcpy 2. 设置callback 3. dmaengine_submit 4. issue_pending DMA引擎 执行传输 完成中断 提交描述符 回调通知 同步等待:dma_sync_wait / wait_for_completion

4.4 描述符链与批量提交

对于需要传输大量数据或分散/聚集I/O的场景,单个描述符往往不够。DMA引擎支持描述符链(Descriptor Chain):多个描述符通过链表指针串联,硬件自动依次执行,无需CPU逐条干预。

描述符链的典型结构:

struct my_hw_desc {
    __le32 src_addr;
    __le32 dst_addr;
    __le16 length;
    __le16 flags;       /* 中断使能、链结束等 */
    __le32 next_desc;   /* 下一个描述符的DMA地址,0表示链尾 */
} __attribute__((packed));

批量提交时,驱动只需将链头描述符提交给DMA引擎,引擎会自动遍历整个链。这极大降低了CPU与硬件之间的交互次数。

性能数据:在典型网络驱动中,使用描述符链批量提交可以将CPU占用率降低30%~50%,同时提升吞吐量20%以上。描述符链的长度通常由硬件FIFO深度和延迟要求共同决定。

4.5 描述符管理常见陷阱

陷阱 后果 解决方案
描述符未对齐 DMA引擎无法正确读取描述符,导致传输错误或系统崩溃 使用dma_pool_create()时指定对齐参数,或手动确保描述符地址对齐到16/32/64字节
描述符在传输中被修改 硬件读取到不一致的描述符,传输数据损坏 提交后使用dma_wmb()确保写入可见;在完成回调前不修改描述符
描述符泄漏 池中描述符耗尽,后续传输失败 在完成回调中必须调用dma_pool_free()或kmem_cache_free();使用引用计数或跟踪链表
回调中睡眠 在中断上下文中调用wait_for_completion()或mutex_lock()导致死锁 回调中只做轻量工作(如唤醒workqueue),将耗时操作推迟到进程上下文
关键提醒:描述符管理是DMA驱动中最容易出bug的区域之一。建议在开发初期就实现描述符池的统计接口(如/sys/kernel/debug/),监控分配/回收计数,及时发现泄漏。