14

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

为了将 FPGA 中的数据导入 Linux 用户空间,我编写了一个内核驱动程序。该驱动程序使用 FPGA 中的 DMA 块将大量数据从外部接口复制到连接到 ARMv7 内核的 D​​DR3 内存中。dma_alloc_coherent()当使用with进行探测时,驱动程序将此内存分配为一组连续的 1MB 缓冲区,并通过在文件中实现并将地址返回给使用预分配缓冲区的应用程序GFP_USER,将这些内存公开给用户空间应用程序。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()bufsend() MSG_ZEROCOPYvmsplice()send(..., MSG_ZEROCOPY)

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

更新

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

4

2 回答 2

8

正如我在我的问题的更新中发布的那样,根本问题是零拷贝网络不适用于已使用映射的内存remap_pfn_range()(它也dma_mmap_coherent()恰好在引擎盖下使用)。原因是这种类型的内存(带有VM_PFNMAP标志集)没有struct page*与每个页面相关联的形式的元数据,这是它需要的。

struct page*那么解决方案是以s内存相关联的方式分配内存。

现在适用于我分配内存的工作流程是:

  1. 用于struct page* page = alloc_pages(GFP_USER, page_order);分配一块连续的物理内存,其中将分配的连续页数由 给出2**page_order
  2. 通过调用将高阶/复合页面拆分为 0 阶页面split_page(page, page_order);。现在这意味着它已成为一个包含条目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,CPU 会处理缓存以确保我们没有读取过时的数据:

  1. dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);

现在,当我们想要实现 时mmap(),我们真正要做的就是vm_insert_page()重复调用我们预先分配的所有 0 阶页面:

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()并使用 scatter-gather( sg) 调用而不是DMA 对它们进行 DMA single
  • 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.

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

于 2019-11-04T16:14:43.190 回答
2

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

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

Per-cpu 页面缓存服务于一页分配请求,而 buddy-allocator 保留 11 个列表,每个列表分别包含 2^{0-10} 个物理页面。这些列表在分配和释放页面时表现良好,当然,前提是您请求的是 2 次幂大小的缓冲区。

于 2019-11-05T09:47:55.483 回答