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