:完善虚拟DMA驱动的数据传输逻辑,实现中断处理、SG传输、性能统计,并编写测试用例)
上一节我们把虚拟DMA驱动的骨架搭起来了能申请通道、能配置传输参数。但说实话那只是个空壳子真正干活儿的部分——数据传输、中断响应、还有高性能场景必备的SGScatter-Gather模式都还没动。今天咱们就把这些硬骨头啃下来。我个人习惯写驱动一定要先把中断处理想清楚。为什么因为DMA的精髓就是异步CPU把活儿派下去DMA干完了得通知你。这个通知机制要是搞砸了整个系统就卡死了。我在项目中遇到过好几次中断处理函数写得不对导致数据丢包或者系统死锁排查起来特别痛苦。中断处理让DMA学会“喊报告”先看中断处理的核心逻辑。当DMA传输完成硬件会触发一个中断。我们的驱动要做三件事从硬件寄存器读取中断状态确认是哪个通道完成的清理中断标志位防止重复触发调用注册的回调函数通知上层数据准备好了代码实现大概是这样的static irqreturn_t virtual_dma_irq_handler(int irq, void *dev_id) { struct virtual_dma_device *vdma dev_id; u32 status; int i; // 读取中断状态寄存器 status readl(vdma-base VIRTUAL_DMA_IRQ_STATUS); for (i 0; i vdma-num_channels; i) { if (status (1 i)) { struct virtual_dma_channel *chan vdma-channels[i]; // 清理中断标志 writel(1 i, vdma-base VIRTUAL_DMA_IRQ_CLEAR); // 更新传输状态 chan-state VIRTUAL_DMA_STATE_COMPLETE; // 回调通知上层 if (chan-callback) chan-callback(chan-callback_param); // 性能统计记录完成时间 do_gettimeofday(chan-stat.end_time); chan-stat.transfer_count; } } return IRQ_HANDLED; }注意中断处理函数里不要做耗时操作比如打印日志、分配内存。我曾经见过有人直接在中断里用printk结果系统响应变得极慢。回调函数也要设计成轻量级的真正耗时的处理应该放到工作队列或tasklet里。SG传输解决大块数据搬运的痛点你想想看如果一次要传输1MB的数据但系统内存是碎片化的物理地址不连续怎么办传统的DMA只能处理连续物理地址这就尴尬了。SG传输就是为此而生——它允许你把多个不连续的物理内存块通过一个描述符链表串起来DMA硬件会自动遍历这个链表完成所有数据块的搬运。说白了SG就是DMA的“批处理模式”。我们的虚拟DMA驱动要实现SG需要准备一个描述符数组struct virtual_dma_sg_entry { dma_addr_t src_addr; // 源物理地址 dma_addr_t dst_addr; // 目的物理地址 size_t length; // 本段长度 }; struct virtual_dma_sg_desc { struct virtual_dma_sg_entry *entries; int num_entries; int current_entry; // 当前正在传输的条目索引 };启动SG传输的代码逻辑static int virtual_dma_start_sg(struct virtual_dma_channel *chan, struct virtual_dma_sg_desc *desc) { unsigned long flags; int ret 0; spin_lock_irqsave(chan-lock, flags); // 检查通道状态 if (chan-state ! VIRTUAL_DMA_STATE_IDLE) { dev_err(chan-dev, Channel %d is busy\n, chan-id); ret -EBUSY; goto out; } // 保存SG描述符 chan-sg_desc desc; chan-state VIRTUAL_DMA_STATE_RUNNING; // 配置第一个SG条目到硬件寄存器 writel(desc-entries[0].src_addr, chan-base VIRTUAL_DMA_SRC_ADDR); writel(desc-entries[0].dst_addr, chan-base VIRTUAL_DMA_DST_ADDR); writel(desc-entries[0].length, chan-base VIRTUAL_DMA_LENGTH); // 设置SG模式标志 writel(1, chan-base VIRTUAL_DMA_SG_MODE); // 启动传输 writel(1, chan-base VIRTUAL_DMA_START); // 性能统计记录开始时间 do_gettimeofday(chan-stat.start_time); out: spin_unlock_irqrestore(chan-lock, flags); return ret; }经验之谈SG传输的关键在于描述符链表的维护。我在MTK平台上调试过一款摄像头驱动SG列表有128个条目结果因为描述符地址没有对齐到16字节硬件直接罢工了。记住很多DMA控制器对描述符有对齐要求务必查阅芯片手册。性能统计用数据说话驱动写得好不好不能光靠感觉。我们需要一套性能统计机制量化DMA的传输效率。我习惯在通道结构体里加一个统计子结构struct virtual_dma_stats { struct timeval start_time; // 传输开始时间 struct timeval end_time; // 传输结束时间 unsigned long transfer_count; // 总传输次数 unsigned long total_bytes; // 总传输字节数 unsigned long max_latency; // 最大延迟(us) unsigned long min_latency; // 最小延迟(us) unsigned long avg_latency; // 平均延迟(us) };每次传输完成后更新统计信息static void update_stats(struct virtual_dma_channel *chan, size_t bytes) { unsigned long latency; struct timeval now; do_gettimeofday(now); latency (now.tv_sec - chan-stat.end_time.tv_sec) * 1000000 (now.tv_usec - chan-stat.end_time.tv_usec); chan-stat.total_bytes bytes; chan-stat.transfer_count; // 更新最大/最小延迟 if (latency chan-stat.max_latency) chan-stat.max_latency latency; if (latency chan-stat.min_latency || chan-stat.min_latency 0) chan-stat.min_latency latency; // 更新平均延迟 chan-stat.avg_latency (chan-stat.avg_latency * (chan-stat.transfer_count - 1) latency) / chan-stat.transfer_count; }通过sysfs接口把这些数据暴露给用户空间方便调试static ssize_t stats_show(struct device *dev, struct device_attribute *attr, char *buf) { struct virtual_dma_channel *chan dev_get_drvdata(dev); return sprintf(buf, Transfer count: %lu\n Total bytes: %lu\n Max latency: %lu us\n Min latency: %lu us\n Avg latency: %lu us\n, chan-stat.transfer_count, chan-stat.total_bytes, chan-stat.max_latency, chan-stat.min_latency, chan-stat.avg_latency); }测试用例验证驱动的正确性驱动写完了不测试等于没写。我建议至少写三个层次的测试用例测试级别测试内容预期结果单元测试单次DMA传输4字节对齐数据正确中断触发SG测试5个不连续内存块总大小64KB所有数据正确搬运压力测试连续1000次传输随机大小无数据丢失无内存泄漏这里给一个简单的测试代码片段static int test_single_transfer(struct virtual_dma_device *vdma) { struct virtual_dma_channel *chan; dma_addr_t src, dst; void *src_buf, *dst_buf; int ret; // 分配DMA缓冲区 src_buf dma_alloc_coherent(vdma-dev, 4096, src, GFP_KERNEL); dst_buf dma_alloc_coherent(vdma-dev, 4096, dst, GFP_KERNEL); if (!src_buf || !dst_buf) return -ENOMEM; // 填充源数据 memset(src_buf, 0xAA, 4096); memset(dst_buf, 0x00, 4096); // 请求通道 chan virtual_dma_request_channel(vdma, 0); if (!chan) { ret -EBUSY; goto out_free; } // 启动传输 ret virtual_dma_start_transfer(chan, src, dst, 4096); if (ret) goto out_release; // 等待完成实际应用应该用异步回调 msleep(100); // 验证数据 if (memcmp(src_buf, dst_buf, 4096) 0) pr_info(Test PASS: data match\n); else pr_err(Test FAIL: data mismatch\n); out_release: virtual_dma_release_channel(chan); out_free: dma_free_coherent(vdma-dev, 4096, src_buf, src); dma_free_coherent(vdma-dev, 4096, dst_buf, dst); return ret; }核心要点回顾中断处理要快进快出清理标志位后尽快调用回调SG传输解决了物理地址不连续的问题但要注意描述符对齐性能统计是驱动优化的基础用sysfs暴露给用户空间测试用例要覆盖单次传输、SG传输、压力测试三个层次嗯到这里虚拟DMA驱动的核心逻辑就完整了。从通道管理到中断处理从SG传输到性能统计再到测试验证这一套流程在MTK平台上我已经验证过多次。你按照这个思路去写基本不会出大问题。下一节咱们聊聊如何把这个驱动集成到内核的DMA Engine框架中让上层应用可以更方便地调用。