PCI 对等 DMA 支持

PCI 总线对于在总线上的两个设备之间执行 DMA 传输提供了相当不错的支持。这种类型的事务此后称为对等 (Peer-to-Peer)(或 P2P)。然而,有许多问题使得 P2P 事务难以以完全安全的方式完成。

最大的问题之一是 PCI 不要求在层次结构域之间转发事务,而在 PCIe 中,每个根端口定义一个单独的层次结构域。更糟糕的是,没有简单的方法来确定给定的根复合体是否支持此功能。(请参阅 PCIe r4.0,第 1.3.1 节)。因此,在撰写本文时,内核仅支持当所涉及的端点都位于同一个 PCI 桥接器后面时执行 P2P,因为这样的设备都位于同一个 PCI 层次结构域中,并且规范保证层次结构内的所有事务都可路由,但不要求在层次结构之间进行路由。

第二个问题是,为了利用 Linux 中现有的接口,用于 P2P 事务的内存需要由 struct pages 支持。然而,PCI BAR 通常不是缓存一致的,因此这些页面存在一些边缘情况,因此开发人员需要小心处理它们。

驱动程序编写者指南

在给定的 P2P 实现中,可能涉及三种或更多不同类型的内核驱动程序

  • 提供者 - 一个驱动程序,它向其他驱动程序提供或发布 P2P 资源,如内存或门铃寄存器。

  • 客户端 - 一个驱动程序,通过设置到或来自它的 DMA 事务来利用资源。

  • 编排器 - 一个驱动程序,它编排客户端和提供者之间的数据流。

在许多情况下,这三种类型之间可能存在重叠(即,驱动程序同时作为提供者和客户端是很典型的)。

例如,在 NVMe 目标复制卸载实现中

  • NVMe PCI 驱动程序既是客户端、提供者又是编排器,因为它将任何 CMB(控制器内存缓冲区)公开为 P2P 内存资源(提供者),它接受 P2P 内存页面作为请求中的缓冲区以直接使用(客户端),并且它还可以将 CMB 用作提交队列条目(编排器)。

  • RDMA 驱动程序在此安排中是一个客户端,以便 RNIC 可以直接 DMA 到 NVMe 设备公开的内存。

  • NVMe 目标驱动程序 (nvmet) 可以将数据从 RNIC 编排到 P2P 内存 (CMB),然后再到 NVMe 设备(反之亦然)。

这是当前内核支持的唯一安排,但人们可以想象对它进行轻微的调整,这将允许相同的功能。例如,如果特定的 RNIC 添加了一个带有某些内存的 BAR,则其驱动程序可以添加对 P2P 提供者的支持,然后 NVMe 目标可以在正在使用的 NVMe 卡不支持 CMB 的情况下使用 RNIC 的内存,而不是 CMB。

提供者驱动程序

提供者只需使用 pci_p2pdma_add_resource() 将 BAR(或 BAR 的一部分)注册为 P2P DMA 资源即可。这将为所有指定的内存注册 struct pages。

之后,它可以选择使用 pci_p2pmem_publish() 将其所有资源发布为 P2P 内存。这将允许任何编排器驱动程序查找和使用该内存。以这种方式标记时,资源必须是没有任何副作用的常规内存。

目前,这相当基础,因为所有资源通常都是 P2P 内存。未来的工作可能会将其扩展到包括其他类型的资源,如门铃。

客户端驱动程序

客户端驱动程序只需像往常一样使用映射 API dma_map_sg()dma_unmap_sg() 函数,并且实现将为支持 P2P 的内存执行正确的操作。

编排器驱动程序

编排器驱动程序必须做的第一件事是编译将在给定事务中涉及的所有客户端设备的列表。例如,NVMe 目标驱动程序创建一个列表,其中包括命名空间块设备和正在使用的 RNIC。如果编排器有权访问要使用的特定 P2P 提供者,它可以使用 pci_p2pdma_distance() 检查兼容性,否则它可以使用 pci_p2pmem_find() 找到与所有客户端兼容的内存提供者。如果支持多个提供者,则将首先选择距离所有客户端最近的提供者。如果多个提供者的距离相等,则将随机选择返回的提供者(它不是任意的,而是真正随机的)。此函数返回用于提供者的 PCI 设备,并获取引用,因此当不再需要时,应使用 pci_dev_put() 返回。

选择提供者后,编排器可以使用 pci_alloc_p2pmem()pci_free_p2pmem() 从提供者分配 P2P 内存。pci_p2pmem_alloc_sgl()pci_p2pmem_free_sgl() 是用于分配带有 P2P 内存的散布/收集列表的便捷函数。

Struct Page 注意事项

驱动程序编写者应非常小心,不要将这些特殊的 struct pages 传递给没有为此做好准备的代码。目前,内核接口没有任何检查来确保这一点。这显然排除了将这些页面传递给用户空间的可能性。

P2P 内存在技术上也是 IO 内存,但不应在其后面有任何副作用。因此,加载和存储的顺序不应重要,并且不需要 ioreadX()、iowriteX() 和类似函数。

P2P DMA 支持库

int pci_p2pdma_add_resource(struct pci_dev *pdev, int bar, size_t size, u64 offset)

添加内存以用作 p2p 内存

参数

struct pci_dev *pdev

要向其添加内存的设备

int bar

要添加的 PCI BAR

size_t size

要添加的内存大小,可以为零以使用整个 BAR

u64 offset

