/dev/mem 外部内核 ram 上的 Memcpy 性能

Memcpy performance on /dev/mem outside kernel ram

我正在使用带有自定义 linux 的 SoC。我通过指定内核引导参数 mem=512M 保留了 1GB 总 RAM 的上部 512MB。 我可以通过打开 /dev/mem 和 mmap 内核未使用的上层 512MB 来从用户空间程序访问上层内存。 知道我想通过 memcpy() 复制该区域内的大块内存,但性能约为 50MB/sek。当我通过内核和它们之间的 memcpy 分配缓冲区时,我可以达到大约 500MB/sek。 我很确定是因为我的特殊内存区域的缓存被禁用但不知道如何告诉内核在这里使用缓存。

有人知道如何解决这个问题吗?

注意:其中很多都是我的热门评论,所以我会尽量避免逐字重复。

关于 DMA、内核访问和用户space 访问的缓冲区。可以通过任何合适的机制分配缓冲区。

如前所述,在用户 space 中使用 mem=512M/dev/mem 以及 mmapmem 驱动程序可能无法设置最佳缓存策略。此外,mem=512M 更通常用于告诉内核 从不 使用内存(例如,我们想用更少的系统内存进行测试)并且我们 不会将上面的512M用于任何事情。

更好的方法可能是放弃 mem=512M 并使用您提到的 CMA。另一种方法可能是将驱动程序绑定到内核并让它在系统启动期间保留完整的内存块[可能使用 CMA]。

可以通过内核命令行参数[来自grub.cfg]选择内存区域,例如mydev.area=mydev.size=。这对于必须在系统启动的 "early" 阶段知道这些值的 "bound" 驱动程序很有用。

所以,现在我们有了 "big" 区域。现在,我们需要有一种方法让设备获得访问权限,并让应用程序对其进行映射。内核驱动程序可以做到这一点。打开设备后,ioctl 可以使用正确的内核策略设置映射。

因此,根据分配机制,ioctl 可以由应用程序提供 address/length 可以将它们传递回应用程序[适当映射].

当我必须这样做时,我创建了一个描述内存的结构 area/buffer。可以是整个区域,也可以是大区域,可以根据需要进行细分。与其使用等同于 malloc [就像你所写的那样] 的可变长度动态方案,我发现固定大小的子池效果更好。在内核中,这称为 "slab" 分配器。

该结构具有给定区域的 "id" 编号。它还具有三个地址:应用程序可以使用的地址、内核驱动程序可以使用的地址以及将提供给 H/W 设备的地址。此外,在多个设备的情况下,它可能具有当前关联的特定设备的 ID。

所以,你把大面积再细分成这样。 5 台设备。 Dev0 需要 10 个 1K 缓冲区,Dev1 需要 10 个 20K 缓冲区,Dev3 需要 10 个 2K 缓冲区,...

应用程序和内核驱动程序将保留这些描述符结构的列表。该应用程序将使用另一个 ioctl 启动 DMA,该 ioctl 将采用描述符 ID 号。对所有设备重复此操作。

然后应用程序可以发出等待完成的 ioctl。驱动程序填写刚刚完成的操作的描述符。该应用程序处理数据和循环。它这样做 "in-place"-- 见下文。

您担心 memcpy 速度慢。正如我们所讨论的,这可能是由于您在 /dev/mem.

上使用 mmap 的方式所致

但是,如果您要从设备 DMA 访问内存,CPU 缓存可能会过时,因此您必须考虑到这一点。真正的设备驱动程序有很多内核支持例程来处理这个问题。

这里有一个大问题:为什么你需要在全部做一个memcpy?如果设置正确,应用程序可以直接对数据进行操作 而无需 复制它。也就是说,DMA 操作将数据准确地放在应用程序需要的地方。

据推测,现在,您已经在设备上获得了 memcpy "racing"。也就是说,您必须快速复制数据,以便您可以在不丢失任何数据的情况下启动下一个 DMA。

"big"区域应该被细分[如上所述]并且内核驱动程序应该知道这些部分。因此,驱动程序将 DMA 启动到 id 0。完成后,它立即 [在 ISR 中]启动 DMA 到 id 1。完成后,它会进入其子池中的下一个。对于每个设备,这可以以类似的方式完成。应用程序可以使用 ioctl

轮询完成

这样,驱动程序可以使所有设备 运行 保持最大速度,并且应用程序可以有足够的时间来处理给定的缓冲区。而且,再一次,它不需要复制它。

另外说一下。您设备上的 DMA 寄存器是否双缓冲?我假设您的设备不支持复杂的 scatter/gather 列表并且相对简单。

在我的特殊情况下,在 H/W 的 rev 1 中,DMA 寄存器 不是 双缓冲的。

因此,在缓冲区 0 上启动 DMA 后,驱动程序必须等到缓冲区 0 的完成中断,然后才能将 DMA 寄存器设置为下一个 t运行sfer 到缓冲区 1。因此,驱动程序必须 "race" 为下一个 DMA 进行设置 [并且 window 的时间很短]。启动缓冲区 0 后,如果驱动程序更改了设备上的 DMA 寄存器,则会中断已经激活的请求。

