DMA学习笔记(二)——DMA传输速率优化

发布于:2025-07-08 ⋅ 阅读:(19) ⋅ 点赞:(0)

DMA学习笔记(二)——DMA传输速率优化

楼主最近在实习期间接手了一个NPU芯片PCIe驱动的性能优化工作,主要任务是解决DMA传输效率低下的问题。经过深入分析和优化,最终将DMA传输性能平均提升了28.5%,其中16MB大文件传输速度从3.2GB/s提升到4.05GB/s。这里分享一下整个优化过程和关键技术点。

1. 问题背景

1.1 项目环境

  • 硬件平台:SOC+自研NPU
  • 操作系统:Ubuntu 20.04 LTS,内核版本5.4.0
  • 开发环境:gcc 9.4.0,linux-headers开发包

1.2 性能问题分析

通过perf工具分析发现原始驱动存在以下问题:

  • 中断处理占用25%的CPU时间
  • 频繁的上下文切换导致性能损失
  • DMA描述符设置开销较大

使用perf进行性能分析:

sudo perf record -g ./test_dma_performance
sudo perf report

2. 原始实现分析

2.1 DMA描述符结构定义

首先看看NPU设备的DMA描述符结构:

// DMA描述符结构
struct npu_dma_descriptor {
    u64 src_addr;              // 源地址
    u64 dst_addr;              // 目标地址
    u32 size;                  // 传输大小
    u32 control;               // 控制字
    u64 next_desc;             // 下一个描述符地址
    u32 status;                // 状态字段
} __packed;

// NPU设备结构
struct npu_device {
    struct pci_dev *pdev;              // PCI设备
    void __iomem *mmio_base;           // MMIO基地址
    
    // DMA相关
    struct npu_dma_descriptor *dma_ring;   // DMA描述符环
    dma_addr_t dma_ring_phys;              // 描述符环物理地址
    void *dma_buffer_virt;                 // DMA缓冲区虚拟地址
    dma_addr_t dma_buffer_phys;            // DMA缓冲区物理地址
    
    // 同步机制
    struct mutex dev_mutex;            // 设备互斥锁
    spinlock_t dma_lock;              // DMA锁
    wait_queue_head_t dma_wait_queue; // DMA等待队列
    
    bool dma_busy;                     // DMA忙状态
    int irq;                          // 中断号
};

2.2 原始DMA传输实现

// 原始实现:单描述符传输
int original_dma_transfer(struct npu_device *dev, 
                         struct dma_transfer_request *req)
{
    struct npu_dma_descriptor *desc = &dev->dma_ring[0];
    unsigned long flags;
    
    spin_lock_irqsave(&dev->dma_lock, flags);
    
    if (dev->dma_busy) {
        spin_unlock_irqrestore(&dev->dma_lock, flags);
        return -EBUSY;
    }
    
    dev->dma_busy = true;
    
    // 设置单个描述符 - 问题所在!
    desc->src_addr = req->src_addr;
    desc->dst_addr = req->dst_addr;
    desc->size = req->size;
    desc->control = DMA_START | DMA_INTERRUPT_EN;
    desc->next_desc = 0;  // 单个传输
    
    spin_unlock_irqrestore(&dev->dma_lock, flags);
    
    // 启动DMA
    npu_write32(dev, NPU_REG_DMA_CTRL, 1);
    
    // 等待完成 - 每次都要中断!
    wait_event_timeout(dev->dma_wait_queue, !dev->dma_busy, 
                      msecs_to_jiffies(DMA_TIMEOUT_MS));
    
    return dev->dma_busy ? -ETIMEDOUT : 0;
}

2.3 原始实现的性能问题

  1. 单描述符传输:每次传输都使用一个描述符,无法充分利用硬件的批量传输能力
  2. 频繁中断:每次传输完成都产生中断,CPU开销大
  3. 串行等待:每次都要等待传输完成才能进行下一次传输

3. 优化方案设计

3.1 批量描述符链机制

核心思想是将大的传输拆分成多个小块,使用描述符链进行批量传输,只在最后一个描述符产生中断。

#define MAX_TRANSFER_SIZE     (16 * 1024 * 1024)  // 16MB
#define MIN_TRANSFER_SIZE     (4 * 1024)          // 4KB
#define OPTIMAL_CHUNK_SIZE    (1 * 1024 * 1024)   // 1MB
#define DMA_DESC_COUNT        256

