PCI 对等 DMA 支持

PCI 总线对于在总线上的两个设备之间执行 DMA 传输具有相当不错的支持。此类型的事务此后称为对等 (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 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)

在分散列表中分配对等 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() 应该用作属性的 show 操作。

成功时返回 0

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

显示指示是否启用了 p2pdma 的 configfs/sysfs 属性

参数

char *page

存储的值的内容

struct pci_dev *p2p_dev

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

bool use_p2pdma

是否已启用 p2pdma

说明

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

成功时返回 0