我们在第 2 版中使用双缓冲修复了此问题。当驱动程序设置 DMA regs 时,它会命中 "start" 端口。所有 DMA 端口立即被设备锁存。此时,驱动程序可以自由地为缓冲区 1 进行完整设置,并且当缓冲区 0 完成时,设备将自动切换到缓冲区 1 [无需驱动程序干预]。驱动程序会收到中断,但可能会花费几乎整个 t运行sfer 时间来设置下一个请求。

因此,对于 rev 1 样式系统,uio 方法可能 不会 起作用——它会太慢。对于 rev 2,uio 可能是可能的,但我不是粉丝,即使它是可能的。

注意:在我的例子中,我们没有使用read(2)write(2)到设备read/write 回调。一切都是通过特殊的 ioctl 调用处理的,这些调用采用各种结构,如上面提到的那样。在早期,我们确实使用read/write的方式类似于uio使用它们的方式。但是,我们发现映射是人为的和有限制的 [而且很麻烦],因此我们转换为 "only ioctl" 方法。

更重要的是,有什么要求?每秒传输的数据量 t运行。设备数量是做什么用的?都是输入还是也有输出?

在我的案例中[对广播质量 hidef H.264 视频进行了 R/T 处理],我们能够在驱动程序 应用程序中进行处理 space 以及自定义 FPGA 逻辑。但是,我们使用了完整的 [非 uio] 驱动程序方法,尽管在架构上它在某些地方看起来像 uio。

我们对可靠性、R/T可预测性、保证运行延迟有严格的要求。我们必须每秒处理 60 个视频帧。如果我们 运行 结束,即使是一小部分,我们的客户也会开始尖叫。 uio 不可能为我们做到这一点。

所以,您是从一个简单的方法开始的。但是,我可能会退后一步,看看需求、设备 capabilities/restrictions、获取连续缓冲区的替代方法、R/T 吞吐量和延迟,然后重新评估。您当前的解决方案是否真正满足了所有需求?目前,您已经 运行 进入热点 [应用程序和设备之间的数据竞争] and/or 限制。或者,你会更好地使用本机驱动程序来为你提供更大的灵活性(即可能有一个尚未知会强制使用本机驱动程序)。

Xilinx 可能在他们的 SDK 中提供了一个合适的框架完整驱动程序,您可以很快地破解它。

非常感谢您抽出宝贵时间。你的回答对我很有用。我喜欢从驱动程序本身管理缓冲区(dma 缓冲区)的想法。

正如您在 /dev/mem 的源代码中所阐明的,当我在 mem=512M 从内核中排除的区域上使用 mmap 时,内核威胁如下设备内存并禁用缓存。

我找到了一个中间解决方案。我删除了内核引导参数并在我的设备树中添加了一个保留内存区域,如下所示:

/ { 
    reserved-memory {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        my_reserved: databuffer@20000000 {
            reg = <0x20000000 0x20000000>;
        };
    };
};

这样做,我从 cat /proc/iomem 获得了 0x00000000 - 0x3fffffff 的系统内存。 cat /proc/meminfo 给我的空闲内存只有 500 MB,所以我的区域没有被使用。

当我现在打开 /dev/memmmap 这个区域时,我从 [= 得到了大约 260 MB/sek 26=]memcpy() 和来自 memset() 的大约 1200 MB/sek。该区域被视为内存并被缓存。我仍然不知道为什么它的性能只有 malloc 区域的一半,但要好得多。

我认为对于我的案例来说,完美的解决方案应该是更好的 /dev/mem,比如 /dev/cma 设备驱动程序,它从我在 bootargs 中定义的 cma 区域分配缓冲区。 在该设备上,我可以通过 ioctl() 设置缓存、一致性策略等内容。这将使我有机会在该区域自行设置偏好。

我发现了关于那个问题的有趣帖子,其他人是如何解决它的。 Continous memory on ARM and cache coherency

前面会介绍一个我为了和你一样的目的而制作的设备驱动程序。请参考

https://github.com/ikwzm/udmabuf

udmabuf(用户 space 可映射 DMA 缓冲区)

概览

udmabuf介绍

udmabuf 是一个 Linux 设备驱动程序,它在内核 space 中分配连续的内存块作为 DMA 缓冲区,并使它们可供用户使用 space。当用户应用程序使用 UIO(用户 space I/O)在用户 space 中实现设备驱动程序时,这些内存块旨在用作 DMA 缓冲区。

用户 space 可以通过打开设备文件(例如 /dev/udmabuf0)并映射到用户内存 space 或使用读()/写()函数。

CPU 分配的 DMA 缓冲区的缓存可以通过在打开设备文件时设置 O_SYNC 标志来禁用。也可以刷新或使 CPU 缓存无效,同时保留 CPU 缓存启用。

udmabuf分配的一个DMA buffer的物理地址可以通过读/sys/class/udmabuf/udmabuf0/phys_addr得到。

可以在加载设备驱动程序时指定 DMA 缓冲区的大小和设备次设备号(例如,通过 insmod 命令加载时)。一些平台允许在设备树中指定它们。

udmabuf 的架构

Figure 1. Architecture

图 1. 架构


支持的平台

  • OS : Linux 内核版本 3.6 - 3.8、3.18、4.4
    (作者在3.18和4.4上测试过)
  • CPU:ARM Cortex-A9(Xilinx ZYNQ / Altera CycloneV SoC)