// 计算最优描述符数量
static int calculate_optimal_descriptors(size_t total_size)
{
    int desc_count;
    
    if (total_size <= OPTIMAL_CHUNK_SIZE) {
        return 1;
    }
    
    desc_count = (total_size + OPTIMAL_CHUNK_SIZE - 1) / OPTIMAL_CHUNK_SIZE;
    return min(desc_count, DMA_DESC_COUNT - 1);
}

3.2 描述符链设置

// 设置描述符链
static int setup_descriptor_chain(struct npu_device *dev,
                                 struct dma_transfer_request *req,
                                 int desc_count)
{
    struct npu_dma_descriptor *desc_ring = dev->dma_ring;
    size_t remaining_size = req->size;
    u64 src_addr = req->src_addr;
    u64 dst_addr = req->dst_addr;
    int i;
    
    for (i = 0; i < desc_count; i++) {
        size_t chunk_size = min(remaining_size, (size_t)OPTIMAL_CHUNK_SIZE);
        
        desc_ring[i].src_addr = src_addr;
        desc_ring[i].dst_addr = dst_addr;
        desc_ring[i].size = chunk_size;
        desc_ring[i].control = DMA_START;
        desc_ring[i].status = 0;
        
        // 设置链接指针
        if (i < desc_count - 1) {
            desc_ring[i].next_desc = dev->dma_ring_phys + 
                                   (i + 1) * sizeof(struct npu_dma_descriptor);
        } else {
            desc_ring[i].next_desc = 0;  // 最后一个描述符
            desc_ring[i].control |= DMA_INTERRUPT_EN;  // 只在最后产生中断
        }
        
        src_addr += chunk_size;
        dst_addr += chunk_size;
        remaining_size -= chunk_size;
        
        if (remaining_size == 0) {
            break;
        }
    }
    
    // 确保内存一致性
    dma_sync_single_for_device(&dev->pdev->dev, dev->dma_ring_phys,
                              desc_count * sizeof(struct npu_dma_descriptor),
                              DMA_TO_DEVICE);
    
    return 0;
}

4. 优化后的DMA传输实现

4.1 完整的优化实现

// 优化后的DMA传输函数
int optimized_dma_transfer(struct npu_device *dev,
                          struct dma_transfer_request *req)
{
    unsigned long flags;
    int desc_count;
    int ret = 0;
    ktime_t start_time, end_time;
    
    pr_debug("DMA transfer: src=0x%lx, dst=0x%lx, size=%zu\n",
             req->src_addr, req->dst_addr, req->size);
    
    // 参数检查
    if (req->size == 0 || req->size > MAX_TRANSFER_SIZE) {
        pr_err("Invalid transfer size: %zu\n", req->size);
        return -EINVAL;
    }
    
    // 地址对齐检查
    if ((req->src_addr & 0x3) || (req->dst_addr & 0x3)) {
        pr_err("Address not aligned: src=0x%lx, dst=0x%lx\n",
               req->src_addr, req->dst_addr);
        return -EINVAL;
    }
    
    spin_lock_irqsave(&dev->dma_lock, flags);
    
    if (dev->dma_busy) {
        spin_unlock_irqrestore(&dev->dma_lock, flags);
        pr_warn("DMA engine is busy\n");
        return -EBUSY;
    }
    
    dev->dma_busy = true;
    spin_unlock_irqrestore(&dev->dma_lock, flags);
    
    start_time = ktime_get();
    
    // 计算最优描述符数量
    desc_count = calculate_optimal_descriptors(req->size);
    pr_debug("Using %d descriptors for %zu bytes\n", desc_count, req->size);
    
    // 设置描述符链
    ret = setup_descriptor_chain(dev, req, desc_count);
    if (ret) {
        pr_err("Failed to setup descriptor chain\n");
        goto error_cleanup;
    }
    
    // 启动链式DMA传输
    npu_write32(dev, NPU_REG_DMA_SRC, (u32)dev->dma_ring_phys);
    npu_write32(dev, NPU_REG_DMA_SRC + 4, (u32)(dev->dma_ring_phys >> 32));
    npu_write32(dev, NPU_REG_DMA_CTRL, DMA_CHAIN_MODE | DMA_START);
    
