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



我在Cyclone V SoC上运行Linux 5.1,这是一个在一个芯片中有两个ARMv7内核的FPGA。我的目标是从外部接口收集大量数据,并通过TCP套接字流式传输(部分)这些数据。这里的挑战是数据速率非常高,可能会使GbE接口饱和。我有一个工作实现,它只使用对套接字的write()调用,但最高可达55MB/s;大约是理论GbE极限的一半。我现在正试图让零拷贝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)调用似乎无法处理它

我在这里错过了什么?有没有任何方法可以使用零拷贝TCP发送,通过dma_mmap_coherent()从内核驱动程序获得用户空间地址?我还能用别的方法吗?

更新

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

正如我在问题的更新中所说,根本问题是零拷贝网络不适用于使用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. 通过调用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,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()现在允许套接字将该缓冲区用于具有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.

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

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

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

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

相关内容

  • 没有找到相关文章

最新更新