2. NVMe PCI 端點功能目標

作者:

Damien Le Moal <dlemoal@kernel.org>

NVMe PCI 端點功能目標驅動程式透過配置為 PCI 傳輸型別的 NVMe Fabrics 目標控制器來實現 NVMe PCIe 控制器。

2.1. 概述

NVMe PCI 端點功能目標驅動程式允許透過 PCIe 鏈路暴露 NVMe 目標控制器,從而實現類似於常規 M.2 SSD 的 NVMe PCIe 裝置。目標控制器以與使用 NVMe over Fabrics 相同的方式建立:控制器透過一個埠表示與 NVMe 子系統的介面。埠傳輸型別必須配置為“pci”。子系統可以配置為具有由常規檔案或塊裝置支援的名稱空間,或者可以使用 NVMe 直通將現有物理 NVMe 裝置或 NVMe Fabrics 主機控制器(例如 NVMe TCP 主機控制器)暴露給 PCI 主機。

NVMe PCI 端點功能目標驅動程式儘可能依賴 NVMe 目標核心程式碼來解析和執行 PCIe 主機提交的 NVMe 命令。然而,透過使用 PCI 端點框架 API 和 DMA API,該驅動程式還負責管理 PCIe 鏈路上的所有資料傳輸。這意味著 NVMe PCI 端點功能目標驅動程式實現了若干 NVMe 資料結構管理和部分 NVMe 命令解析功能。

  1. 如果支援,驅動程式使用 DMA 管理提交佇列中 NVMe 命令的檢索,否則使用 MMIO。檢索到的每個命令都使用工作項執行,以透過在不同 CPU 上並行執行多個命令來最大化效能。驅動程式使用工作項持續輪詢所有提交佇列的門鈴,以檢測 PCIe 主機提交的命令。

  2. 驅動程式使用 MMIO 複製主機完成佇列中的條目,將已完成命令的完成佇列條目傳輸到 PCIe 主機。在完成佇列中釋出完成條目後,驅動程式使用 PCI 端點框架 API 向主機發起中斷,以訊號通知命令完成。

  3. 對於任何具有資料緩衝區的命令,NVMe PCI 端點目標驅動程式解析命令的 PRP 或 SGL 列表,以建立表示主機上命令資料緩衝區對映的 PCI 地址段列表。如果支援 DMA,命令資料緩衝區透過此 PCI 地址段列表在 PCIe 鏈路上進行傳輸。如果不支援 DMA,則使用 MMIO,這會導致效能不佳。對於寫命令,命令資料緩衝區在透過目標核心程式碼執行命令之前從主機傳輸到本地記憶體緩衝區。對於讀命令,分配一個本地記憶體緩衝區來執行命令,並且一旦命令完成,該緩衝區的內容就會傳輸到主機。

2.1.1. 控制器能力

透過 BAR 0 暫存器暴露給 PCIe 主機的 NVMe 能力與目標核心程式碼實現的 NVMe 目標控制器的能力幾乎相同。存在一些例外情況。

  1. NVMe PCI 端點目標驅動程式始終將控制器能力 CQR 位設定為請求“連續佇列(Contiguous Queues Required)”。這是為了方便將佇列 PCI 地址範圍對映到本地 CPU 地址空間。

  2. 門鈴步幅(DSTRB)始終設定為 4B

  3. 由於 PCI 端點框架不提供處理 PCI 級別復位的方法,因此控制器能力 NSSR 位(支援 NVM 子系統復位)始終被清除。

  4. 引導分割槽支援(BPS)、持久記憶體區域支援(PMRS)和控制器記憶體緩衝區支援(CMBS)能力從未報告。

2.1.2. 支援的功能

NVMe PCI 端點目標驅動程式實現了對 PRP 和 SGL 的支援。該驅動程式還實現了 IRQ 向量合併和提交佇列仲裁突發。

在啟動控制器之前,最大佇列數和最大資料傳輸大小(MDTS)可以透過 configfs 進行配置。為避免執行命令時本地記憶體使用過多的問題,MDTS 預設為 512 KB,並限制為最大 2 MB(任意限制)。

2.1.3. 所需的最小 PCI 地址對映視窗數量

大多數 PCI 端點控制器提供有限數量的對映視窗,用於將 PCI 地址範圍對映到本地 CPU 記憶體地址。NVMe PCI 端點目標控制器將對映視窗用於以下目的。

  1. 一個記憶體視窗,用於發起 MSI 或 MSI-X 中斷

  2. 一個記憶體視窗,用於 MMIO 傳輸

  3. 每個完成佇列一個記憶體視窗

鑑於 NVMe PCI 端點目標驅動程式操作的高度非同步性,上述記憶體視窗通常不會同時使用,但這種情況可能會發生。因此,可以支援的安全最大完成佇列數等於 PCI 端點控制器總記憶體對映視窗數減去二。例如,對於一個具有 32 個出站記憶體視窗的端點 PCI 控制器,可以安全地執行多達 30 個完成佇列,而不會有因缺少記憶體視窗而導致 PCI 地址對映錯誤的風險。

