首页 > 解决方案 > 如何有效地将大页面支持的缓冲区传递给 Linux 中的 BM DMA 设备?

问题描述

我需要为在 FPGA 中实现的总线主控 DMA PCIe 设备提供一个巨大的循环缓冲区(几 GB)。

缓冲区不应在引导时保留。因此,缓冲区可能不连续。

该器件支持分散-聚集 (SG) 操作,但出于性能原因,缓冲区的连续连续段的地址和长度存储在 FPGA 内部。因此,使用标准 4KB 页面是不可接受的(每 1GB 缓冲区最多有 262144 个段)。

正确的解决方案应该在用户空间中分配由 2MB 大页面组成的缓冲区(将最大段数减少 512 倍)。缓冲区的虚拟地址应通过 ioctl 传输到内核驱动程序。然后应该计算段的地址和长度并将其写入FPGA。

理论上,我可以使用get_user_pages创建页面列表,然后调用sg_alloc_table_from_pages来获取适合在 FPGA 中编程 DMA 引擎的 SG 列表。不幸的是,在这种方法中,我必须准备每 1GB 缓冲区长度为 262144 页的页面结构的中间列表。该列表存储在 RAM 中,而不是 FPGA 中,因此问题较少,但无论如何最好避免它。

事实上,我不需要为内核保留页面映射,因为大页面受到保护以防换出,并且它们被映射给将处理接收到的数据的用户空间应用程序。

所以我正在寻找的是一个函数sg_alloc_table_from_user_hugepages,它可以获取基于大页面的内存缓冲区的这样一个用户空间地址,并将其直接传输到正确的分散列表中,而无需为内核执行不必要的和消耗内存的映射。当然,这样的函数应该验证缓冲区确实由大页面组成。

我找到并阅读了这些帖子:(A)(B),但找不到好的答案。在当前的 Linux 内核中是否有任何官方方法可以做到这一点?

标签: linuxdriverdmahuge-pages

解决方案


目前我有一个非常低效的解决方案,基于get_user_pages_fast

   int sgt_prepare(const char __user *buf, size_t count, 
               struct sg_table * sgt, struct page *** a_pages,
               int * a_n_pages)
   {
       int res = 0;
       int n_pages;
       struct page ** pages = NULL;
       const unsigned long offset = ((unsigned long)buf) & (PAGE_SIZE-1);
       //Calculate number of pages
       n_pages = (offset + count + PAGE_SIZE - 1) >> PAGE_SHIFT;
       printk(KERN_ALERT "n_pages: %d",n_pages);
       //Allocate the table for pages
       pages = vzalloc(sizeof(* pages) * n_pages);
       printk(KERN_ALERT "pages: %p",pages);
       if(pages == NULL) {
           res = -ENOMEM;
           goto sglm_err1;
       }
       //Now pin the pages
       res = get_user_pages_fast(((unsigned long)buf & PAGE_MASK), n_pages, 0, pages); 
       printk(KERN_ALERT "gupf: %d",res);   
       if(res < n_pages) {
           int i;
           for(i=0; i<res; i++)
               put_page(pages[i]);
           res = -ENOMEM;
           goto sglm_err1;
       }
       //Now create the sg-list
       res = sg_alloc_table_from_pages(sgt, pages, n_pages, offset, count, GFP_KERNEL);
       printk(KERN_ALERT "satf: %d",res);   
       if(res < 0)
           goto sglm_err2;
       *a_pages = pages;
       *a_n_pages = n_pages;
       return res;
   sglm_err2:
       //Here we jump if we know that the pages are pinned
       {
           int i;
           for(i=0; i<n_pages; i++)
               put_page(pages[i]);
       }
   sglm_err1:
       if(sgt) sg_free_table(sgt);
       if(pages) kfree(pages);
       * a_pages = NULL;
       * a_n_pages = 0;
       return res;
   }
   
   void sgt_destroy(struct sg_table * sgt, struct page ** pages, int n_pages)
   {
       int i;
       //Free the sg list
       if(sgt->sgl)
           sg_free_table(sgt);
       //Unpin pages
       for(i=0; i < n_pages; i++) {
           set_page_dirty(pages[i]);
           put_page(pages[i]);
       }
   }

sgt_prepare函数构建了sg_table我可以用来创建 DMA 映射的 sgt 结构。我已经验证它包含的条目数等于使用的大页面数。

不幸的是,它需要创建页面列表(通过a_pages指针参数分配和返回),并在使用缓冲区时保留。

因此,我真的不喜欢这种解决方案。现在我有 256 个 2MB 大页面用作 DMA 缓冲区。这意味着我必须创建并保留不必要的 128*1024 页面结构。我还浪费了 512 MB 的内核地址空间用于不必要的内核映射。

有趣的问题是,是否a_pages只能暂时保留(直到创建 sg-list)?理论上应该是可能的,因为页面仍然被锁定......


推荐阅读