    // 等待传输完成
    ret = wait_event_timeout(dev->dma_wait_queue, !dev->dma_busy,
                            msecs_to_jiffies(DMA_TIMEOUT_MS));
    
    if (ret == 0) {
        pr_err("DMA transfer timeout\n");
        ret = -ETIMEDOUT;
        goto error_cleanup;
    }
    
    end_time = ktime_get();
    
    // 更新统计信息
    atomic64_inc(&dev->total_transfers);
    atomic64_add(req->size, &dev->total_bytes);
    
    // 性能统计
    {
        u64 transfer_time_us = ktime_to_us(ktime_sub(end_time, start_time));
        u64 bandwidth_mbps = (req->size * 1000) / (transfer_time_us * 1024);
        
        pr_debug("DMA transfer completed: %zu bytes in %llu us, %llu MB/s\n",
                 req->size, transfer_time_us, bandwidth_mbps);
    }
    
    return 0;

error_cleanup:
    spin_lock_irqsave(&dev->dma_lock, flags);
    dev->dma_busy = false;
    spin_unlock_irqrestore(&dev->dma_lock, flags);
    
    // 重置DMA引擎
    npu_write32(dev, NPU_REG_DMA_CTRL, DMA_RESET);
    
    return ret;
}

4.2 优化的中断处理

// 中断处理优化
irqreturn_t npu_dma_interrupt_handler(int irq, void *dev_id)
{
    struct npu_device *dev = (struct npu_device *)dev_id;
    u32 status;
    unsigned long flags;
    
    status = npu_read32(dev, NPU_REG_DMA_STATUS);
    
    if (!(status & DMA_INTERRUPT_PENDING)) {
        return IRQ_NONE;
    }
    
    // 清除中断状态
    npu_write32(dev, NPU_REG_DMA_STATUS, status);
    
    spin_lock_irqsave(&dev->dma_lock, flags);
    
    if (status & DMA_TRANSFER_COMPLETE) {
        dev->dma_busy = false;
        wake_up(&dev->dma_wait_queue);
        pr_debug("DMA transfer completed successfully\n");
    }
    
    if (status & DMA_ERROR) {
        dev->dma_busy = false;
        dev->error_count++;
        wake_up(&dev->dma_wait_queue);
        pr_err("DMA transfer error, status=0x%x\n", status);
    }
    
    spin_unlock_irqrestore(&dev->dma_lock, flags);
    
    return IRQ_HANDLED;
}

5. 性能测试与验证

5.1 测试程序设计

// 测试不同大小的传输性能
static void test_transfer_performance(int fd)
{
    size_t test_sizes[] = {
        4 * 1024,        // 4KB
        64 * 1024,       // 64KB
        1024 * 1024,     // 1MB
        4 * 1024 * 1024, // 4MB
        16 * 1024 * 1024 // 16MB
    };
    
    int num_tests = sizeof(test_sizes) / sizeof(test_sizes[0]);
    struct dma_transfer_request req;
    struct timeval start, end;
    int i, j;
    
    printf("DMA Performance Test Results:\n");
    printf("%-10s %-15s %-15s %-15s\n", 
           "Size", "Time(ms)", "Bandwidth(MB/s)", "Status");
    printf("------------------------------------------------\n");
    
    for (i = 0; i < num_tests; i++) {
        double total_time = 0;
        int success_count = 0;
        
        // 每个大小测试10次取平均值
        for (j = 0; j < 10; j++) {
            req.src_addr = 0x1000000;  // 测试源地址
            req.dst_addr = 0x2000000;  // 测试目标地址
            req.size = test_sizes[i];
            req.direction = 0;
            req.flags = 0;
            
            gettimeofday(&start, NULL);
            
            int ret = ioctl(fd, IOCTL_DMA_TRANSFER, &req);
            
            gettimeofday(&end, NULL);
            
            if (ret == 0) {
                double time_ms = (end.tv_sec - start.tv_sec) * 1000.0 +
                               (end.tv_usec - start.tv_usec) / 1000.0;
                total_time += time_ms;
                success_count++;
            }
            
            usleep(10000);  // 10ms间隔
        }
        
        if (success_count > 0) {
            double avg_time = total_time / success_count;
            double bandwidth = (test_sizes[i] / (1024.0 * 1024.0)) / (avg_time / 1000.0);
            
            printf("%-10zu %-15.2f %-15.2f %-15s\n",
                   test_sizes[i], avg_time, bandwidth, "PASS");
        } else {
            printf("%-10zu %-15s %-15s %-15s\n",
                   test_sizes[i], "N/A", "N/A", "FAIL");
        }
    }
}

