零拷贝用户空间TCP发送dma_mmap_coherent()映射内存

14

我正在运行Linux 5.1,这是一个带有两个ARMv7内核的FPGA芯片Cyclone V SoC。我的目标是从外部接口收集大量数据,并通过TCP套接字流式传输(部分)此数据。挑战在于数据速率非常高,可能接近饱和千兆以太网界面。我已经有了一个工作实现,只使用write()调用将数据发送到套接字,但它的峰值仅为55MB/s;大约是理论千兆以太网限制的一半。我现在正在尝试使零拷贝TCP传输工作,以增加吞吐量,但我遇到了障碍。

为了将数据从FPGA传输到Linux用户空间,我编写了一个内核驱动程序。该驱动程序使用FPGA中的DMA块将大量数据从外部接口复制到连接在ARMv7内核上的DDR3存储器中。当通过dma_alloc_coherent()使用GFP_USER进行探测时,驱动程序将此内存分配为一组连续的1MB缓冲区,并通过在/dev/中实现mmap(),并使用dma_mmap_coherent()在预分配的缓冲区上向用户空间应用返回地址来将其公开给用户空间应用。

到目前为止,用户空间应用程序看到的是有效数据,而吞吐量大于360MB/s,还有余地(外部接口不足以真正了解上限是多少)。

为了实现零拷贝TCP网络,我的第一种方法是在套接字上使用SO_ZEROCOPY

sent_bytes = send(fd, buf, len, MSG_ZEROCOPY);
if (sent_bytes < 0) {
    perror("send");
    return -1;
}

然而,这会导致send: Bad address的结果。

经过一番搜索后,我的第二种方法是使用一个管道和splice(),然后跟着使用vmsplice()

ssize_t sent_bytes;
int pipes[2];
struct iovec iov = {
    .iov_base = buf,
    .iov_len = len
};

pipe(pipes);

sent_bytes = vmsplice(pipes[1], &iov, 1, 0);
if (sent_bytes < 0) {
    perror("vmsplice");
    return -1;
}
sent_bytes = splice(pipes[0], 0, fd, 0, sent_bytes, SPLICE_F_MOVE);
if (sent_bytes < 0) {
    perror("splice");
    return -1;
}
然而,结果是相同的:vmsplice: Bad address
请注意,如果我将对vmsplice()send()的调用替换为一个仅打印由buf指向的数据的函数(或者是没有MSG_ZEROCOPYsend()),一切都正常工作;因此,用户空间可以访问数据,但vmsplice()/send(..., MSG_ZEROCOPY)调用似乎无法处理它。
我错过了什么?有没有办法使用从内核驱动程序通过dma_mmap_coherent()获得的用户空间地址进行零拷贝TCP发送?我能用另一种方法吗?
更新:
因此,我深入研究了内核中的sendmsg() MSG_ZEROCOPY路径,最终失败的调用是get_user_pages_fast()。此调用返回-EFAULT,因为check_vma_flags()发现vma中设置了VM_PFNMAP标志。当使用remap_pfn_range()dma_mmap_coherent()将页面映射到用户空间时,会设置此标志。我的下一个方法是找到另一种方法来mmap这些页面。
2个回答

8
正如我在问题的更新中所发布的,潜在的问题在于使用remap_pfn_range()(在dma_mmap_coherent()内部也使用)映射的内存无法进行零拷贝网络操作。原因在于这种类型的内存(带有VM_PFNMAP标志)每页没有与之相关联的struct page*元数据。
解决方案是以一种方式分配内存,使得struct page*与内存相关联。
现在适用于我分配内存的工作流程是:
  1. 使用struct page * page = alloc_pages(GFP_USER, page_order);分配连续的物理内存块,其中将分配的连续页面数由2 ** page_order给出。
  2. 通过调用split_page(page, page_order)将高阶/复合页面拆分为0阶页面。这意味着struct page*现在已成为具有2 ** page_order个条目的数组。
现在要将这样的区域提交到DMA(用于数据接收):
  1. dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
  2. dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
  3. dmaengine_submit(dma_desc);
当我们从DMA收到传输完成的回调时,需要取消映射该区域以将该内存块的所有权转移回CPU,这会处理缓存以确保我们不读取旧数据:
  1. dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);
现在,当我们想要实现mmap()时,我们只需为预先分配的所有0阶页面重复调用vm_insert_page()即可。
static int my_mmap(struct file *file, struct vm_area_struct *vma) {
    int res;
...
    for (i = 0; i < 2**page_order; ++i) {
        if ((res = vm_insert_page(vma, vma->vm_start + i*PAGE_SIZE, &page[i])) < 0) {
            break;
        }
    }
    vma->vm_flags |= VM_LOCKED | VM_DONTCOPY | VM_DONTEXPAND | VM_DENYWRITE;
...
    return res;
}


在关闭文件时,不要忘记释放页面:
for (i = 0; i < 2**page_order; ++i) {
    __free_page(&dev->shm[i].pages[i]);
}

这种实现方式使得套接字可以使用该缓冲区和 MSG_ZEROCOPY 标志以用于sendmsg()

尽管这个方法可行,但我认为有两个问题:

  • 使用此方法只能分配大小为2的次方的缓冲区,尽管您可以实现逻辑来调用alloc_pages多次,每次使用递减序列获取由不同大小的子缓冲区组成的任何大小缓冲区。这将需要一些逻辑来在mmap()中将这些缓冲区连接在一起,并使用散射-聚集(sg)调用而不是single进行DMA。
  • split_page() 在其文档中说:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

如果内核中有一些界面来分配任意数量的连续物理页面,这些问题将很容易解决。我不知道为什么没有,但我并不认为上述问题很重要,以至于去挖掘为什么这不可用/如何实现它 :-)

2
也许这会帮助您理解为什么alloc_pages需要2的幂页数。为了优化页面分配过程(并减少外部碎片),Linux内核开发了per-cpu页面缓存和buddy-allocator来分配内存(还有另一个分配器slab,用于服务小于一页的内存分配)。Per-cpu页面缓存服务于一次请求分配一页,而buddy-allocator保留11个列表,每个列表包含2^{0-10}个物理页面。这些列表在分配和释放页面时表现良好,当然前提是您请求的是2的幂大小的缓冲区。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接