2.1.4. 最大佇列對數量

將 NVMe PCI 端點目標驅動程式繫結到 PCI 端點控制器後,將分配 BAR 0 以提供足夠的空間來容納管理佇列和多個 I/O 佇列。可支援的最大 I/O 佇列對數量受多種因素限制。

  1. NVMe 目標核心程式碼將最大 I/O 佇列數限制為線上 CPU 的數量。

  2. 佇列對的總數(包括管理佇列)不能超過可用的 MSI-X 或 MSI 向量數。

  3. 完成佇列的總數不得超過 PCI 對映視窗總數減去 2(參見上文)。

NVMe 端點功能驅動程式允許透過 configfs 配置最大佇列對數量。

2.1.5. 限制和不符合 NVMe 規範之處

與 NVMe 目標核心程式碼類似,NVMe PCI 端點目標驅動程式不支援多個提交佇列使用相同的完成佇列。所有提交佇列都必須指定一個唯一的完成佇列。

2.2. 使用者指南

本節描述了硬體要求以及如何設定 NVMe PCI 端點目標裝置。

2.2.1. 核心要求

核心必須在啟用配置選項 CONFIG_PCI_ENDPOINT、CONFIG_PCI_ENDPOINT_CONFIGFS 和 CONFIG_NVME_TARGET_PCI_EPF 的情況下編譯。CONFIG_PCI、CONFIG_BLK_DEV_NVME 和 CONFIG_NVME_TARGET 也必須啟用(顯然)。

除此之外,所使用的端點硬體應至少有一個可用的 PCI 端點控制器驅動程式。

為了方便測試,也建議啟用 null-blk 驅動程式 (CONFIG_BLK_DEV_NULL_BLK)。這樣,就可以使用以 null_blk 塊裝置作為子系統名稱空間的簡單設定。

2.2.2. 硬體要求

要使用 NVMe PCI 端點目標驅動程式,至少需要一個端點控制器裝置。

查詢系統中端點控制器裝置的列表

# ls /sys/class/pci_epc/
 a40000000.pcie-ep

如果 PCI_ENDPOINT_CONFIGFS 已啟用

# ls /sys/kernel/config/pci_ep/controllers
 a40000000.pcie-ep

端點板當然也必須透過一根 RX-TX 訊號互換的 PCI 線纜連線到主機。如果使用的主機 PCI 插槽不具備即插即用能力,則在配置 NVMe PCI 端點裝置時應關閉主機電源。

2.2.3. NVMe 端點裝置

建立 NVMe 端點裝置是一個兩步過程。首先,必須定義一個 NVMe 目標子系統和埠。其次,必須設定 NVMe PCI 端點裝置並將其繫結到建立的子系統和埠。

2.2.4. 建立 NVMe 子系統和埠

關於如何配置 NVMe 目標子系統和埠的詳細資訊超出了本文件的範圍。以下僅提供一個簡單的埠和子系統示例,其中包含一個由 null_blk 裝置支援的名稱空間。

首先,確保 configfs 已啟用

# mount -t configfs none /sys/kernel/config

接下來,建立一個 null_blk 裝置(預設設定會生成一個沒有記憶體支援的 250 GB 裝置)。預設情況下,建立的塊裝置將是 /dev/nullb0

# modprobe null_blk
# ls /dev/nullb0
/dev/nullb0

必須載入 NVMe PCI 端點功能目標驅動程式

# modprobe nvmet_pci_epf
# lsmod | grep nvmet
nvmet_pci_epf          32768  0
nvmet                 118784  1 nvmet_pci_epf
nvme_core             131072  2 nvmet_pci_epf,nvmet

現在,建立一個子系統和一個埠,我們將在設定 NVMe PCI 端點目標裝置時使用它們來建立 PCI 目標控制器。在此示例中,該埠建立時最大支援 4 個 I/O 佇列對

# cd /sys/kernel/config/nvmet/subsystems
# mkdir nvmepf.0.nqn
# echo -n "Linux-pci-epf" > nvmepf.0.nqn/attr_model
# echo "0x1b96" > nvmepf.0.nqn/attr_vendor_id
# echo "0x1b96" > nvmepf.0.nqn/attr_subsys_vendor_id
# echo 1 > nvmepf.0.nqn/attr_allow_any_host
# echo 4 > nvmepf.0.nqn/attr_qid_max

接下來,使用 null_blk 塊裝置建立並啟用子系統名稱空間

# mkdir nvmepf.0.nqn/namespaces/1
# echo -n "/dev/nullb0" > nvmepf.0.nqn/namespaces/1/device_path
# echo 1 > "nvmepf.0.nqn/namespaces/1/enable"

最後,建立目標埠並將其連結到子系統

# cd /sys/kernel/config/nvmet/ports
# mkdir 1
# echo -n "pci" > 1/addr_trtype
# ln -s /sys/kernel/config/nvmet/subsystems/nvmepf.0.nqn \
        /sys/kernel/config/nvmet/ports/1/subsystems/nvmepf.0.nqn