PCI BAR 中的偏移量

描述

内存将获得 ZONE_DEVICE struct pages,以便它可以与任何 DMA 请求一起使用。

int pci_p2pdma_distance_many(struct pci_dev *provider, struct device **clients, int num_clients, bool verbose)

确定 p2pdma 提供者和正在使用的客户端之间的累积距离。

参数

struct pci_dev *provider

要对照客户端列表检查的 p2pdma 提供者

struct device **clients

要检查的设备数组 (NULL 终止)

int num_clients

数组中的客户端数量

bool verbose

如果为 true,则当我们返回 -1 时,为设备打印警告

描述

如果任何客户端不兼容,则返回 -1,否则返回一个正数,其中较小的数字是首选选择。(如果有一个客户端与提供者相同,则它将返回 0,这是最佳选择)。

“兼容”意味着提供者和客户端要么都位于同一个 PCI 根端口后面,要么连接到每个设备的主机桥列在“pci_p2pdma_whitelist”中。

bool pci_has_p2pmem(struct pci_dev *pdev)

检查给定的 PCI 设备是否已发布任何 p2pmem

参数

struct pci_dev *pdev

要检查的 PCI 设备

struct pci_dev *pci_p2pmem_find_many(struct device **clients, int num_clients)

查找与指定客户端列表兼容且距离最短的对等 DMA 内存设备。

参数

struct device **clients

要检查的设备数组 (NULL 终止)

int num_clients

列表中客户端设备的数量。

描述

如果多个设备位于同一个交换机后面,则会优先选择“最接近”正在使用的客户端设备的设备。(因此,如果提供者之一与客户端之一相同,则将优先使用该提供者,而不是任何其他不相关的提供者)。如果多个提供者距离相等,则会随机选择一个。

返回指向 PCI 设备的指针,并已获取引用(使用 pci_dev_put 返回引用),如果未找到兼容的设备,则返回 NULL。找到的提供者也将被分配给客户端列表。

void *pci_alloc_p2pmem(struct pci_dev *pdev, size_t size)

分配对等 DMA 内存。

参数

struct pci_dev *pdev

从中分配内存的设备。

size_t size

要分配的字节数。

描述

返回分配的内存,如果出错则返回 NULL。

void pci_free_p2pmem(struct pci_dev *pdev, void *addr, size_t size)

释放对等 DMA 内存。

参数

struct pci_dev *pdev

从中分配内存的设备。

void *addr

已分配内存的地址。

size_t size

已分配的字节数。

pci_bus_addr_t pci_p2pmem_virt_to_bus(struct pci_dev *pdev, void *addr)

返回使用 pci_alloc_p2pmem() 获取的给定虚拟地址的 PCI 总线地址。

参数

struct pci_dev *pdev

从中分配内存的设备。

void *addr

已分配内存的地址。

struct scatterlist *pci_p2pmem_alloc_sgl(struct pci_dev *pdev, unsigned int *nents, u32 length)

在散列表(scatterlist)中分配对等 DMA 内存。

参数

struct pci_dev *pdev

从中分配内存的设备。

unsigned int *nents

列表中 SG 条目的数量。

u32 length

要分配的字节数。

返回

如果出错,则返回 NULL;如果成功,则返回 struct scatterlist 指针和 **nents**。

void pci_p2pmem_free_sgl(struct pci_dev *pdev, struct scatterlist *sgl)

释放由 pci_p2pmem_alloc_sgl() 分配的散列表。

参数

struct pci_dev *pdev

从中分配内存的设备。

struct scatterlist *sgl

已分配的散列表。

void pci_p2pmem_publish(struct pci_dev *pdev, bool publish)

发布对等 DMA 内存,以便其他设备可以通过 pci_p2pmem_find() 使用。

参数

struct pci_dev *pdev

具有要发布的对等 DMA 内存的设备。

bool publish

设置为 true 以发布内存,设置为 false 以取消发布内存。

描述

已发布的内存可供其他 PCI 设备驱动程序用于对等 DMA 操作。未发布的内存保留供注册对等内存的设备驱动程序独占使用。

int pci_p2pdma_enable_store(const char *page, struct pci_dev **p2p_dev, bool *use_p2pdma)

解析 configfs/sysfs 属性存储以启用 p2pdma。

参数

const char *page

要存储的值的内容。

struct pci_dev **p2p_dev

返回选择要使用的 PCI 设备(如果在存储的值中指定了设备)。

bool *use_p2pdma

返回是否启用 p2pdma。

描述

解析属性值以决定是否启用 p2pdma。该值可以选择一个 PCI 设备(使用其完整的 BDF 设备名称)或一个布尔值(采用 kstrtobool() 接受的任何格式)。false 值禁用 p2pdma,true 值期望调用者自动找到兼容的设备,而指定 PCI 设备则期望调用者使用特定的提供者。

pci_p2pdma_enable_show() 应用作该属性的显示操作。

成功返回 0。

ssize_t pci_p2pdma_enable_show(char *page, struct pci_dev *p2p_dev, bool use_p2pdma)

显示一个 configfs/sysfs 属性,指示是否启用了 p2pdma。

参数

char *page

存储的值的内容。

struct pci_dev *p2p_dev

选定的 p2p 设备(如果未选择任何设备,则为 NULL)。

bool use_p2pdma

是否已启用 p2pdma。

描述

使用 pci_p2pdma_enable_store() 的属性应使用此函数来显示属性的值。

成功返回 0。