我正在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
这些页面的方法。
正如我在问题的更新中发布的那样,潜在的问题是,零复制网络不适用于使用映射的内存remap_pfn_range()
(也dma_mmap_coherent()
恰好在后台使用)。原因是这种类型的内存(VM_PFNMAP
设置了标志)没有所需的struct page*
与每个页面相关联的元数据。
然后将溶液是在一种方式分配存储器struct page*
小号都与所述存储器相关联。
现在对我来说分配内存的工作流程是:
使用struct page* page = alloc_pages(GFP_USER, page_order);
要分配的连续的物理存储器,在那里将被分配的连续页的数目由下式给出的块2**page_order
。
通过调用,将高阶/复合页面分为0阶页面split_page(page, page_order);
。现在,这意味着struct page* page
已成为具有2**page_order
条目的数组。
现在将这样的区域提交给DMA(用于数据接收):
dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
dmaengine_submit(dma_desc);
当我们从DMA收到回调已完成传输的回调时,我们需要取消映射该区域,以将该内存块的所有权转移回CPU,这将负责缓存以确保我们不会读取过时的数据:
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.
如果内核中有一些接口可以分配任意数量的连续物理页面,则可以轻松解决这些问题。我不知道为什么不存在,但是我不认为上述问题如此重要,以至于无法解释为什么不可用/如何实现它:-)