DMA 如何与 PCI Express 设备一起使用?

Posted

技术标签:

【中文标题】DMA 如何与 PCI Express 设备一起使用?【英文标题】:How does DMA work with PCI Express devices? 【发布时间】:2015-02-12 18:32:01 【问题描述】:

假设 CPU 想要从 PCI Express 设备进行 DMA 读取传输。与 PCI Express 设备的通信由事务层数据包 (TLP) 提供。理论上,TLP 的最大有效载荷大小为 1024 个双字。那么当 CPU 向 4 兆字节大小的 PCI Express 设备发出 DMA 读取命令时,DMA 控制器是如何工作的呢?

【问题讨论】:

【参考方案1】:

在 PCIe 枚举阶段,确定允许的最大负载大小(它可以低于设备的最大负载大小:例如,中间 PCIe 交换机的最大负载大小较小)。

大多数 PCIe 设备都是 DMA 主设备,因此驱动程序将命令传输到设备。设备将发送多个写入数据包以传输 xx 个最大 TLP 块中的 4 MiB。

编辑1回复评论1:

基于 PCI 的总线在芯片组中没有芯片或子电路形式的“DMA 控制器”。总线上的每个设备都可以成为总线主机。主存始终是从属。

假设您已经构建了自己的 PCIe 设备卡,它可以充当 PCI 主设备,并且您的程序(在 CPU 上运行)想要将数据从该卡发送到主内存 (4 MiB)。

设备驱动程序从操作系统知道特定内存区域的内存映射(一些关键字:内存映射 I/O、PCI 总线枚举、PCI BAR、)。

驱动程序将命令(写入)、源地址、目标地址和长度传输到设备。这可以通过将字节发送到预定义 BAR 内的特殊地址或写入 PCI 配置空间来完成。卡上的 DMA 主机检查这些特殊区域是否有新任务(分散-收集列表)。如果是这样,这些任务就会排队。

现在 DMA 主机知道要发送到哪里,发送多少数据。他将从本地内存中读取数据并将其包装成最大有效负载大小的 512 字节 TLP(路径设备 主内存上的最大有效负载大小可从枚举中获知)并将其发送到目标地址。基于 PCI 地址的路由机制将这些 TLP 引导到主存储器。

【讨论】:

tnanks 回答,你说过; “该设备将发送几个写入数据包以传输 xx 最大 TLP 块中的 4 MiB。”但是这个过程将如何完成,我的意思是假设 pcie 设备的最大有效负载大小为 512 字节,当它成为 DMA 的总线主控器时,它将这些 TLP 数据包发送到 dma 控制器或主存储器?如果 asnwer 是主内存,设备和内存之间的接口在哪里?最后设备本身如何知道在发送 TLP 数据包时要等待多长时间? @spartacus 我扩展了我对您的评论问题的回答。 我的理解是:一旦PCIe设备(端点)被Bios固件(MMIO)分配到主机(CPU)地址空间中的内存地址,这些分配的地址就会写入PCIe的BAR设备。然后,当主机写入映射地址空间中的寄存器时,PCIe(类似于 DMA)将写入的数据传输到端点中相同的等效地址。这种理解正确吗?【参考方案2】:

我缺少内存管理方面。总线主机使用物理地址将数据以块的形式发送到内存,该地址以前由软件使用操作系统的 API 解析。但是 4 MB 的大小跨越了大量的 MMU 页面,并且 MMU 是 CPU 的一部分。不在驱动内存和 PCIe 的芯片组中。 所以,我不相信,这是完整的故事。恐怕,每个块都必须单独处理。

【讨论】:

【参考方案3】:

@Paebbels 已经解释了大部分内容。在 PCI/PCI-e 中,“DMA”是在总线主控方面实现的,它是具有总线主控功能的外围设备掌握着控制权。外围设备拥有可供其支配的内存读/写事务,这取决于外围设备,它将使用什么粒度和写入(或读取)的顺序。 IE。精确的实现细节是特定于外围设备的硬件,并且在主机 CPU 上运行的相应软件驱动程序必须知道如何操作特定的外围设备,以在其中激发所需的 DMA 流量。

关于“内存管理方面”,让我向尊敬的听众推荐 Jon Corbet 的一本精巧的书 two chapters,这正是 Linux 中的这个主题。内存管理与 DMA 接壤,位于 OS 内核的底层。 Linux 及其源代码和文档通常是开始寻找“事物如何在幕后工作”的好地方(开源)。我会试着总结一下主题。

