Linux驱动开发——MSI-X中断与DMA传输

发布时间:2026/7/7 11:21:27
Linux驱动开发——MSI-X中断与DMA传输 上一期讲完XDMA架构兴冲冲打开Xilinx官方驱动改完描述符和通道配置准备上线。可真正上板加载驱动接连碰到MSI-X注册失败、读到的数据全是0、CPU被中断占满卡死……这些官方手册一笔带过甚至完全不提的问题也是驱动开发最折磨人的环节。「IRQ handler took too long」「系统卡死」「数据全是0」这几个问题全是坑前置知识本文假设你已经熟悉XDMA基本概念见上一期《XDMA架构——SG DMA怎么工作的》并具备Linux内核模块基础。第一个真实痛苦MSI-X注册失败系统根本进不去你照着手册写ret pci_enable_msix(pfdev, entries, 32);if (ret) {dev_err(pfdev-dev, MSI-X enable failed: %d\n, ret);return ret;}一加载驱动——textpcilib: 0000:01:00.0: cant enable MSI-X for rc系统直接报错驱动加载失败。为什么BIOS把MSI-X的路由给别的设备了。PCIe RCRoot Complex有硬件限制一个设备能用的MSI-X向量数由BIOS分配不是你想用多少就用多少。三步排查# 1. 先看系统到底给了多少MSI-X向量cat /proc/interrupts | grep -i xdma# 2. 看PCIe设备的MSI-X能力lspci -vvv -s 01:00.0 | grep -A 10 MSI-X# 3. 如果向量数不够试试减少请求数4~8个是最稳妥的选择补充高版本Linux推荐使用pci_enable_msix_range自适应申请向量兼容性优于固定数量申请。简单场景直接限制4~8个即可。通常4~8个向量是最稳妥的选择覆盖主要场景就够了。32个向量在某些服务器平台上会触发BIOS限制。第二个真实痛苦中断来了但数据全是0驱动加载成功了XDMA也启动了MSI-X也触发了。但用户态读到数据全是0。你在isr里加了打印static irqreturn_t xdma_isr(int irq, void *dev_id){pr_err(%s: IRQ fired!\n, __func__);return IRQ_HANDLED;}打印出来了说明中断确实来了。但数据呢问题在哪你忘了映射DMA缓冲区到用户态。很多教程里的mmap实现是错的// ❌ 错误做法直接映射BARstatic int xdma_mmap(struct file *filp, struct vm_area_struct *vma){struct xdma_dev *dev filp-private_data;// 直接映射BAR——PCIe BAR的延迟很高if (remap_pfn_range(vma, vma-vm_start,pci_resource_start(pfdev, 0) PAGE_SHIFT,vma-vm_end - vma-vm_start,vma-vm_page_prot))return -EAGAIN;return 0;}PCIe BAR的读延迟是200~300ns见PCIe协议篇你这样映射后用户态读4字节就要等300ns。这不是零拷贝这是零优化。正确做法用dma_alloc_coherent分配专用DMA缓冲区// ✅ 正确做法static int xdma_mmap(struct file *filp, struct vm_area_struct *vma){struct xdma_dev *dev filp-private_data;unsigned long size vma-vm_end - vma-vm_start;// 映射的是DMA一致的专用内存缓冲区不是BARif (remap_pfn_range(vma, vma-vm_start,dev-host_buf_dma PAGE_SHIFT,size, vma-vm_page_prot))return -EAGAIN;return 0;}// 分配DMA缓冲区probe里dev-host_buf dma_alloc_coherent(pfdev-dev, DMA_BUF_SIZE,dev-host_buf_dma, GFP_KERNEL);XDMA的C2H通道直接把数据写进这块DMA内存mmap映射后用户态读的就是这块内存。零拷贝、延迟低、带宽高。第三个真实痛苦中断风暴系统卡死跑了一会儿系统突然变卡。top一看一个CPU核100%在处理中断。这就是中断风暴Interrupt Storm。原因描述符完成后中断状态寄存器没有被清除。// ❌ 错误读完状态就完了没有写回去清除static irqreturn_t xdma_isr(int irq, void *dev_id){struct xdma_dev *dev dev_id;u32 status readl(dev-bar XDMA_C2H_STATUS);// 读取状态后直接返回硬件认为中断未处理// 硬件会一直触发同一个中断return IRQ_HANDLED; // 中断标志没有清}XDMA中断状态寄存器是写1清除Write-1-to-Clear读出来的是当前状态写对应位1清除该中断。正确做法写1清零static irqreturn_t xdma_isr(int irq, void *dev_id){struct xdma_dev *dev dev_id;// 寄存器偏移基于常规XDMA配置实际项目请以PG195手册为准#define XDMA_C2H_INT_STATUS 0x1048#define XDMA_C2H_DONE_PIDC 0x1050u32 status readl(dev-bar XDMA_C2H_INT_STATUS);if (!(status 0x1)) // 检查C2H完成中断return IRQ_NONE;// 写1清零writel(status, dev-bar XDMA_C2H_INT_STATUS);// 读取完成的描述符索引u32 done_idx readl(dev-bar XDMA_C2H_DONE_PIDC);done_idx 0xFFFF;// 备注0xFF为掩码适用于描述符数量≤255场景数量更大请调整掩码位数set_bit(done_idx 0xFF, dev-completed_mask);wake_up_interruptible(dev-waitq);// 重要原则中断服务程序禁止耗时操作复杂业务请转交工作队列/用户态处理return IRQ_HANDLED;}另外还有个常见问题每个描述符都触发中断。如果是1MHz采样率、每描述符1KB数据32个描述符全满的时间约32ms中断频率约30次/秒这还好。但如果描述符很小4KB且每个都触发中断中断频率会很高CPU扛不住。优化方案用轮询poll替代高频中断// 用户态用poll/epoll替代interruptstatic __u32 xdma_poll(struct file *filp, poll_table *wait){struct xdma_dev *dev filp-private_data;__u32 mask 0;poll_wait(filp, dev-waitq, wait);if (dev-completed_mask ! 0)mask | POLLIN | POLLRDNORM;return mask;}用户态int fd open(/dev/xdma0, O_RDWR);struct pollfd pfd { .fd fd, .events POLLIN };while (1) {poll(pfd, 1, 1000); // 等待数据超时1sif (pfd.revents POLLIN) {// 有数据了读取int idx find_first_bit(completed_mask);process_buffer(idx);}}第四个真实痛苦probe里申请的内存remove里忘了释放static void xdma_remove(struct pci_dev *pfdev){struct xdma_dev *dev pci_get_drvdata(pfdev);// ❌ 漏了dma_free_coherent——内核内存泄漏// ❌ 漏了pci_disable_msix// ❌ 漏了free_irqiounmap(dev-bar);pci_disable_device(pfdev);kfree(dev); // 只有这个}一加载卸载驱动几十次系统内存就没了。完整removestatic void xdma_remove(struct pci_dev *pfdev){struct xdma_dev *dev pci_get_drvdata(pfdev);if (!dev) return;// 停止DMAC2H控制寄存器偏移0x1000写0停止writel(0, dev-bar 0x1000);// 释放中断for (int i 0; i NUM_MSIX; i)free_irq(dev-msix_entries[i].vector, dev);pci_disable_msix(pfdev);// 释放DMA缓冲区不能忘if (dev-host_buf)dma_free_coherent(pfdev-dev, DMA_BUF_SIZE,dev-host_buf, dev-host_buf_dma);// 释放描述符环如果有if (dev-desc_ring)dma_free_coherent(pfdev-dev, dev-desc_ring_size,dev-desc_ring, dev-desc_ring_dma);// 释放BAR映射if (dev-bar)iounmap(dev-bar);pci_disable_device(pfdev);kfree(dev);}调试清单按这个顺序查当你遇到“数据读不到/中断不触发/系统卡死”按这个顺序排查□ dmesg | grep -i xdma ← 先看内核日志有没有报错□ cat /proc/interrupts | grep xdma ← 看中断有没有注册上□ lspci -vvv -s 01:00.0 | grep -i msi ← 看MSI-X向量数□ readl(bar XDMA_C2H_INT_STATUS) ← 直接读寄存器看状态□ 描述符的src_addr/dst_addr是不是64B对齐□ 描述符的control字段有没有设置CONTROL_COMPLETE_EN中断使能□ MSI-X中断有没有正确清除写1清零□ DMA缓冲区有没有正确分配dma_alloc_coherent□ remove里所有资源是否都有对应的释放关键代码完整的proberemovec **源码下载**由于公众号正文篇幅有限代码请点击阅读原文总结痛苦点根因标准解法MSI-X 注册失败BIOS/PCIe RC 限制最大向量数降低申请向量至4~8个用命令行排查硬件能力读取数据全0错误映射 BAR 空间未使用 DMA 一致性内存用dma_alloc_coherent分配缓冲区mmap映射该内存中断风暴、CPU 100%中断状态未执行「写1清零」中断频率过高ISR内主动写回状态寄存器清中断高频场景改用 poll/epoll内核内存泄漏remove 函数未成对释放资源probe申请的中断、MSI-X、DMA内存、BAR映射在 remove 中全部释放关注我下一期讲用户态开发——如何正确读取DMA缓冲区、poll机制、ring buffer设计写出一个工业级的FPGA驱动用户态程序。