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