首先,请注意,对主机 RAM 的 DMA 访问(从外围 PCI 设备)与 PCI MMIO 是不同的事情 = 外围设备拥有自己的私有 RAM 库,想要做到这一点可通过 MMIO BAR 提供给主机系统。这与 DMA 不同,一种不同的机制(虽然不完全),或者如果你愿意的话,可能是“相反的观点”……假设 PCI/PCI-e 上的主机和外围设备之间的差异不是很大,并且主机桥/根复合体仅在树形拓扑、总线初始化等方面具有某种特殊作用:-) 我希望我已经把你弄糊涂了。

包含 PCI(-e) 总线树和现代主机 CPU 的计算机系统实际上与多个“地址空间”一起工作。您可能听说过 CPU 的物理地址空间(在 CPU 内核、RAM 控制器和 PCI 根桥之间的“前端总线”上)与由操作系统在帮助下管理的“虚拟地址空间”部分 CPU 支持个别用户空间进程(包括内核本身的一个这样的虚拟空间,与物理地址空间不同)。这两个地址空间,物理地址空间和多种虚拟地址空间,与 PCI(-e) 总线无关。而且,你猜怎么着:PCI(-e) 总线有自己的地址空间,称为“总线空间”。请注意,还有所谓的“PCI 配置空间”= 另一个并行地址空间。现在让我们从 PCI 配置空间中抽象出来,因为无论如何访问它是间接且复杂的 = 不会“妨碍”我们的主题。

所以我们有三个不同的地址空间(或类别):物理地址空间、虚拟空间和 PCI(-e) 总线空间。这些需要相互“映射”。地址需要翻译。内核中的虚拟内存管理子系统使用它的页表和一些 x86 硬件魔法(关键字:MMU)来完成它的工作:从虚拟地址转换为物理地址。当与 PCI(-e) 设备,或者更确切地说是它们的“内存映射 IO”,或者在使用 DMA 时,地址需要在 CPU 物理地址空间和 PCI(-e) 总线空间之间进行转换。在硬件中,在总线事务中,PCI(-e) 根联合体的工作是处理有效负载流量,包括地址转换。在软件方面,内核向驱动程序提供功能(作为其内部 API 的一部分),以便能够在需要的地方转换地址。尽管该软件只关心其各自的虚拟地址空间,但在与 PCI(-e) 外围设备通信时,它需要使用来自“总线空间”的地址为 DMA 编程它们的“基地址寄存器”,因为那是PCI(-e) 外围设备上线。外围设备不会主动与我们玩“多地址转换游戏”……这取决于软件,或者特别是操作系统,使 PCI(-e) 总线空间分配成为主机 CPU 物理地址的一部分空间,并使 PCI 设备可以访问主机物理空间。 (虽然不是典型的场景,但主机甚至可以有多个 PCI(-e) 根联合体,托管多棵 PCI(-e) 总线树。它们的地址空间分配不得在主机 CPU 物理地址空间中重叠。)

有一个捷径,虽然不完全是:在 x86 PC 中,PCI(-e) 地址空间和主机 CPU 物理地址空间合而为一。 不确定这是否在硬件中硬连线(根复合体只是没有任何特定的映射/翻译能力),或者这是否是“事情发生的方式”,在 BIOS/UEFI 和 Linux 中。我只想说这恰好是这种情况。 但是,与此同时,这并没有让 Linux 驱动程序编写者的生活变得更轻松。 Linux 可以在各种硬件平台上运行,它确实有一个用于转换地址的 API,并且在地址空间之间交叉时必须使用该 API。

也许有趣的是,在 PCI(-e) 驱动程序和 DMA 的上下文中,API 简写是“bus_to_virt()”和“virt_to_bus()”。因为,对于软件而言,重要的是其各自的虚拟地址——那么为什么要强迫驱动程序作者转换(并跟踪)虚拟、物理和总线地址空间,使事情变得复杂,对吧?还有用于分配内存以供 DMA 使用的简写:pci_alloc_consistent() 和 pci_map_single() - 以及它们的释放对应物,以及几个同伴 - 如果有兴趣,你真的应该参考 Jon Corbet 的书和进一步的文档(和内核源代码)。

