AMD IOMMU IO_PAGE_FAULT故障排查及驱动适配咨询
AMD-Vi IO_PAGE_FAULT 错误原因及驱动适配方案
错误原因分析
- 无效DMA地址传递:报错地址
0xfffffffffffffffc是一个无效的64位IOVA(IO虚拟地址),说明驱动直接传递了内核虚拟地址、未映射的物理地址,或者未初始化的指针给设备DMA控制器。当IOMMU启用时,设备只能访问经过IOMMU映射的IOVA,无法直接解析内核虚拟地址或裸物理地址,因此触发页错误。 - 未适配DMA API:禁用IOMMU时,设备可以直接访问物理内存,驱动的错误操作不会暴露;但启用IOMMU后,必须通过内核DMA API完成地址映射,否则IOMMU无法识别设备发起的地址请求。
- DMA掩码不匹配:设备DMA控制器的地址宽度与IOMMU配置的地址宽度不匹配,导致地址被截断或溢出,生成无效地址。
驱动需要添加的适配代码(基于Linux标准DMA API)
1. 设置DMA地址掩码
在驱动probe函数中,必须设置设备支持的DMA地址宽度,确保IOMMU使用正确的地址范围:
int ret; // 设置64位DMA掩码(根据设备实际支持的宽度调整) ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)); if (ret) { dev_err(&pdev->dev, "Failed to set DMA mask\n"); return ret; }
2. 分配DMA可用内存(替代直接物理内存分配)
如果驱动需要分配内存供设备DMA访问,使用dma_alloc_coherent()自动完成IOMMU映射:
// 分配4KB连续内存,返回内核虚拟地址(供CPU访问)和IOVA(供设备访问) void *cpu_addr = dma_alloc_coherent(&pdev->dev, 4096, &dma_addr, GFP_KERNEL); if (!cpu_addr) { dev_err(&pdev->dev, "Failed to allocate DMA coherent memory\n"); return -ENOMEM; } // 使用dma_addr作为设备DMA控制器的目标地址 // 释放时调用dma_free_coherent(&pdev->dev, 4096, cpu_addr, dma_addr);
3. 映射分散聚集(Scatter-Gather)表项
针对设备的SG能力,使用dma_map_sg()映射每个SG表项,获取设备可访问的IOVA:
struct scatterlist sg[NUM_SG_ENTRIES]; int sg_count; dma_addr_t sg_dma_addrs[NUM_SG_ENTRIES]; int i; // 初始化SG表(假设已经填充了物理页或内核虚拟地址) sg_init_table(sg, NUM_SG_ENTRIES); // 填充SG表项(示例) sg_set_buf(&sg[0], cpu_addr1, len1); sg_set_buf(&sg[1], cpu_addr2, len2); // 映射SG表到IOVA,返回映射的表项数量 sg_count = dma_map_sg(&pdev->dev, sg, NUM_SG_ENTRIES, DMA_TO_DEVICE); if (sg_count != NUM_SG_ENTRIES) { dev_err(&pdev->dev, "Failed to map SG list\n"); return -EIO; } // 提取每个SG表项的IOVA,传给设备DMA控制器 for (i = 0; i < sg_count; i++) { sg_dma_addrs[i] = sg_dma_address(&sg[i]); } // DMA操作完成后,解除映射 dma_unmap_sg(&pdev->dev, sg, NUM_SG_ENTRIES, DMA_TO_DEVICE);
4. 映射用户空间缓冲区
如果测试程序从用户空间传递缓冲区,需要用DMA API映射用户空间内存:
// 用户空间缓冲区地址和长度 void __user *user_buf; size_t buf_len; dma_addr_t dma_addr; // 映射用户空间缓冲区到IOVA(DMA_TO_DEVICE表示设备向缓冲区写入) dma_addr = dma_map_single(&pdev->dev, user_buf, buf_len, DMA_TO_DEVICE); if (dma_mapping_error(&pdev->dev, dma_addr)) { dev_err(&pdev->dev, "Failed to map user buffer\n"); return -EFAULT; } // 使用dma_addr作为设备DMA目标地址 // 操作完成后解除映射 dma_unmap_single(&pdev->dev, dma_addr, buf_len, DMA_TO_DEVICE);
5. 缓存同步(可选但重要)
当设备完成DMA写入后,需要同步缓存确保CPU能正确读取数据:
// 同步单一缓冲区 dma_sync_single_for_cpu(&pdev->dev, dma_addr, buf_len, DMA_FROM_DEVICE); // 同步SG表 dma_sync_sg_for_cpu(&pdev->dev, sg, sg_count, DMA_FROM_DEVICE);
关键注意事项
- 所有传给设备DMA控制器的地址必须是
dma_addr_t类型的IOVA,而非内核虚拟地址或phys_addr_t物理地址。 - 必须在DMA操作完成后解除地址映射,避免内存泄漏或IOMMU表项耗尽。
- 如果设备支持32位DMA,需将DMA掩码设置为
DMA_BIT_MASK(32),避免生成64位无效地址。
内容的提问来源于stack exchange,提问作者gyuyoun
相关产品推荐
相关产品推荐

