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 原始实现的性能问题
- 单描述符传输:每次传输都使用一个描述符,无法充分利用硬件的批量传输能力
- 频繁中断:每次传输完成都产生中断,CPU开销大
- 串行等待:每次都要等待传输完成才能进行下一次传输
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 描述符链优化策略
- 自适应分块:根据传输大小自动选择最优的分块策略
- 中断合并:只在描述符链的最后一个描述符产生中断
- 内存对齐:确保所有地址都满足硬件对齐要求
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%。主要优化点包括:
- 减少中断次数:从每次传输一个中断,改为描述符链共享一个中断
- 智能分块策略:根据传输大小自适应选择最优的分块方案
- 硬件特性充分利用:利用硬件的链式传输能力,减少CPU干预
这次优化让我深刻理解了Linux内核DMA机制的工作原理,也积累了宝贵的性能调优经验。在实际项目中,性能优化往往需要深入理解硬件特性,结合软件算法设计,才能取得理想的效果。
参考资料
- Linux内核文档:Documentation/DMA-API.txt
- PCIe规范文档
- Linux设备驱动程序(第三版)- DMA章节
本文基于实际项目经验总结,代码示例已脱敏处理。如有技术问题,欢迎交流讨论。