4. DMA传输描述符:描述符生命周期、描述符池管理、async_tx与同步机制
DMA传输描述符(Descriptor)是驱动与DMA引擎之间的核心契约。每个描述符封装了一次DMA传输所需的全部信息:源地址、目的地址、传输长度、传输方向、中断标志以及链接指针。本章深入剖析描述符从创建到释放的完整生命周期,并重点讲解高性能驱动中不可或缺的描述符池管理策略,以及async_tx框架下的异步提交与同步等待机制。
核心观点:描述符管理是DMA驱动性能的分水岭。动态分配/释放描述符在高频传输场景下会引入不可接受的延迟和碎片;描述符池预分配+异步提交+批量回收是工业级驱动的标准实践。
4.1 描述符生命周期:从创建到回收
一个典型的DMA描述符在驱动中经历以下五个阶段:
- 分配(Allocation):从内存中分配描述符对象,通常使用
dma_pool_alloc()或kmem_cache_alloc()从预分配池中获取。 - 初始化(Init):填充描述符字段:源/目的地址、长度、方向、DMA通道句柄、回调函数等。
- 提交(Submit):将描述符挂入DMA引擎的待处理队列,调用
tx_submit()返回一个dma_cookie_t。 - 执行与完成(Execution & Completion):DMA引擎硬件执行传输,完成后触发中断或轮询更新状态。
- 回收(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总线地址,供硬件使用 */
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);
}
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/),监控分配/回收计数,及时发现泄漏。