DMA中断原理:完成、错误与向量
你想想看,DMA 只管搬数据,搬完了总得有人知道吧?这就是中断的作用。DMA 控制器在关键节点上踩一脚刹车,发个信号给 CPU,告诉它“活干完了”或者“出问题了”。
我个人习惯把 DMA 中断分成三类:完成中断、错误中断,以及承载它们的中断向量。这三者互相配合,构成了 DMA 与 CPU 的沟通机制。
核心观点:没有中断的 DMA 就像没有铃铛的快递员——你不知道包裹啥时候到,也不知道中途有没有摔坏。
一、完成中断
说白了,完成中断就是 DMA 通知 CPU:“我这趟传输搞定了,数据已经就位,你过来检查吧。” 触发时机通常有这几个:
- 单次块传输(Block Transfer)结束
- 链式描述符列表全部执行完毕
- 循环模式下,缓冲区满或半满
我在调优网络包处理的时候遇到过一个问题:完成中断来得太频繁,CPU 光忙着响应了,机器吞吐量反而往下掉。后来我才意识到,中断不是越频繁越好。你想想看,CPU 每次进中断都要保存上下文、查描述符、清标志位,一套下来好几百个时钟周期就没了。
我的经验:批量传输场景下,尽量启用批量完成中断(比如传输完 4 个 block 才发一次中断),或者用延时中断定时器做防抖。压力测试时盯着 /proc/interrupts 看,如果每秒中断数超过 10 万次,就得考虑合并中断了。
完成中断处理函数里,一般要干这几件事:
- 读取 DMA 状态寄存器,确认传输结果
- 清除中断挂起位(写 1 清零)
- 更新驱动里的描述符环形队列指针
- 通知上层协议栈数据已可用
/* 典型完成中断处理伪代码 */
void dma_done_irq_handler(int irq, void *dev_id) {
struct dma_device *dev = dev_id;
u32 status = readl(dev->regs + DMA_STATUS);
if (status & DMA_DONE) {
// 清除完成标志
writel(DMA_DONE, dev->regs + DMA_STATUS);
// 推进描述符队列
dev->completed_desc++;
// 唤醒等待数据的进程
wake_up_interruptible(&dev->wait_queue);
}
}
注意:有些 DMA 控制器在清除完成标志之前,不会响应下一次传输。所以必须先清标志,再后续处理。我曾经因为顺序搞反,导致中断风暴——清完状态之后又触发了新的完成中断,递归处理差点让系统 hang 死。
二、错误中断
错误中断比完成中断麻烦多了。它告诉 CPU:“出事了,但我也不知道该怎么办,你来擦屁股。” 常见错误类型有这些:
| 错误类型 | 触发原因 | 处理方式 |
|---|---|---|
| 总线错误 | DMA 访问非法地址、设备未响应 | 记录地址、停止通道、复位 PCIe 链路 |
| 传输超时 | 数据总线一直没准备好,超出计数器上限 | 超时复位、重新发起握手 |
| 数据对齐错误 | 源地址或目的地址不满足对齐要求 | 返回负值、驱动尝试软件修正或报错 |
| 描述符错误 | 链表中出现无效指针或非法配置 | 停止 DMA、打印描述符内容、排查内存损坏 |
我在项目里遇到最头疼的是总线错误。那次 FPGA 端的 DMA 在传输过程中突然拉低了 TREADY,主机这边的 DMA 控制器等了 256 个时钟就超时了,触发了错误中断。但错误标志还留在寄存器里,不清掉的话,整个 DMA 通道就直接废了。所以错误中断处理的第一件事,永远是“停通道→读错误详情→清标志→复位通道”。
避坑指南:我曾经在错误中断里只做了打印日志,没停 DMA 通道。结果 DMA 一直在错误的地址上读写,把内核堆栈都冲碎了。嗯,从那以后我要求所有错误处理函数必须调用 dma_ctrl(dev, DMA_TERMINATE_ALL, 0) 先停止当前所有传输。
/* 错误中断处理示例 */
void dma_err_irq_handler(int irq, void *dev_id) {
struct dma_device *dev = dev_id;
u32 err_status = readl(dev->regs + DMA_ERR_STATUS);
pr_err("DMA error on channel %d: status=0x%x\n", dev->chan, err_status);
// 终止当前传输
dma_ctrl(dev, DMA_TERMINATE_ALL, 0);
// 清除所有错误标志
writel(err_status, dev->regs + DMA_ERR_STATUS);
// 复位通道状态机
writel(1, dev->regs + DMA_CH_RESET);
// 通知驱动程序重新初始化
schedule_work(&dev->recovery_work);
}
三、中断向量
中断向量就是 DMA 中断在中断控制器里的“身份证号”。CPU 收到中断信号后,得通过查向量表才知道是哪个设备在叫。说白了,中断向量是一个物理中断号(IRQ number),每个 DMA 通道或者每个 DMA 控制器通常有一个独立的向量。
在 x86 平台上,DMA 中断可能会被映射到 MSI(Message Signaled Interrupt)向量上。ARM 平台则用 GIC(Generic Interrupt Controller)的 SPI 或 PPI 来分配。中断向量的编号和亲和性配置直接相关——你把中断号配给哪个 CPU 核心,那个核心就会处理对应的 DMA 完成或错误中断。
关键点:中断向量本身不决定性能,但它决定了中断发往哪个 CPU。选错了核心,缓存 miss 率能翻好几倍。
你可以在 /proc/interrupts 里看到每个 DMA 中断的触发次数和 CPU 分布:
$ cat /proc/interrupts | grep dma
196: 123456 98765 0 0 IR-PCI-MSI 512000-edge ioat-msix
197: 111222 333444 0 0 IR-PCI-MSI 512001-edge ioat-msix
左边的 196、197 就是中断向量号。右边的 ioat-msix 表示这是 Intel I/OAT DMA 驱动注册的中断名字。每一列对应一个 CPU 核心,数字是该核心处理的中断次数。如果发现只有一个核心在承受所有中断,那就需要做中断亲和性调整了——这部分我们后面几章重点讲。
小技巧:查看某个 DMA 通道的中断向量号,可以用 cat /sys/bus/pci/devices/0000:00:04.0/irq 直接拿到 IRQ 号。调试的时候特别好用。
好,到这里你应该能感受到完成中断、错误中断和中断向量各自扮演的角色了。下面这张图把它们的互动关系画出来了:
从图里你可以看到,完成中断和错误中断在中断逻辑里产生后,统一被映射到一个中断向量(可能是同一个,也可能是不同的 MSI-X 向量)。中断向量携带一个 IRQ 号,中断控制器根据亲和性设置把这个信号路由到某个指定 CPU 上。CPU 再调用我们前面写的处理函数。
记住一句话:中断向量是桥梁,亲和性是方向盘。桥梁本身不会堵车,但方向盘打歪了,所有车全挤到一个 CPU 上,那性能就完蛋了。