dma_mmap_coherent() 映射内存的零拷贝用户-space TCP 发送
Zero-copy user-space TCP send of dma_mmap_coherent() mapped memory
我是 运行 Linux Cyclone V SoC 上的 5.1,它是一种 FPGA,在一个芯片上有两个 ARMv7 内核。我的目标是从外部接口收集大量数据并通过 TCP 套接字将这些数据(部分)流出。这里的挑战是数据速率非常高,可能接近饱和 GbE 接口。我有一个工作实现,它只使用 write()
对套接字的调用,但它的最高速度为 55MB/s;大约是理论 GbE 限制的一半。我现在正尝试让零拷贝 TCP 传输工作以增加吞吐量,但我遇到了瓶颈。
为了将数据从 FPGA 中取出到 Linux user-space,我编写了一个内核驱动程序。该驱动程序使用 FPGA 中的 DMA 块将大量数据从外部接口复制到连接到 ARMv7 内核的 DDR3 内存中。当使用 dma_alloc_coherent()
和 GFP_USER
探测时,驱动程序将此内存分配为一堆连续的 1MB 缓冲区,并通过在文件中实现 mmap()
将这些内存公开给用户 space 应用程序/dev/
并在预分配缓冲区上使用 dma_mmap_coherent()
将地址返回给应用程序。
到目前为止一切顺利; user-space 应用程序正在查看有效数据,并且吞吐量大于 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()
without MSG_ZEROCOPY
), 一切正常;因此用户space可以访问数据,但是vmsplice()
/send(..., MSG_ZEROCOPY)
调用似乎无法处理它。
我在这里错过了什么?有没有什么方法可以使用零拷贝 TCP 发送用户 space 地址,通过 dma_mmap_coherent()
从内核驱动程序获得?我可以使用另一种方法吗?
更新
所以我深入研究了内核中的sendmsg()
MSG_ZEROCOPY
路径,最终失败的调用是get_user_pages_fast()
。此调用 returns -EFAULT
因为 check_vma_flags()
找到 vma
中设置的 VM_PFNMAP
标志。当使用 remap_pfn_range()
或 dma_mmap_coherent()
将页面映射到用户 space 时,显然会设置此标志。我的下一个方法是找到另一种方法来 mmap
这些页面。
正如我在问题更新中发布的那样,根本问题是 zerocopy 网络不适用于使用 remap_pfn_range()
映射的内存(dma_mmap_coherent()
恰好在后台使用以及)。原因是这种类型的内存(设置了 VM_PFNMAP
标志)没有与每个页面关联的 struct page*
形式的元数据,这是它需要的。
然后解决方案是以 struct page*
s 与内存关联的方式分配内存。
现在我分配内存的工作流程是:
- 使用
struct page* page = alloc_pages(GFP_USER, page_order);
分配一块连续的物理内存,其中将分配的连续页面数由2**page_order
给出。
- 通过调用
split_page(page, page_order);
将 high-order/compound 页面拆分为 0 阶页面。现在这意味着 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()
时,我们真正需要做的就是为我们预先分配的所有 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
并以递减顺序获得由 sub 组成的任何大小的缓冲区- 不同大小的缓冲区。然后,这将需要一些逻辑将这些缓冲区在 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,为小于页面的内存分配提供服务。
Per-cpu 页面缓存服务单页分配请求,而 buddy-allocator 保留 11 个列表,每个列表分别包含 2^{0-10} 个物理页面。这些列表在分配和释放页面时表现良好,当然,前提是您请求的是 2 的幂大小的缓冲区。
我是 运行 Linux Cyclone V SoC 上的 5.1,它是一种 FPGA,在一个芯片上有两个 ARMv7 内核。我的目标是从外部接口收集大量数据并通过 TCP 套接字将这些数据(部分)流出。这里的挑战是数据速率非常高,可能接近饱和 GbE 接口。我有一个工作实现,它只使用 write()
对套接字的调用,但它的最高速度为 55MB/s;大约是理论 GbE 限制的一半。我现在正尝试让零拷贝 TCP 传输工作以增加吞吐量,但我遇到了瓶颈。
为了将数据从 FPGA 中取出到 Linux user-space,我编写了一个内核驱动程序。该驱动程序使用 FPGA 中的 DMA 块将大量数据从外部接口复制到连接到 ARMv7 内核的 DDR3 内存中。当使用 dma_alloc_coherent()
和 GFP_USER
探测时,驱动程序将此内存分配为一堆连续的 1MB 缓冲区,并通过在文件中实现 mmap()
将这些内存公开给用户 space 应用程序/dev/
并在预分配缓冲区上使用 dma_mmap_coherent()
将地址返回给应用程序。
到目前为止一切顺利; user-space 应用程序正在查看有效数据,并且吞吐量大于 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()
without MSG_ZEROCOPY
), 一切正常;因此用户space可以访问数据,但是vmsplice()
/send(..., MSG_ZEROCOPY)
调用似乎无法处理它。
我在这里错过了什么?有没有什么方法可以使用零拷贝 TCP 发送用户 space 地址,通过 dma_mmap_coherent()
从内核驱动程序获得?我可以使用另一种方法吗?
更新
所以我深入研究了内核中的sendmsg()
MSG_ZEROCOPY
路径,最终失败的调用是get_user_pages_fast()
。此调用 returns -EFAULT
因为 check_vma_flags()
找到 vma
中设置的 VM_PFNMAP
标志。当使用 remap_pfn_range()
或 dma_mmap_coherent()
将页面映射到用户 space 时,显然会设置此标志。我的下一个方法是找到另一种方法来 mmap
这些页面。
正如我在问题更新中发布的那样,根本问题是 zerocopy 网络不适用于使用 remap_pfn_range()
映射的内存(dma_mmap_coherent()
恰好在后台使用以及)。原因是这种类型的内存(设置了 VM_PFNMAP
标志)没有与每个页面关联的 struct page*
形式的元数据,这是它需要的。
然后解决方案是以 struct page*
s 与内存关联的方式分配内存。
现在我分配内存的工作流程是:
- 使用
struct page* page = alloc_pages(GFP_USER, page_order);
分配一块连续的物理内存,其中将分配的连续页面数由2**page_order
给出。 - 通过调用
split_page(page, page_order);
将 high-order/compound 页面拆分为 0 阶页面。现在这意味着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()
时,我们真正需要做的就是为我们预先分配的所有 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
并以递减顺序获得由 sub 组成的任何大小的缓冲区- 不同大小的缓冲区。然后,这将需要一些逻辑将这些缓冲区在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,为小于页面的内存分配提供服务。
Per-cpu 页面缓存服务单页分配请求,而 buddy-allocator 保留 11 个列表,每个列表分别包含 2^{0-10} 个物理页面。这些列表在分配和释放页面时表现良好,当然,前提是您请求的是 2 的幂大小的缓冲区。