5.2 性能对比结果

优化前后性能对比:

传输大小    优化前(MB/s)    优化后(MB/s)    提升幅度
----------------------------------------------------
4KB         45.2           52.1           15.3%
64KB        182.5          241.8          32.5%
1MB         512.3          683.7          33.4%
4MB         1250.8         1687.2         34.9%
16MB        3200.1         4050.3         26.6%

平均提升幅度:28.5%
最大提升幅度:34.9% (4MB传输)

6. 关键技术要点

6.1 描述符链优化策略

  1. 自适应分块:根据传输大小自动选择最优的分块策略
  2. 中断合并:只在描述符链的最后一个描述符产生中断
  3. 内存对齐:确保所有地址都满足硬件对齐要求

6.2 性能调优参数

// 关键参数定义
#define OPTIMAL_CHUNK_SIZE    (1 * 1024 * 1024)   // 1MB最优块大小
#define DMA_DESC_COUNT        256                  // 最大描述符数量
#define DMA_TIMEOUT_MS        5000                 // 超时时间

这些参数是通过大量测试确定的最优值:

  • 1MB块大小:在减少中断次数和保持传输效率之间的最佳平衡点
  • 256个描述符:支持最大256MB的单次传输请求
  • 5秒超时:给大文件传输留出充足的时间

6.3 错误处理机制

// 错误恢复处理
error_cleanup:
    spin_lock_irqsave(&dev->dma_lock, flags);
    dev->dma_busy = false;
    spin_unlock_irqrestore(&dev->dma_lock, flags);
    
    // 重置DMA引擎
    npu_write32(dev, NPU_REG_DMA_CTRL, DMA_RESET);
    
    return ret;

7. 遇到的坑和解决方案

7.1 内存一致性问题

问题:在多核系统上,CPU修改描述符后,GPU可能读取到旧的缓存数据。

解决方案:使用dma_sync_single_for_device()确保内存一致性:

// 确保内存一致性
dma_sync_single_for_device(&dev->pdev->dev, dev->dma_ring_phys,
                          desc_count * sizeof(struct npu_dma_descriptor),
                          DMA_TO_DEVICE);

7.2 中断丢失问题

问题:在高负载情况下偶发中断丢失,导致传输卡死。

解决方案:在中断处理函数中增加状态检查和超时机制:

// 增加超时检查
ret = wait_event_timeout(dev->dma_wait_queue, !dev->dma_busy,
                        msecs_to_jiffies(DMA_TIMEOUT_MS));

if (ret == 0) {
    pr_err("DMA transfer timeout\n");
    // 执行错误恢复
}

7.3 地址对齐问题

问题:硬件要求4字节地址对齐,用户传入的地址可能不满足要求。

解决方案:在传输前进行严格的地址检查:

// 地址对齐检查
if ((req->src_addr & 0x3) || (req->dst_addr & 0x3)) {
    pr_err("Address not aligned: src=0x%lx, dst=0x%lx\n",
           req->src_addr, req->dst_addr);
    return -EINVAL;
}

8. 总结

通过引入批量描述符链机制,成功将DMA传输性能提升了28.5%。主要优化点包括:

  1. 减少中断次数:从每次传输一个中断,改为描述符链共享一个中断
  2. 智能分块策略:根据传输大小自适应选择最优的分块方案
  3. 硬件特性充分利用:利用硬件的链式传输能力,减少CPU干预

这次优化让我深刻理解了Linux内核DMA机制的工作原理,也积累了宝贵的性能调优经验。在实际项目中,性能优化往往需要深入理解硬件特性,结合软件算法设计,才能取得理想的效果。

参考资料

  • Linux内核文档:Documentation/DMA-API.txt
  • PCIe规范文档
  • Linux设备驱动程序(第三版)- DMA章节

本文基于实际项目经验总结,代码示例已脱敏处理。如有技术问题,欢迎交流讨论。


网站公告

今日签到

点亮在社区的每一天
去签到