我有一个PCIe端点设备连接到主机。ep(端点)的512MB BAR被映射,memcpy用于传输数据。Memcpy相当慢(~2.5s)。当我没有映射所有的BAR(100字节),但运行满512MB的Memcpy时,我在0.5s内得到一个segfault,但当读回BAR的末尾时,数据显示了正确的数据。这意味着数据的读取与I对整个BAR空间进行mmap时相同。
数据是如何写入的?为什么它比正确的方式(没有segfault)快得多?
映射整个BAR的代码(耗时2.5秒):
fd = open(filename, O_RDWR | O_SYNC)
map_base = mmap(NULL, 536870912, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
int rand_fd = open(infile, O_RDONLY);
rand_base = mmap(0, 536870912, PROT_READ, MAP_SHARED, rand_fd, 0);
memcpy(map_base, rand_base, 536870912);
if(munmap(map_base, map_size) == -1)
{
PRINT_ERROR;
}
close(fd);
仅映射100字节的代码(耗时0.5秒):
fd = open(filename, O_RDWR | O_SYNC)
map_base = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
int rand_fd = open(infile, O_RDONLY);
rand_base = mmap(0, 536870912, PROT_READ, MAP_SHARED, rand_fd, 0);
memcpy(map_base, rand_base, 536870912);
if(munmap(map_base, map_size) == -1)
{
PRINT_ERROR;
}
close(fd);
为了检查写入的数据,我使用pcimemhttps://github.com/billfarrow/pcimem
编辑:当一致性数据在segfault之后被"写入"时,我很愚蠢,这不是应该的数据。因此,我关于memcpy在segfault之后完成的结论是错误的。我接受这个答案,因为它为我提供了有用的信息。
假设filename
只是一个普通文件(用于保存数据),则不使用O_SYNC
。这只会让事情慢下来[可能,很多]。
打开BAR设备时,请考虑使用O_DIRECT
。这样可以最大限度地减少缓存影响。也就是说,如果BAR设备自己进行缓存,则尽可能消除内核的缓存。
数据是如何写入的,为什么它比正确的方式(没有segfault)快得多?
;短";mmap/read
不工作。额外的数据来自之前的";"满";映射。所以,你的测试无效。
为确保结果一致,请对输出文件执行unlink
。用O_CREAT
执行open
。然后,使用ftruncate
将文件扩展到完整大小。
这里有一些代码可以尝试:
#define SIZE (512 * 1024 * 1024)
// remove the output file
unlink(filename);
// open output file (create it)
int ofile_fd = open(filename, O_RDWR | O_CREAT,0644)
// prevent segfault by providing space in the file
ftruncate(ofile_fd,SIZE);
map_base = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, ofile_fd, 0);
// use O_DIRECT to minimize caching effects when accessing the BAR device
#if 0
int rand_fd = open(infile, O_RDONLY);
#else
int rand_fd = open(infile, O_RDONLY | O_DIRECT);
#endif
rand_base = mmap(0, SIZE, PROT_READ, MAP_SHARED, rand_fd, 0);
memcpy(map_base, rand_base, SIZE);
if (munmap(map_base, map_size) == -1) {
PRINT_ERROR;
}
// close the output file
close(ofile_fd);
根据BAR设备的特性,为了最大限度地减少PCIe读取/获取/事务请求的数量,确保它作为32位(或64位)元素进行访问可能会有所帮助。
BAR空间是否允许/支持/鼓励访问";普通的";记忆力
通常memcpy
足够聪明;宽";自动访问内存(如果内存地址对齐,则在此处为)。也就是说,memcpy
将自动使用64位回迁,使用movq
,或者可能使用一些XMM
指令,例如movdqa
准确地了解您拥有的BAR设备将有所帮助。数据表/appnote应提供足够的信息。
更新:
感谢您的示例代码。不幸的是,由于某种原因,aarch64gcc给出了"O_DIRECT未声明"。在不使用该标志的情况下,速度与我的原始代码相同。
将#define _GNU_SOURCE
添加到任何#include
之上以解析O_DIRECT
PCIe设备是我们正在开发的FPGA。比特流当前是Xilinx DMA示例代码。BAR只有512MB的内存供系统R/W使用。–userYou
意外的是,我的答案是基于我访问Xilinx FPGA设备的BAR空间的经验(已经有一段时间了,大约在2010年)。
在诊断速度问题时,我们使用了PCIe总线分析仪。这可以显示CPU请求的总线请求的字节宽度。它还显示了周转时间(例如,从设备返回数据包之前的总线读取请求时间)。
我们还必须调整设备/BAR的PCIe配置寄存器中的参数(例如传输大小、事务回放)。这是反复试验,在决定最佳配置之前,我们(我)尝试了大约27种不同的组合
大约3年前,在一个不相关的arm系统(例如nVidia Jetson)上,我不得不对GPU内存执行memcpy
。它可能只是我使用的特定交叉编译器,但memcpy
的反汇编表明它only使用了字节范围的传输。也就是说,它没有x86的对手那么聪明。我编写/重写了一个使用unsigned long long
[和/或unsigned __int128
]传输的版本。这大大加快了速度。请参见下文。
因此,您可能希望反汇编生成的memcpy
代码。库函数和/或它可能内联到函数中的代码。
只是一个想法。。。如果你只是想要批量传输,你可能希望在FPGA上有设备程序DMA引擎的设备驱动程序。通过对设备驱动程序的自定义ioctl
调用可以更有效地处理这一问题,该调用接受描述所需传输的自定义结构(与来自用户空间的read
或mmap
相比)。
您正在为设备编写自定义设备驱动程序吗?或者,你只是在使用一些通用的设备驱动程序吗?
以下是我必须做的,才能在手臂上获得快速memcpy
。它生成ldp/stp
asm指令。
// qcpy.c -- fast memcpy
#include <string.h>
#include <stddef.h>
#ifndef OPT_QMEMCPY
#define OPT_QMEMCPY 128
#endif
#ifndef OPT_QCPYIDX
#define OPT_QCPYIDX 1
#endif
// atomic type for qmemcpy
#if OPT_QMEMCPY == 32
typedef unsigned int qmemcpy_t;
#elif OPT_QMEMCPY == 64
typedef unsigned long long qmemcpy_t;
#elif OPT_QMEMCPY == 128
typedef unsigned __int128 qmemcpy_t;
#else
#error qmemcpy.c: unknown/unsupported OPT_QMEMCPY
#endif
typedef qmemcpy_t *qmemcpy_p;
typedef const qmemcpy_t *qmemcpy_pc;
// _qmemcpy -- fast memcpy
// RETURNS: number of bytes transferred
size_t
_qmemcpy(qmemcpy_p dst,qmemcpy_pc src,size_t size)
{
size_t cnt;
size_t idx;
cnt = size / sizeof(qmemcpy_t);
size = cnt * sizeof(qmemcpy_t);
if (OPT_QCPYIDX) {
for (idx = 0; idx < cnt; ++idx)
dst[idx] = src[idx];
}
else {
for (; cnt > 0; --cnt, ++dst, ++src)
*dst = *src;
}
return size;
}
// qmemcpy -- fast memcpy
void
qmemcpy(void *dst,const void *src,size_t size)
{
size_t xlen;
// use fast memcpy for aligned size
if (OPT_QMEMCPY > 0) {
xlen = _qmemcpy(dst,src,size);
src += xlen;
dst += xlen;
size -= xlen;
}
// copy remainder with ordinary memcpy
if (size > 0)
memcpy(dst,src,size);
}
更新#2:
说到机缘巧合,我正在使用Jetson Orin。字节行为非常有趣。
只是一个想法。。。如果您在与FPGA相同的系统中有一个Jetson,您可以通过明智地使用cuda
来获得DMA操作
由于需要,我不能使用任何自定义内核模块,所以我试图在用户空间中完成这一切。
那是一个苛刻的情妇。。。使用自定义H/W,几乎可以公理化地使用自定义设备驱动程序。因此,这个要求听起来像是一个营销/执行要求,而不是一个技术要求。如果因为不知道目标内核版本而无法发送.ko
文件,则可以将驱动程序作为.o
发送,并将.ko
的创建推迟到安装脚本。
我们想使用DMA引擎,但我在这方面的学习曲线正在上升。我们在FPGA中使用DMA,但我认为只要我们可以写入dtb中指定的地址,就意味着DMA引擎已经设置并工作。现在我想知道我是否完全误解了这一部分userYou
你可能不会让DMA这么做。如果启动memcpy
,DMA引擎如何知道传输长度?
根据驱动程序的不同,使用read/write
和mmap
进行DMA可能会有更好的运气。
但是,如果是我,我会保留自定义驱动程序选项:
如果你必须在驱动程序/系统启动时调整/修改BAR配置寄存器,我想不起是否可以将配置寄存器映射到用户空间。
当进行
mmap
时,该设备可以被视为";后备存储器";用于映射。也就是说,仍然有一层额外的内核缓冲[就像映射普通文件时一样]。设备内存仅从内核[缓冲区]内存定期更新。自定义驱动程序可以使用一些只有内核/驱动程序才能访问的技巧来设置[保证]直接映射。
历史注释:
当我上一次使用Xilinx FPGA时(12年前),固件加载器实用程序(由Xilinx以二进制和源代码形式提供)将从固件/微码
.xsvf
文件中读取字节,使用(例如)fscanf(fi,"%c",&myint)
来获取字节。这太可怕了。我重构了该实用程序来修复这一问题以及状态机的处理,并将加载时间从15分钟减少到45秒。
希望Xilinx现在已经修复了这个实用程序。