2.2.5. 建立 NVMe PCI 端點裝置

當 NVMe 目標子系統和埠準備就緒後,現在可以建立並啟用 NVMe PCI 端點裝置。NVMe PCI 端點目標驅動程式應該已經載入(在建立埠時會自動載入)

# ls /sys/kernel/config/pci_ep/functions
nvmet_pci_epf

接下來,建立功能 0

# cd /sys/kernel/config/pci_ep/functions/nvmet_pci_epf
# mkdir nvmepf.0
# ls nvmepf.0/
baseclass_code    msix_interrupts   secondary
cache_line_size   nvme              subclass_code
deviceid          primary           subsys_id
interrupt_pin     progif_code       subsys_vendor_id
msi_interrupts    revid             vendorid

使用任何裝置 ID 配置該功能(裝置的供應商 ID 將自動設定為與 NVMe 目標子系統供應商 ID 相同的值)

# cd /sys/kernel/config/pci_ep/functions/nvmet_pci_epf
# echo 0xBEEF > nvmepf.0/deviceid
# echo 32 > nvmepf.0/msix_interrupts

如果使用的 PCI 端點控制器不支援 MSI-X,則可以配置 MSI 作為替代

# echo 32 > nvmepf.0/msi_interrupts

接下來,讓我們將端點裝置與我們建立的目標子系統和埠繫結

# echo 1 > nvmepf.0/nvme/portid
# echo "nvmepf.0.nqn" > nvmepf.0/nvme/subsysnqn

然後可以將端點功能繫結到端點控制器,並啟動控制器

# cd /sys/kernel/config/pci_ep
# ln -s functions/nvmet_pci_epf/nvmepf.0 controllers/a40000000.pcie-ep/
# echo 1 > controllers/a40000000.pcie-ep/start

在端點機器上,當 NVMe 目標裝置和端點裝置建立並連線時,核心訊息將顯示相關資訊。

null_blk: disk nullb0 created
null_blk: module loaded
nvmet: adding nsid 1 to subsystem nvmepf.0.nqn
nvmet_pci_epf nvmet_pci_epf.0: PCI endpoint controller supports MSI-X, 32 vectors
nvmet: Created nvm controller 1 for subsystem nvmepf.0.nqn for NQN nqn.2014-08.org.nvmexpress:uuid:2ab90791-2246-4fbb-961d-4c3d5a5a0176.
nvmet_pci_epf nvmet_pci_epf.0: New PCI ctrl "nvmepf.0.nqn", 4 I/O queues, mdts 524288 B

2.2.6. PCI 根複合主機

啟動 PCI 主機將導致 PCIe 鏈路初始化(這可能由 PCI 端點驅動程式透過核心訊息發出訊號)。端點上的核心訊息也將訊號通知主機 NVMe 驅動程式何時啟用裝置控制器

nvmet_pci_epf nvmet_pci_epf.0: Enabling controller

在主機側,NVMe PCI 端點功能目標裝置將可被發現為 PCI 裝置,並具有配置的供應商 ID 和裝置 ID

# lspci -n
0000:01:00.0 0108: 1b96:beef

該裝置將被識別為一個具有單個名稱空間的 NVMe 裝置

# lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
nvme0n1     259:0    0   250G  0 disk

然後,NVMe 端點塊裝置可以像任何其他常規 NVMe 名稱空間塊裝置一樣使用。可以使用 *nvme* 命令列工具獲取有關端點裝置的更詳細資訊。

# nvme id-ctrl /dev/nvme0
NVME Identify Controller:
vid       : 0x1b96
ssvid     : 0x1b96
sn        : 94993c85650ef7bcd625
mn        : Linux-pci-epf
fr        : 6.13.0-r
rab       : 6
ieee      : 000000
cmic      : 0xb
mdts      : 7
cntlid    : 0x1
ver       : 0x20100
...

2.3. 端點繫結

NVMe PCI 端點目標驅動程式使用 PCI 端點 configfs 裝置屬性如下。

vendorid

忽略(使用 NVMe 目標子系統的供應商 ID)

deviceid

任何值都可以(例如 PCI_ANY_ID)

revid

無關緊要

progif_code

必須是 0x02 (NVM Express)

baseclass_code

必須是 0x01 (PCI_BASE_CLASS_STORAGE)

subclass_code

必須是 0x08 (Non-Volatile Memory controller)

cache_line_size

無關緊要

subsys_vendor_id

忽略(使用 NVMe 目標子系統的子系統供應商 ID)

subsys_id

任何值都可以(例如 PCI_ANY_ID)

msi_interrupts

至少等於所需的佇列對數量

msix_interrupts

至少等於所需的佇列對數量

interrupt_pin

如果不支援 MSI 和 MSI-X,則使用的中斷 PIN

NVMe PCI 端點目標功能還在功能目錄的 *nvme* 子目錄中定義了一些特定的可配置欄位。這些欄位如下。

mdts_kb

最大資料傳輸大小(單位:KiB)(預設:512)

portid

要使用的目標埠 ID

subsysnqn

要使用的目標子系統的 NQN