因此,作为驱动程序作者,您分配一块 RAM 供 DMA 使用,获得各自“虚拟”风格的指针(一些内核空间),然后将该指针转换为 PCI“总线”空间,然后您可以将其引用到您的 PCI(-e) 外围设备 =“这是您可以上传输入数据的地方”。

然后您可以指示外围设备在分配的内存窗口中执行 DMA 事务。 RAM 中的 DMA 窗口可以大于(通常是)“最大 PCI-e 事务大小”——这意味着外围设备需要发出几个连续事务来完成整个分配窗口的传输(可能或可能不需要,具体取决于您的应用程序)。 如何分片传输的组织方式,具体取决于您的 PCI 外围硬件和软件驱动程序。外围设备可以只使用一个已知整数计数的连续偏移量。或者它可以使用链表。该列表可以动态增长。您可以通过一些 BAR 将列表提供给外围设备,或者您可以使用第二个 DMA 窗口(或单个窗口的子部分)在 RAM 中构建链接列表,外围 PCI 设备将沿着该链运行。这就是 scatter-gather DMA 在实际的当代 PCI-e 设备中的工作原理。

外围设备可以使用 IRQ 发送完成或其他一些事件的信号。一般而言,涉及 DMA 的外围设备的操作将是对 BAR 的直接轮询访问、DMA 传输和 IRQ 信令的混合。

正如您可能已经推断的那样,在进行 DMA 时,外围设备不一定需要在板上拥有一个私有缓冲区,该缓冲区与主机 RAM 中的 DMA 窗口分配一样大。恰恰相反 - 如果应用程序适合,外设可以轻松地将数据从(或到)一个字长 (32b/64b) 的内部寄存器或价值单个“PCI-e 有效负载大小”的缓冲区“流式传输”对于这种安排。或者一个很小的双缓冲或类似的。或者外设确实可以有一个巨大的私有 RAM 来启动 DMA - 如果不需要/不需要从总线直接访问 MMIO,那么这样的私有 RAM 不需要映射到 BAR(!)。

请注意,外设可以轻松地将 DMA 启动到另一个外设的 MMIO BAR,因为它可以 DMA 将数据传输到主机 RAM 或从主机 RAM 传输数据。即,给定一条 PCI 总线,两个外围设备实际上可以直接相互发送数据,而无需使用主机“前端总线”上的带宽(或现在的任何东西,在 PCI 根复合体的北部:快速路径,环面,你的名字它)。

在 PCI 总线初始化期间,BIOS/UEFI 或操作系统将总线地址空间(和物理地址空间)的窗口分配给 PCI 总线段和外围设备 - 以满足 BAR 对地址空间的渴望,同时保持分配非全系统重叠。各个 PCI 桥(包括主桥/根复合体)被配置为“解码”它们各自分配的空间,但对于不属于它们自己的地址“保持高阻抗”(静默)。随意在“正解码”与“减法解码”上自行搜索 Google,其中一条通过 PCI(-e) 总线的特定路径可以变成“最后的地址接收器”,可能仅适用于旧版 ISA 等。

另一个切线提示可能是:如果您从未在驱动程序中编写过简单的 MMIO,即使用 PCI 设备提供的 BAR,请知道相关关键字(API 调用)是 ioremap()(及其对应的 iounmap,在驱动程序上)卸下)。这就是你如何让你的 BAR 在你的驱动程序中以内存式访问方式访问。

并且:您可以通过调用 mmap() 使映射的 MMIO 条或 DMA 窗口直接对用户空间进程可用。因此,您的用户空间进程可以直接访问该内存窗口,而无需通过 ioctl() 的昂贵且间接的兔子洞。

嗯。取模 PCI 总线延迟和带宽、可缓存属性等。

我觉得这是我在引擎盖下过于深入的地方,并且已经失去动力......欢迎更正。

【讨论】:

以上是关于DMA 如何与 PCI Express 设备一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

将 Linux IOMMU API 与用户空间地址一起使用

第二十七篇:Windows驱动中的PCI, DMA, ISR, DPC, ScatterGater, MapRegsiter, CommonBuffer, ConfigSpace

PCI9054进行DMA操作时,如何设置FIFO?

pcie与DMA求助

使 Angular 路由与 Express 路由一起工作

PCI千兆网卡和PCI Express千兆网卡