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

时间:2019-10-30 14:07:37

标签: linux linux-kernel embedded-linux splice zero-copy

我正在Cyclone V SoC上运行Linux 5.1,这是一个FPGA,在一个芯片中具有两个ARMv7内核。我的目标是从外部接口收集大量数据,并通过TCP套接字流出(部分)这些数据。这里的挑战是数据速率非常高,并且可能接近饱和GbE接口。我有一个可行的实现,只使用对套接字的write()调用,但它的最高速度为55MB / s;大约是理论GbE限制的一半。我现在正在尝试使零拷贝TCP传输能够提高吞吐量,但是我遇到了麻烦。

为了将数据从FPGA传送到Linux用户空间,我编写了内核驱动程序。该驱动程序使用FPGA中的DMA块将大量数据从外部接口复制到附加到ARMv7内核的DDR3存储器中。在使用dma_alloc_coherent()GFP_USER进行探测时,驱动程序将该内存分配为一堆连续的1MB缓冲区,并通过在mmap()中的文件上实现/dev/来将这些内存公开给用户空间应用程序并使用预分配缓冲区上的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(或send() MSG_ZEROCOPY),一切正常。因此用户空间可以访问数据,但是vmsplice() / send(..., MSG_ZEROCOPY)调用似乎无法处理它。

我在这里想念什么?有没有办法使用零拷贝TCP发送和通过dma_mmap_coherent()从内核驱动程序获得的用户空间地址一起使用?我可以使用另一种方法吗?

更新

因此,我深入研究了内核中的sendmsg() MSG_ZEROCOPY路径,最终失败的调用是get_user_pages_fast()。该调用返回-EFAULT,因为check_vma_flags()找到了VM_PFNMAP中设置的vma标志。当使用remap_pfn_range()dma_mmap_coherent()将页面映射到用户空间时,显然会设置此标志。我的下一个方法是找到另一种mmap这些页面的方法。

2 个答案:

答案 0 :(得分: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* 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]);
}

现在以这种方式实现mmap()允许套接字使用带有sendmsg()标志的MSG_ZEROCOPY将此缓冲区使用。

尽管这种方法行得通,但有两种方法无法使我满意:

  • 您只能使用此方法分配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.

如果内核中有一些接口可以分配任意数量的连续物理页,则可以轻松解决这些问题。我不知道为什么没有,但是我不认为上述问题如此重要,以至于不去研究为什么不可用/如何实现它:-)

答案 1 :(得分:2)

也许这可以帮助您理解为什么alloc_pages需要2的幂的页码。

为了优化经常使用的页面分配过程(并减少外部碎片),Linux内核开发了按CPU页面缓存和伙伴分配器来分配内存(还有另一个分配器slab来为内存分配服务)。小于一页)。

每个CPU页面缓存服务于一页分配请求,而伙伴分配器保留11个列表,每个列表分别包含2 ^ {0-10}个物理页面。这些列表在分配和释放页面时表现良好,当然,前提是您要求使用2的幂次方缓冲区。