dma_map_sg后sg总长度不足请求值?代码排查求助
我通过vm_mmap和get_user_pages_fast分配16G系统内存,传入dma_map_sg后,所有SG条目的总长度小于16G;首次加载内核模块时dma_len等于16G,后续加载则不足。禁用IOMMU后该现象消失。
测试代码
#include <linux/memblock.h> #include <linux/kernel.h> #include <linux/slab.h> #include <linux/vmalloc.h> #include <linux/module.h> #include <linux/pci.h> #include <linux/mman.h> #include <linux/dma-mapping.h> #define __SIZE_TO_PAGE_NUM(a) ((a) >> PAGE_SHIFT) #define XFER_PAGE_NUM __SIZE_TO_PAGE_NUM(xfer->size) #define MAX_PIN_SIZE SZ_1G #define MAX_PIN_PAGE_NUM __SIZE_TO_PAGE_NUM(MAX_PIN_SIZE) static struct sg_table sgt; static struct page** pages = NULL; static int probe_status = 0; static u64 addr = 0; static u64 size = SZ_16G; static u32 page_num_pinned = 0; // 新增全局变量保存实际pin的页数 void dma_test_remove(struct pci_dev *pdev); static struct pci_device_id dma_test_dev_id[] = { { PCI_DEVICE(0x1e3e, 0x2), }, {} }; static int dma_test_probe(struct pci_dev *pdev, const struct pci_device_id *pent) { u64 buf; u32 page_num; u32 index; int n; int ret; int i; struct scatterlist *sg; u64 dma_len = 0; int count1 = 0; int count0 = 0; /* init dma*/ ret = dma_set_mask(&pdev->dev, DMA_BIT_MASK(48)); if (ret) { printk("dma_set_mask failed\n"); return ret; } ret = dma_set_coherent_mask(&pdev->dev, DMA_BIT_MASK(48)); if (ret) { printk("dma_set_coherent_mask failed\n"); return ret; } // alloc va addr = vm_mmap(NULL, 0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED | MAP_POPULATE, 0); if (IS_ERR(ERR_PTR(addr))) { printk("vm_mmap alloc va failed\n"); return -EFAULT; } printk("alloc va success\n"); page_num = size >> PAGE_SHIFT; pages = vmalloc(page_num * sizeof(struct page *)); // 修正结构体类型错误:struct pages* -> struct page* if (!pages) { vm_munmap(addr, size); printk("vmalloc alloc memory failed\n"); return -ENOMEM; } page_num_pinned = 0; // 重置全局变量 // pin memory buf = addr; while (page_num) { n = min_t(typeof(n), page_num, MAX_PIN_PAGE_NUM); // 恢复单次pin页限制 n = get_user_pages_fast(buf, n, FOLL_WRITE, (pages + page_num_pinned)); if (n < 0) { printk("pin pages failed %d\n", n); goto unpin_dma_pages; } page_num_pinned += n; page_num -= n; buf += (unsigned long)n << PAGE_SHIFT; } printk("pin pages success\n"); /*create sg table*/ ret = sg_alloc_table_from_pages(&sgt, pages, size >> PAGE_SHIFT, 0, size, GFP_KERNEL); if(ret) { printk("alloc sg_table failed %d\n", ret); goto unpin_dma_pages; } printk("alloc sg table success\n"); dma_len = 0; count0 = 0; count1 = 0; for_each_sg(sgt.sgl, sg, sgt.nents, i){ // 使用sgt.nents替代sg_nents(sgt.sgl) dma_len += sg_dma_len(sg); count0++; if (sg_dma_len(sg) == 0) count1++; } printk("count0 %d count1 %d\n", count0, count1); if (dma_len < size) { printk("dma len error in sg_alloc_table dma_len %llx size %llx\n", dma_len, size); } // dma map ret = dma_map_sg(&pdev->dev, sgt.sgl, sgt.nents, DMA_BIDIRECTIONAL); // 使用sgt.nents if (unlikely(!ret)) { printk("dma map sg failed %d\n", ret); ret = -ENOMEM; goto free_sg; } probe_status = 1; dma_len = 0; i = 0; count0 = 0; count1 = 0; for_each_sg(sgt.sgl, sg, sgt.nents, i){ // 使用sgt.nents dma_len += sg_dma_len(sg); count0++; if (sg_dma_len(sg) == 0) { count1++; } } printk("count0 %d count1 %d dma_len %llx request len %llx\n", count0, count1, dma_len, size); if (dma_len < size) { printk("dma len error in dma_map_sg dma_len %llx size %llx\n", dma_len, size); } printk("dma map success\n"); return 0; free_sg: sg_free_table(&sgt); unpin_dma_pages: for (index = 0; index < page_num_pinned; index++) { put_page(pages[index]); } vfree(pages); vm_munmap(addr, size); return ret; } void dma_test_remove(struct pci_dev *pdev) { int index; if (probe_status) dma_unmap_sg(&pdev->dev, sgt.sgl, sgt.nents, DMA_BIDIRECTIONAL); // 使用sgt.nents sg_free_table(&sgt); // 使用实际pin成功的页数释放 for (index = 0; index < page_num_pinned; index++) { put_page(pages[index]); } if (addr) vm_munmap(addr, size); if (pages) vfree(pages); printk("remove dma-test module\n"); } static struct pci_driver dma_test_pci_driver = { .name = "dma-test", .id_table = dma_test_dev_id, .probe = dma_test_probe, .remove = dma_test_remove, }; static int __init dma_map_test_init(void) { return pci_register_driver(&dma_test_pci_driver); } static void __exit dma_map_test_exit(void) { pci_unregister_driver(&dma_test_pci_driver); } module_init(dma_map_test_init); module_exit(dma_map_test_exit); MODULE_DEVICE_TABLE(pci, dma_test_dev_id); MODULE_AUTHOR("zhanged"); MODULE_DESCRIPTION("zhanged Test dma_map_sg for large size"); MODULE_LICENSE("GPL and additional rights");
服务器环境
- Ubuntu 24.04 LTS
- 内核版本:6.8.0-48-generic(更换内核后问题仍存在)
- CPU:Intel Core i7-10700
- 开启
intel_iommu=on,禁用IOMMU后问题消失
问题分析与修复建议
代码中的错误点
get_user_pages_fast无限制调用
原代码注释了单次pin页的限制,一次性尝试pin全部16G内存。当系统内存碎片化或IOMMU资源不足时,后续加载模块会因无法分配足够DMA映射资源导致部分SG条目失效。恢复min_t限制,分批次pin内存可缓解此问题。内存释放逻辑错误
原remove函数中遍历总页数释放内存,但实际只有page_num_pinned页被成功pin住,遍历总页数会访问未初始化的数组元素,引发内存错误。需改用实际pin成功的页数进行释放。SG条目数获取错误
使用sg_nents(sgt.sgl)获取SG条目数不准确,sg_table结构体的nents字段才是维护的有效条目数,应替换为sgt.nents。结构体类型错误
原代码中vmalloc(page_num * sizeof(struct pages *))存在类型错误,应为struct page *。
IOMMU相关原因
开启IOMMU时,DMA映射需要分配大量IOMMU页表条目。首次加载模块时系统尚有足够空闲资源,后续加载时资源耗尽,导致部分内存无法完成DMA映射,表现为SG条目长度为0。禁用IOMMU后,DMA直接使用物理地址,无需额外页表资源,问题消失。
修复后的代码说明
上述代码已修正了所有提到的错误点:恢复分批次pin页逻辑、修正内存释放逻辑、使用正确的SG条目数、修复结构体类型错误。
内容的提问来源于stack exchange,提问作者andyZhang

