3. DMA传输流程:描述符初始化、Doorbell机制、完成中断与数据一致性
DMA(Direct Memory Access)是PCIe设备实现高性能数据传输的核心机制。在驱动开发中,DMA传输的完整流程涉及描述符链表的构建、硬件门铃触发、完成中断处理以及数据一致性维护四个关键环节。任何一个环节出现异常,都可能导致数据传输错误、系统崩溃或性能骤降。
3.1 DMA描述符初始化
DMA描述符(Descriptor)是驱动与硬件之间约定的事务控制结构,通常驻留在主机内存中。描述符定义了传输方向、数据地址、长度、下一个描述符指针以及控制标志位。
3.1.1 描述符结构定义
典型的DMA描述符结构如下(以64位系统为例):
struct dma_descriptor {
uint64_t src_addr; // 源地址(系统物理地址或PCIe总线地址)
uint64_t dst_addr; // 目的地址
uint32_t length; // 传输字节数
uint16_t control; // 控制标志:方向、中断使能、链结束等
uint16_t status; // 硬件回写状态:完成/错误/重试
uint64_t next_desc; // 下一个描述符的物理地址(链表指针)
uint32_t reserved; // 保留字段,对齐到32字节
} __attribute__((packed));
3.1.2 描述符链表初始化流程
驱动在初始化阶段需要完成以下步骤:
- 分配连续物理内存:使用
dma_alloc_coherent()或pci_alloc_consistent()分配描述符表,确保内存不被CPU缓存干扰。 - 填充描述符字段:设置源/目的地址(需转换为PCIe总线地址)、传输长度、控制标志。
- 建立链表关系:将
next_desc指向下一个描述符的物理地址,最后一个描述符的next_desc置为0或循环指向头部。 - 通知硬件基地址:将描述符表的物理基地址写入设备的BAR空间寄存器。
关键点:描述符内存必须使用一致性DMA分配接口,避免CPU缓存与硬件访问之间的数据不一致。使用
dma_alloc_coherent() 返回的内存天然是非缓存的(uncacheable)。
3.1.3 描述符初始化SVG流程图
3.2 Doorbell机制
Doorbell(门铃)是驱动通知硬件“描述符已就绪,可以开始传输”的轻量级机制。驱动通过向设备的特定Doorbell寄存器写入一个值(通常是描述符索引或地址),触发硬件DMA引擎开始处理描述符链表。
3.2.1 Doorbell工作原理
| 角色 | 操作 | 说明 |
|---|---|---|
| 驱动(软件) | 写Doorbell寄存器 | 写入待处理的描述符索引或生产者指针 |
| PCIe设备(硬件) | 检测Doorbell写完成 | 通过PCIe Memory Write事务感知门铃事件 |
| DMA引擎 | 从描述符表读取描述符 | 发起实际的DMA读/写传输 |
3.2.2 Doorbell寄存器操作示例
// 假设Doorbell寄存器偏移为 0x1000,位于BAR0
void ring_doorbell(struct pci_dev *pdev, uint32_t desc_index)
{
void __iomem *doorbell_reg = pci_iomap(pdev, 0, 0) + 0x1000;
// 写入描述符索引,触发硬件开始传输
writel(desc_index, doorbell_reg);
// 确保写操作已经到达设备(PCIe posted write)
// 通常需要读回同一寄存器或使用 mmiowb()
wmb(); // 写内存屏障
readl(doorbell_reg); // 读回以flush PCIe写缓冲
}
调试陷阱:Doorbell写入后必须执行
readl() 回读或 mmiowb(),否则PCIe posted write可能滞留在写缓冲中,导致硬件无法及时感知门铃事件。这是DMA传输“卡住”的常见原因之一。
3.2.3 Doorbell与描述符交互SVG图
3.3 DMA完成中断
当硬件完成一个或多个描述符的DMA传输后,需要通过中断通知驱动。完成中断是驱动确认传输结果、回收描述符、触发下一批传输的关键信号。
3.3.1 中断处理流程
- 硬件触发MSI/MSI-X中断:设备在完成描述符后,将完成状态写入描述符的status字段,然后发送中断。
- 中断服务例程(ISR):驱动读取中断状态寄存器,确认中断来源。
- 遍历完成描述符:从上次处理的描述符位置开始,检查status字段是否标记为“完成”。
- 回调上层:将完成的DMA事务通知上层协议栈或应用程序。
- 重新填充描述符:将已完成的描述符回收,加入空闲队列供下次使用。
3.3.2 中断处理代码骨架
static irqreturn_t dma_isr(int irq, void *dev_id)
{
struct my_dev *dev = (struct my_dev *)dev_id;
uint32_t intr_status;
// 1. 读取中断状态寄存器
intr_status = readl(dev->regs + INT_STATUS_REG);
if (!(intr_status & DMA_DONE_MASK))
return IRQ_NONE;
// 2. 清除中断标志
writel(DMA_DONE_MASK, dev->regs + INT_CLEAR_REG);
// 3. 处理已完成的描述符
while (dev->completed_idx != dev->producer_idx) {
struct dma_descriptor *desc = &dev->desc_ring[dev->completed_idx];
if (!(desc->status & DESC_DONE))
break; // 硬件尚未完成
// 通知上层:传输完成
dma_complete_callback(dev, desc);
// 回收描述符
desc->status = 0;
dev->completed_idx = (dev->completed_idx + 1) % DESC_RING_SIZE;
}
return IRQ_HANDLED;
}
性能优化:对于高吞吐场景,建议使用MSI-X多队列中断,将不同DMA通道的中断绑定到不同CPU核心,避免单核瓶颈。同时,在ISR中尽量只做最小处理,将耗时操作推迟到tasklet或workqueue中。
3.3.3 中断与描述符完成状态SVG图
3.4 数据一致性维护
数据一致性是DMA传输中最容易被忽视但又最关键的问题。CPU缓存(Cache)与PCIe设备直接访问的内存之间可能存在数据不一致,导致驱动读到脏数据或设备读到过期数据。
3.4.1 一致性问题的根源
| 场景 | 问题描述 | 后果 |
|---|---|---|
| CPU写数据,设备DMA读 | CPU写入的数据仍在Cache中,未写回内存 | 设备读到旧数据 |
| 设备DMA写数据,CPU读 | 设备写入内存,但CPU Cache中仍是旧数据 | CPU读到脏数据 |
| 描述符状态回写 | 硬件更新描述符status字段,CPU未失效Cache | 驱动误判传输状态 |
3.4.2 一致性维护策略
Linux内核提供了两种DMA映射接口,分别对应不同的一致性需求:
- 一致性DMA映射(Coherent DMA):使用
dma_alloc_coherent(),返回的内存是non-cacheable的,CPU和硬件访问天然一致。适用于描述符表、控制结构等频繁共享的数据。 - 流式DMA映射(Streaming DMA):使用
dma_map_single()/dma_unmap_single(),配合dma_sync_single_for_cpu()/dma_sync_single_for_device()手动维护一致性。适用于数据缓冲区,避免non-cacheable带来的性能损失。
3.4.3 流式DMA映射使用示例
// 发送方向:CPU→设备
void *buf = kmalloc(size, GFP_KERNEL);
dma_addr_t dma_handle = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
// 填充数据到buf(CPU写入)
memcpy(buf, data, len);
// 确保CPU写入对设备可见
dma_sync_single_for_device(dev, dma_handle, size, DMA_TO_DEVICE);
// 将dma_handle写入描述符,触发Doorbell
// ... 设备执行DMA读 ...
// 传输完成后,解除映射
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);
// 接收方向:设备→CPU
dma_addr_t dma_handle = dma_map_single(dev, buf, size, DMA_FROM_DEVICE);
// 将dma_handle写入描述符,触发Doorbell
// ... 设备执行DMA写 ...
// 设备完成后,使CPU Cache失效,读取最新数据
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
// 现在可以安全读取buf中的数据
process_data(buf);
dma_unmap_single(dev, dma_handle, size, DMA_FROM_DEVICE);
常见错误:在
DMA_FROM_DEVICE 方向,如果忘记调用 dma_sync_single_for_cpu(),CPU可能读到Cache中的旧数据。在 DMA_TO_DEVICE 方向,如果忘记调用 dma_sync_single_for_device(),设备可能读到内存中的旧数据。这两个遗漏是DMA数据损坏的头号原因。