DOCA NVMe 模拟应用指南
本文档提供了在 NVIDIA® BlueField® DPU 之上的 NVMe 模拟实现。
NVMe 模拟应用程序展示了如何使用 DOCA DevEmu PCI 通用 API 以及 SPDK 来模拟 NVMe PCIe 功能,利用硬件加速完全模拟存储设备。
NVMe 简要介绍
NVMe(非易失性内存高速接口)是一种高性能存储协议,专为访问非易失性存储介质而设计。NVMe 通过 PCI Express 总线运行,这在 CPU 和存储之间提供了直接且高速的连接。与较旧的存储协议相比,它能够实现显著更低的延迟和更高的每秒输入/输出操作次数。NVMe 通过其可扩展的多队列架构实现这一点,允许并行处理数千个 I/O 命令。
NVMe 模拟是一种机制,允许在虚拟化或开发环境中模拟 NVMe 设备行为,而无需物理 NVMe 硬件。
控制器寄存器
NVMe 控制器具有多个内存映射寄存器,这些寄存器位于基地址寄存器 (BAR) 定义的内存区域内。这些寄存器传递控制器的状态,使主机能够配置操作设置,并促进错误报告。
关键 NVMe 控制器寄存器包括
CC(控制器配置)– 配置控制器的操作参数,包括启用或禁用它以及指定 I/O 命令集。
CSTS(控制器状态)– 报告控制器的状态,包括其就绪状态和任何致命错误。
CAP(能力)– 详细说明控制器的能力。
VS(版本)– 指示支持的 NVMe 版本。
AQA(管理队列属性)– 指定管理队列的大小。
初始化
初始化 NVMe 控制器涉及配置控制器寄存器并准备系统以与 NVMe 设备通信。该过程通常遵循以下步骤
主机清除 CC 寄存器中的使能位(将其设置为 0)以重置控制器。
主机通过在 CC 寄存器中设置初始参数来配置控制器,并通过设置使能位来启用它。
主机设置管理提交队列和完成队列,用于处理管理命令。
发出管理命令以检索命名空间信息并执行其他设置任务。
主机创建 I/O 提交队列和完成队列,为 I/O 操作准备控制器。
重置和关机
重置和关机操作确保控制器得到妥善处理,并维护数据完整性。
重置过程将 CC 寄存器中的使能位设置为 0,停止所有 I/O 操作并清除控制器的状态,以确保其返回到已知状态。
关机通过设置 CC 寄存器中的 关机通知 (SHN) 字段来启动。这允许控制器停止操作、刷新缓存,并确保在断电或重置之前安全处理正在进行的数据。
完成队列 (CQ)
NVMe 中的完成队列存储控制器在处理命令后写入的条目。CQ 中的每个条目都对应于通过提交队列提交的命令,主机检查 CQ 以跟踪这些命令的状态。
CQ 作为主机内存中的循环缓冲区实现。主机可以轮询 CQ 或使用中断来获得新条目可用的通知。
完成队列元素 (CQE)
NVMe 完成队列 (CQ) 中的每个完成队列元素都是一个单独的条目,其中包含有关已完成命令的状态信息,包括
CID – 已完成命令的标识符。
SQID – 发出命令的提交队列的 ID。
SQHD – 标记提交队列中已完成命令的点。
SF – 指示已完成命令的状态。
P – 标记完成条目是否为新条目。
提交队列
提交队列 (SQ) 是主机放置管理和 I/O 命令以供控制器执行的地方。它作为循环缓冲区运行,每个 SQ 都与一个完成队列 (CQ) 配对。NVMe 支持多个 SQ,每个 SQ 都分配给特定的 CQ。
通过门铃机制通知控制器新命令。
提交队列元素 (SQE)
提交队列元素 (SQE) 是提交队列中的单个条目,表示要由 NVMe 控制器处理的命令。每个 SQE 包含
CID – 命令的唯一标识符
OPC – 指定要执行的操作的操作码
PSDT – 指定是使用 PRP(物理区域页)还是 SGL(分散-聚集列表)进行与命令关联的数据传输
NSID – 命令的目标命名空间的标识符
此外,SQE 还包括特定于命令的详细信息的字段,例如逻辑块地址和传输大小。
管理队列对 (Admin QP)
管理队列对由管理提交队列 (SQ) 和管理完成队列 (CQ) 组成,两者都分配了命令标识符 (CID) 0。每个 NVMe 控制器只有一个管理队列对 (QP),它在初始化阶段异步创建。与 I/O 队列不同,此 QP 专门用于控制器管理任务,从而促进管理命令的处理。
管理命令
识别命令 (
SPDK_NVME_OPC_IDENTIFY 0x06
)
此命令允许主机从 NVMe 控制器查询信息,检索描述 NVMe 子系统、控制器或命名空间属性的数据缓冲区。此信息对于主机软件正确配置和有效利用存储设备至关重要。
创建 I/O 提交队列 (
SPDK_NVME_OPC_CREATE_IO_SQ 0x01
)
此命令允许主机指示 NVMe 控制器建立新的队列,用于提交 I/O 命令。
关键参数
SQID – 标识正在创建的特定 I/O 提交队列。
队列深度 – 指定队列可以容纳的条目数。
CQID – 关联的完成队列的标识符。
一旦主机发送带有必要参数的命令,控制器就会分配所需的资源,并返回指示成功或失败的状态。
删除 I/O 提交队列 (
SPDK_NVME_OPC_DELETE_IO_SQ 0x00
)
此命令用于在不再需要 I/O 提交队列时将其删除。
关键参数
SQID – 要删除的 I/O 提交队列的标识符。
一旦主机发出命令,控制器就会释放与队列关联的所有资源,并返回确认删除的状态。队列删除后,无法再向其提交额外的 I/O 命令。
创建 I/O 完成队列 (
SPDK_NVME_OPC_CREATE_IO_CQ 0x05
)
此命令由主机发出,用于在 NVMe 控制器中设置 I/O 完成队列。
关键参数
CQID – 要创建的 I/O 完成队列的标识符。
队列深度 – 完成队列可以容纳的条目数。
PRP1/PRP2 – 指向存储 CQ 条目的内存位置的指针。
MSIX – 与此 CQ 关联的中断向量。
一旦主机发出命令,控制器就会分配必要的资源,将 CQ 链接到指定的中断向量,并返回确认创建的状态。
删除 I/O 完成队列 (
SPDK_NVME_OPC_DELETE_IO_CQ 0x04
)
此命令由主机发出,用于从 NVMe 控制器中删除现有的 I/O 完成队列。
关键参数
CQID – 要删除的 I/O 完成队列的标识符。
收到此命令后,NVMe 控制器会删除指定的 CQ 并释放所有关联的资源。在删除 CQ 之前,链接到它的所有 SQ 都必须删除或重新分配给另一个 CQ。控制器返回一个状态代码,指示删除是否成功或是否发生错误。删除后,CQ 不再处理来自任何链接 SQ 的完成条目。
获取特性 (
SPDK_NVME_OPC_GET_FEATURES 0x0A
)此命令由主机发出,用于查询 NVMe 控制器支持的特定特性。
关键参数
FID(特性 ID)– 指定主机要检索的特性。控制器根据请求的特性返回信息。常见特性包括
仲裁 (FID 0x01)
电源管理 (FID 0x02)
温度阈值 (FID 0x04)
错误恢复 (FID 0x05)
易失性写入缓存 (FID 0x06)
队列数量 (FID 0x07)
中断合并 (FID 0x08)
根据 FID 的不同,特性信息可能会在完成队列条目 (CQE) 中返回,或者写入到主机内存中的输出缓冲区。如果使用输出缓冲区,主机将提供一个内存区域,控制器通过 PRP 条目或 SGL 访问该区域。
选择 – 确定要返回的特性值的版本。有四个选项
当前 (0x0) – 返回特性的活动值
默认 (0x1) – 返回特性的默认值
已保存 (0x2) – 返回来自非易失性内存的已保存值
支持的能力 (0x3) – 返回控制器针对该特性支持的能力
执行命令后,控制器在 CQE 中返回状态代码,指示查询是否成功。
设置特性 (
SPDK_NVME_OPC_SET_FEATURES 0x09
)此命令由主机发出,用于修改 NVMe 控制器上的特定特性。
关键参数
FID(特性 ID):指定主机打算修改的特性。常见特性包括
仲裁 (FID 0x01)
电源管理 (FID 0x02)
温度阈值 (FID 0x04)
错误恢复 (FID 0x05)
易失性写入缓存 (FID 0x06)
队列数量 (FID 0x07)
中断合并 (FID 0x08)
数据位置:根据 FID 的不同,新值可以直接在 SQE 中提供,也可以存储在主机内存中的输入缓冲区中,控制器可以通过 PRP 或 SGL 访问该缓冲区。
保存:此字段允许主机指定修改是否应在控制器重置后仍然存在,如果设置,则修改后的值将保存在控制器非易失性内存中。
发出命令并且控制器按请求修改特性后,它会在 CQE 中返回状态代码,指示修改是否成功或是否发生错误。
日志页 (
SPDK_NVME_OPC_GET_LOG_PAGE 0x02
)此命令由主机发出,用于从 NVMe 控制器检索各种类型的日志页,以监控和诊断 NVMe 设备的状态。
关键参数
LID:日志页标识符,指定要检索的日志页类型。一些常见的日志页包括
SMART / 健康信息 (LID 0x02):提供设备健康指标、温度、可用备用空间等。
错误信息 (LID 0x01):包含有关控制器遇到的错误的详细信息。
固件插槽信息 (LID 0x03):有关固件插槽和活动固件的信息。
遥测主机发起 (LID 0x07):包含有关设备性能的遥测数据。
NUMD:要返回的日志数据 DWORD 的数量。这允许部分或全页检索。
日志页数据位置:检索到的日志数据被写入到主机提供的输出缓冲区中。控制器可以通过 PRP 或 SGL 访问该缓冲区。
当主机发出获取日志命令时,控制器会检索请求的日志信息并将其写入主机提供的内存中。然后,控制器在 CQE 中返回状态代码,指示操作的成功或失败。
输入/输出队列对 (I/O QP)
一个 I/O 队列对由一个提交队列及其对应的完成队列组成,它们用于执行数据传输(I/O 操作)。可以创建多个 I/O 队列对以实现并行 I/O 操作。每个队列对独立运行,最大限度地利用多核处理器。
I/O 提交队列:主机放置读取/写入/刷新命令的位置
I/O 完成队列:控制器在处理命令后发布完成条目的位置
NVM 命令
刷新 (
SPDK_NVME_OPC_FLUSH 0x00
)
NVMe 刷新命令由主机发出,以确保驻留在易失性内存中的任何数据都安全地写入永久存储。如果不存在或未启用易失性写入缓存,则刷新命令将成功完成,而不会产生任何影响。刷新操作完成后,控制器将使用完成条目更新关联的 I/O 完成队列。
读取 (
SPDK_NVME_OPC_READ 0x02
)
NVMe 读取命令是 NVMe 中的核心 I/O 操作之一。此命令由主机发出,用于从指定的命名空间检索数据并将其传输到主机内存。
关键参数
NSID – 从中读取数据的命名空间的标识符。
LBA – 要读取的数据在命名空间内的起始地址。
NLB(LBA 数量)– 以要读取的逻辑块数量指定读取操作的大小。
目标缓冲区 – 控制器从命名空间获取读取数据,并将其发送到主机内存,其中使用 PRP 或 SGL 指定目标缓冲区。
完成后,控制器将完成条目发布到 I/O 完成队列,其中包括指示成功或失败的状态代码。
写入 (
SPDK_NVME_OPC_WRITE 0x01
)
写入命令也是 NVMe 中的核心 I/O 操作之一。此命令由主机发出,用于将数据写入给定逻辑块地址 (LBA) 的特定命名空间。
关键参数
NSID – 正在写入数据的命名空间的标识符
LBA – 数据写入的命名空间内的目标地址
NLB – 以要写入的逻辑块数量指定写入操作的大小
源缓冲区 – 要写入的数据位于主机内存中,控制器从源缓冲区读取此数据,该源缓冲区使用 PRP 或 SGL 提供。
完成后,控制器将完成条目发布到 I/O 完成队列,其中包括指示写入操作成功或失败的状态代码。
SPDK 简要介绍
存储性能开发工具包 (SPDK) 是一个开源框架,提供用于构建高性能、可扩展存储解决方案的工具和库,特别是对于 NVMe 设备。SPDK 允许应用程序通过绕过内核并使用用户空间驱动程序来实现 NVMe over Fabrics (NVMe-oF) 的传输协议,从而实现与存储硬件的直接交互。这种方法显著降低了延迟和开销,使其成为要求苛刻的存储环境的理想选择。
SPDK 的一个关键组件是其高度优化的 NVMe 驱动程序,该驱动程序完全在用户空间中运行。通过允许与 NVMe 设备直接通信而无需涉及内核,此驱动程序最大限度地减少了 I/O 延迟并提高了性能,同时支持本地 NVMe 存储和通过 NVMe-oF 的远程 NVMe 设备。
SPDK 线程模型
SPDK 的线程模型专为高并发、可扩展性和低延迟 I/O 处理而设计。它基于协作式多任务模型运行,其中 SPDK 线程被分配给轮询器,任务完全在用户空间中执行,而无需内核参与。每个 SPDK 线程都在专用 CPU 核心上运行,从而最大限度地减少上下文切换,并在非抢占式环境中实现对工作负载的严格控制。
Reactor 线程
此模型的核心是 Reactor 线程,它是 SPDK 的主要执行线程,负责处理 I/O 处理和应用程序逻辑。每个 Reactor 线程都绑定到特定的核心,并在轮询模式下运行,这意味着它持续轮询任务和 I/O 请求,而不是依赖中断。
示例
struct spdk_thread *thread = spdk_get_thread();
此函数检索当前 SPDK 线程,该线程映射到特定的核心。
SPDK 提供了向 Reactor 线程注册轮询器的能力,轮询器是旨在完成异步 I/O 操作、管理 RPC 服务器或执行自定义操作的定期函数。一旦轮询器完成操作,它可以触发用户定义的回调以完成任务。
注册轮询器的示例
struct spdk_poller *my_poller = spdk_poller_register(my_poll_function, arg, poll_interval_us);
在此示例中,Reactor 线程重复调用 my_poll_function
。
轮询组
轮询组由多个 SPDK 线程协同工作组成,以管理跨多个设备或连接的 I/O。轮询组允许一组线程协调和处理共享工作负载,确保在可用核心之间高效分配任务,并最大限度地减少延迟。

线程同步
在 SPDK 的协作线程模型中,线程同步被设计为高效且最小化,因为线程不会像传统的内核线程那样经历抢占式上下文切换。这允许对任务的执行时间进行细粒度控制。但是,在某些情况下,线程之间的协调变得必要,例如在处理共享资源或在核心之间传递任务时。
SPDK 没有依赖传统的锁定机制(由于争用,这可能会引入性能瓶颈),而是使用消息传递作为线程通信的主要方法。这涉及通过无锁事件环在线程之间发送事件或任务,这允许在没有与锁相关的开销的情况下进行协调。
在线程之间发送消息的示例
void
send_message(struct spdk_thread *target_thread, spdk_msg_fn fn, void
*arg) {
spdk_thread_send_msg(target_thread, fn, arg);
}
target thread
是我们要向其发送消息的 SPDK,fn
是要在目标线程的上下文中执行的函数,而 arg
是传递给该函数的参数。
块设备
SPDK 提供了一个灵活的系统,用于处理不同类型的存储设备,例如 NVMe SSD、虚拟块设备、AIO 设备和 RAM 磁盘。它使用高性能、用户空间 API,让应用程序绕过操作系统的内核,从而减少延迟并提高性能。
SPDK 的块设备 (bdev) 层为应用程序提供了一种统一的方式来对这些设备执行读取和写入操作。它开箱即用地支持流行的设备,并允许用户创建自定义块设备。还支持 RAID 等高级功能以及 DPDK 等技术的加速。
可以使用 SPDK 的 RPC 服务器轻松创建或销毁块设备。在 NVMe-oF 环境中,块设备表示命名空间,这有助于管理跨不同系统的存储。这使得 SPDK 成为构建快速、可扩展存储解决方案的理想选择。

NVMe-oF 目标
NVMe-oF 目标是 SPDK 框架内的用户空间应用程序,它通过网络结构(如以太网、InfiniBand 或光纤通道)公开块设备。它通常使用 TCP、RDMA 或 vfio-user 等传输协议,使客户端能够访问远程存储。
要使用 NVMe-oF 目标,需要将其配置为与以下传输协议之一配合使用
TCP
RDMA(通过 InfiniBand 或 RoCE)
vfio-user(主要用于虚拟机)
FC-NVMe(光纤通道,在 SPDK 环境中不太常见)
NVMe-oF 传输
每个 NVMe-oF 传输都在促进 NVMe-oF 发起程序(客户端)和目标之间的通信方面发挥着关键作用。传输处理 NVMe 命令如何通过网络结构传输。例如
TCP 使用基于 IP 的以太网寻址。
RDMA 利用 InfiniBand 或 RoCE 的低延迟、高吞吐量特性。
vfio-user 提供虚拟化支持,允许虚拟机访问 NVMe 设备。
FC-NVMe 使用光纤通道,通常在企业 SAN 环境中找到。
此外,SPDK 设计为灵活的,允许开发人员创建自定义传输并扩展 NVMFS 目标的功能,使其超出 SPDK 提供的标准传输。
NVMe-oF 传输负责
建立发起程序和目标之间的连接。
将网络层命令转换为 SPDK NVMe 命令。
管理跨网络结构的数据传输。
一旦通过传输建立连接,NVMe-oF 目标就会处理发起程序发送的 NVMe 命令。
应用层
在传输层之上是 SPDK 应用层,它是传输无关的。这意味着,无论使用何种传输(TCP、RDMA 等),应用层都统一处理 NVMe 命令。它负责
管理子系统、命名空间和控制器。
处理通过网络接收的 NVMe 命令。
将这些命令映射到适当的存储设备(例如 NVMe SSD 或虚拟设备,如 SPDK 的
malloc
或null
设备)。
这种统一的应用层确保传输层与相同的逻辑交互,以处理和响应 NVMe 命令,而无需考虑底层网络结构。

SPDK 中的 NVMe 驱动程序
在 NVMe 存储的上下文中,发起程序是需要访问 NVMe 存储设备以发送数据或从 NVMe 存储设备检索数据的主机系统,驱动程序负责生成和发送适当的命令,例如读取和写入。驱动程序使用传输与 NVMe 目标进行通信。在远程目标的情况下,传输通过网络发送这些命令,或者在本地目标的情况下,直接通过 PCIe 总线发送到本地 NVMe 设备。目标从发起程序接收这些请求,处理它们,并使用数据或完成状态进行响应。

RPC 服务器
SPDK 的远程过程调用 (RPC) 服务器为客户端提供了一个灵活的接口,用于与各种 SPDK 服务(如 NVMe-oF、块设备 (bdev) 和其他存储子系统)进行交互。服务器基于 JSON-RPC,并在 SPDK 应用程序中运行,允许外部客户端动态配置、控制和管理 SPDK 组件,而无需重新启动应用程序。通过 RPC 请求,用户可以创建、删除或查询子系统,配置网络存储层,以及管理 NVMe-oF 目标。RPC 服务器的主要功能是处理通常为 JSON 格式的传入 RPC 命令,执行基于 RPC 请求的函数,并将结果返回给客户端。
RPC 服务器在 SPDK 应用程序中运行。
RPC 客户端
SPDK 中的 RPC 客户端与 RPC 服务器交互,以发出用于配置和管理各种 SPDK 子系统的命令。它发送命令(通常为 JSON 格式),要求服务器执行特定任务或检索数据。RPC 客户端的主要功能是构造它希望服务器处理的请求消息,将请求发送到 RPC 服务器,并接收来自服务器的响应。
RPC 客户端在用户或应用程序端运行。
传输 RPC
SPDK 提供了几个传输 RPC,用于配置和管理 NVMe-oF 的传输层。以下是主要的传输 RPC
nvmf_create_transport
nvmf_get_transports
nvmf_subsystem_add_listener
nvmf_get_subsystems
nvmf_delete_listener
nvmf_get_stats
nvmf_delete_transport
块设备 RPC
SPDK 提供了几个传输 RPC,用于配置和管理 NVMe-oF 的传输层。以下是主要的传输 RPC
nvmf_create_transport
nvmf_get_transports
nvmf_subsystem_add_listener
nvmf_get_subsystems
nvmf_delete_listener
nvmf_get_stats
nvmf_delete_transport
命名空间 RPC
命名空间 RPC 用于管理 NVMe-oF 子系统或 NVMe 控制器中的 NVMe 命名空间,以下是可用的命名空间 RPC 列表
nvmf_subsystem_add_ns
nvmf_subsystem_remove_ns
nvmf_subsystem_get_ns
nvmf_subsystem_get_ns_stats
nvmf_subsystem_get_namespaces
解决方案概述
与 SPDK 集成
使用 DOCA 通用 PCI 模拟 SDK,BlueField DPU 可以通过 PCIe 端点模拟 NVMe 设备。这允许 DPU 作为物理 NVMe 设备出现在主机系统上,使主机能够发送 NVMe 命令。虽然 DPU 硬件可以处理数据移动和基本 I/O 任务,但管理完整的 NVMe 协议(包括管理命令、队列管理和寄存器操作)需要额外的软件支持。这就是 SPDK 的用武之地。
借助 SPDK,DPU 可以卸载 NVMe 命令的复杂处理,使其成为功能齐全且高性能的 NVMe 设备,而无需为每个命令开发自定义固件。
DOCA 通用设备模拟作为 NVMe-oF 传输

虽然 NVMe-oF 专为 TCP 或 RDMA 等远程传输而设计,但 SPDK 使我们能够通过添加基于内存的传输将 PCIe 视为另一种传输选项。这允许 DPU 像与远程 NVMe-oF 目标通信一样运行,即使它位于主机系统本地。
为了实现这一点,我们使用 DOCA 传输,这是一种自定义传输层,充当连接隧道并为 NVMe-oF 提供通用模拟功能。通过利用 SPDK 的 NVMe-oF RPC,我们可以创建一个有效模拟 NVMe 设备的 DPU。DOCA 传输确保 NVMe 命令的高效路由和处理,而 SPDK 负责基于软件的模拟。
(该应用程序利用 NVMe-oF 应用程序传输层来实现 NVMe 模拟解决方案,灵感来自 SPDK 博客文章。)
模拟功能作为 NVMe 控制器
在 DOCA 传输中,NVMe 控制器映射到称为模拟管理器的 PCIe DOCA 设备。在此上下文中,模拟管理器充当硬件接口,提供对 NVMe 控制器的访问,NVMe 控制器通过特定的 PCIe 寄存器和内存映射区域公开其功能。
当将设备连接到控制器时,传输负责通过 Connect 命令提供控制器的唯一 ID,如 NVMe-oF 协议中所指定。
为了使核心 NVMe-oF 目标逻辑与我们的 DOCA 传输一起工作,我们需要在 spdk_nvmf_transport_ops
结构中实现特定的操作。这些操作处理连接管理、数据传输和 DOCA 的 NVMe 命令处理等任务。此结构提供了一种将不同传输连接到 SPDK 的标准方法,因此核心 NVMe-oF 逻辑可以与任何传输一起工作,而无需了解其具体细节。
const
struct spdk_nvmf_transport_ops spdk_nvmf_transport_doca = {
.name = "DOCA"
,
.type = SPDK_NVME_TRANSPORT_CUSTOM,
.opts_init = nvmf_doca_opts_init,
.create = nvmf_doca_create,
.dump_opts = nvmf_doca_dump_opts,
.destroy = nvmf_doca_destroy,
.listen = nvmf_doca_listen,
.stop_listen = nvmf_doca_stop_listen,
.listen_associate = nvmf_doca_listen_associate,
.poll_group_create = nvmf_doca_poll_group_create,
.get_optimal_poll_group = nvmf_doca_get_optimal_poll_group,
.poll_group_destroy = nvmf_doca_poll_group_destroy,
.poll_group_add = nvmf_doca_poll_group_add,
.poll_group_remove = nvmf_doca_poll_group_remove,
.poll_group_poll = nvmf_doca_poll_group_poll,
.req_free = nvmf_doca_req_free,
.req_complete = nvmf_doca_req_complete,
.qpair_fini = nvmf_doca_close_qpair,
.qpair_get_listen_trid = nvmf_doca_qpair_get_listen_trid,
};
新的 SPDK RPC
由于 DOCA 传输需要现有 SPDK RPC 未涵盖的特定配置,并且与其他传输(例如管理模拟管理器)不同,我们需要实现自定义 RPC 以向用户公开这些选项
RPC | 描述 | 详情 | 参数 | 输出 | 示例 |
| 提供列出所有模拟管理器的能力,模拟管理器等效于 DOCA 设备 | 返回具有管理功能的所有可用本地 DOCA 设备的名称。 | 无。 | 如果成功,RPC 返回模拟管理器的设备名称列表。如果失败,则返回错误代码。 |
|
| 提供在指定的设备名称下创建模拟功能的能力。 | 创建新的表示设备,检索其 VUID,然后关闭设备。 |
| 如果成功,RPC 返回新创建的功能的 VUID。如果失败,则返回错误代码。 |
|
| 提供销毁模拟功能的能力。 | 销毁 DOCA 设备表示。 |
| 成功时,RPC 不返回任何内容。失败时,它返回一个错误代码。 |
|
| 列出指定设备名称下的所有模拟函数。 | 列出所有可用的表示设备。 |
| 如果成功,RPC 将返回一个列表,其中包含指定设备名称下所有模拟函数的 VUID 和 PCIe 地址。如果失败,它将返回一个错误代码。 |
|
/usr/bin/spdk_rpc.py
是将 RPC 命令发送到 SPDK 的 Python 脚本。spdk_rpc.py
脚本负责通过 JSON-RPC 接口处理 SPDK 命令。
扩展 RPC
一些现有的 SPDK RPC 需要修改,因为默认 RPC 不支持 DOCA 传输的某些配置或功能
RPC | 描述 | 详情 | 参数 | 输出 | 示例 |
| 通过定义传输类型和配置参数来创建新的 NVMe-oF 传输,允许 SPDK 目标使用指定的传输与主机通信。 | 创建 DOCA 传输及其资源。 |
| 无 |
|
| 向 NVMe-oF 子系统添加监听器,使其能够接受通过指定传输的连接。 | 热插拔设备,允许主机将其作为 NVMe 设备进行交互。 |
| 无 |
|
/usr/bin/spdk_rpc.py
是将 RPC 命令发送到 SPDK 的 Python 脚本。spdk_rpc.py
脚本负责通过 JSON-RPC 接口处理 SPDK 命令。
数据结构
为了实现上述 spdk_transport_ops
的 API,我们创建了特定于传输的数据结构,可以有效地与这些 API 交互。这些结构旨在管理传输的状态、连接和操作。
上层代表主机,其次是 DPU,再往下是 DPA,它是 DPU 的一部分。在 DPU 内部,NVMe-oF 应用程序正在运行,分为两个部分:NVMe-oF 库(我们将其用作黑盒)和 DOCA 传输(我们实现的部分)。
在 DOCA 传输中,有多个轮询组,每个轮询组代表一个线程。除了轮询组列表之外,传输还维护一个模拟管理器列表,其中包含此传输管理的所有设备。还有一个特殊的轮询组实例,专门用于轮询 PCIe 事件。
如果我们深入研究轮询组,会发现有两个进度引擎:一个轮询 I/O 队列,另一个处理管理队列。此外,还有一个 PCIe 设备轮询组列表,每个轮询组管理特定设备的完成队列、提交队列和主机内存映射。

以下章节提供有关每个主要结构的更多信息。
模拟管理器
要解决的第一个结构是模拟管理器上下文。在初始化期间,DOCA 传输扫描所有可作为模拟管理器的可用 DOCA 设备。对于每个此类设备,它创建一个 PCIe 类型,初始化一个 DPA 实例,分配一个 DPA 应用程序,并启动 DPA 进程。它还打开 DOCA 设备(模拟管理器)。所有这些设备都存储在模拟管理器上下文中,只要传输处于活动状态,就会对其进行跟踪。

PCIe 设备管理
这是模拟设备的表示器,它包含以下内容

指向此设备所属的 DOCA 传输的指针。
先前描述的模拟管理器
模拟的 PCIe 设备
指向此设备所属的 SPDK 子系统的指针
此设备所属的传输 ID
指向 SPDK NVMe-oF 控制器的指针
状态区域值,在每次查询后更新
管理队列对上下文
管理队列对轮询组,用于管理管理 QP,它是使用轮询调度方法从系统中选择的
FLR 标志,指示是否发生了 FLR 事件
销毁标志,指示是否应销毁 PCIe 设备
一旦用户发出添加监听器 RPC,就会建立此上下文,以方便将模拟设备热插拔到主机,使其能够开始监听来自主机的交互。
DOCA 轮询组
SPDK 中的每个轮询组都与在特定 CPU 核心上运行的线程相关联。对于每个核心,一个 reactor 线程负责执行轮询器,而轮询组是这些轮询器之一。创建 NVMe-oF 时,会将其分配给每个轮询组,以便传输可以处理跨多个 CPU 核心的设备的 I/O 和连接管理,因此每个传输在每个轮询组中都有一个代表。
DOCA 轮询组字段

SPDK NVMe-oF 传输轮询组 (
struct spdk_nvmf_transport_poll_group
):指的是 NVMe-oF 子系统的一部分结构,负责处理轮询组级别的特定于传输的 I/O 操作。进度引擎 (
struct doca_pe
):当每个轮询组运行其轮询器集时,它还会调用 DOCA 进度引擎来管理传输操作。在 DOCA 侧,调用doca_pe_progress
函数来驱动每个轮询组内的进度引擎。这就是 DOCA 的 PE 如何集成到 SPDK 的轮询组机制中的方式。管理 QP 进度引擎 (
struct doca_pe
):另一个进度引擎 (struct doca_pe
) 专用于处理管理队列,而前一个引擎专用于处理 I/O 队列。这种分离可以更好地控制每种队列类型的轮询速率,这有助于优化性能。管理 QP 轮询速率和管理 QP 速率限制器:它们决定了系统检查管理队列中是否有新命令的频率。
PCIe 设备轮询组列表:此轮询组通常轮询其队列的设备列表。
管理轮询组
此对象是每个传输实体,充当轮询 PCIe 事件和管理管理队列活动的专用单元。

进度引擎 – 轮询器使用的 DOCA。
SPDK 轮询器 – 此轮询器用于 SPDK 应用程序线程。它持续监控 PCIe 事件,例如 FLR、状态区域和热插拔事件。
SPDK 线程 – 与当前正在执行的 SPDK 线程关联的应用程序线程。
PCIe 管理员 – 所有 PCIe 设备管理员的列表。
DOCA 传输
此结构保存传输的整体状态和配置,因为它包括

SPDK NVMe-oF 传输 – 定义 SPDK NVMe-oF 框架内的传输层。它保存用于管理传输的基本数据,例如配置参数、操作状态和连接。
模拟管理器 – 包括此传输管理的所有设备
轮询组 – 包含为此传输主动轮询的所有轮询组
上次选择的轮询组 – 用于辅助轮询组的轮询调度选择,确保在传输操作期间跨轮询组均匀分配工作负载
管理轮询组 – 如前所述
监听器数量 – 此传输中的设备数量
PCIe 设备轮询组
根据先前对传输结构的描述,下图说明了 PCIe 设备与传输内轮询组之间的关系。每个 PCIe 设备都包含 I/O 队列和管理队列,它们分布在各个轮询组中。

此关系由一个名为 PCIe 设备轮询组 (struct nvmf_doca_pci_dev_poll_group
) 的结构管理,该结构保存设备的内存映射 (mmap)、PCIe 设备的管理详细信息、管理 QP(如果适用)、I/O 队列列表以及负责轮询这些队列的轮询组。

当为特定设备和轮询组创建新的 I/O 或管理队列时,我们首先检查是否已存在链接这两者的 PCIe 设备轮询组结构。如果不存在,我们创建一个新的 struct nvmf_doca_pci_dev_poll_group
来组合它们。
管理 QP
此结构管理特定设备的队列。它保存一个管理完成队列和一个管理提交队列,以处理管理命令操作。此外,它还包括 I/O 完成队列和 I/O 提交队列的列表,用于管理数据相关操作。该结构还包含一个标志 stopping_all_io_cqs
,指示是否应停止所有完成队列,用于在需要时优雅地停止设备的队列处理。

DOCA IO
I/O 结构负责管理 I/O 操作,包括接收完成队列 (CQ) 及其关联的提交队列 (SQ) 上的门铃。它处理从主机读取 SQ 条目 (SQE)、将 SQE 写回主机以及引发 MSI-X 中断。此结构包含单个 CQ,以及 DPA 引发的 MSI-X 向量索引,以及 DPA 线程轮询的门铃完成。

它还保存了多个回调
Post CQE – 一旦 CQE 发布到主机 CQ,则调用,之后释放资源
Fetch CQE – 当从主机获取 SQE 时调用,解析和执行请求
Copy data – 在数据复制到主机后调用,完成请求并释放资源
Stop SQ – 当管理或 I/O SQ 停止时调用,以完全释放资源
Stop IO – 当管理或 I/O CQ 停止时调用,以完成资源清理
函数 nvmf_doca_io_create
同步创建模拟 I/O。
DOCA CQ
CQ 的主要任务是将 cookie 写入主机。主要字段是

如果是管理 CQ,则 CQ ID 为零。
DOCA 队列是一个共享结构,它充当完成队列(管理和 I/O CQ)和提交队列(管理和 I/O SQ),镜像主机队列,大小相同。它负责从主机获取提交队列条目 (SQE) 并将完成队列条目 (CQE) 发布回主机。NVMe 驱动程序通过其提交队列发出命令,并通过完成队列接收其完成。为了方便 DPU 进行高效处理,DOCA 队列利用 DMA 来处理主机和 DPU 之间双向的数据传输。每个队列都配备一个指向 DMA 结构的指针,该结构包含这些操作所需的资源。在初始化期间,DMA 资源和本地缓冲区根据队列的大小进行分配。DOCA 队列还维护一个任务数组,索引 idx 处的每个任务都与主机队列中相同索引处的任务相对应并同步。当需要 DMA 操作时,这些资源将用于数据传输。下面概述了它的主要字段

缓冲区清单用于分配队列元素
DMA 上下文处理与主机之间的数据传输
队列 MMAP 表示本地内存,元素存储在此处以进行复制
用于将元素复制到主机或从主机复制元素的本地地址
元素本身是用于复制/写入的 DMA 任务
队列可以容纳的最大元素数
DOCA Comch
全双工通信通道用于促进 DPA 和 DPU 之间双向的消息传递。此通道包含在 DOCA IO 中,由两个关键组件组成
带有生产者完成 (
doca_dpa_completion
) 上下文的发送消息队列 (nvmf_doca_dpa_msgq
)。带有消费者完成 (
doca_comch_consumer_completion
) 上下文的接收消息队列 (nvmf_doca_dpa_msgq
)。

DOCA DPA 线程
除了通信通道之外,I/O 结构还包括一个 DPA 线程上下文(每个线程专用于单个 CQ)。它由以下组成
指向 DPA 的指针
指向 DPA 线程的指针
DPA 线程的必要参数,包括
消费者完成:DPA 线程持续轮询以检测来自主机的新消息。
生产者完成:监控以验证消息是否已成功发送。
DB 完成上下文:提供门铃值并连接到 CQ 和 SQ 门铃。
MSIX:允许设备向主机发送中断。

DPA 线程执行三个主要操作
nvmf_doca_dpa_thread_create
– 通过提供 DOCA DPA、DPA 句柄和要传递给 DPA 的参数大小来创建 DPA 线程。它还在设备上分配内存。nvmf_doca_dpa_thread_run
– 将参数复制到 DPA 并运行线程。nvmf_doca_dpa_thread_destroy
– 删除线程并释放已分配的参数。
DOCA SQ
构成 DOCA 提交队列 (nvmf_doca_sq
) 的主要字段包括

对 DOCA I/O 的引用,此提交队列属于该 I/O,其完成发布在该 I/O 中。多个 SQ 可以属于单个 I/O。
先前描述的 DOCA 队列,用于处理从主机复制提交队列条目 (SQE)。
DMA 池 (
nvmf_doca_dma_pool
) 数据复制操作池(稍后定义)。与此提交队列关联的门铃。
门铃的 DPA 句柄。
提交队列标识符 (SQID)。
提交队列的状态,用于监控目的。
NVMe-oF 请求池内存(稍后定义)。
NVMe-oF DOCA 空闲请求列表,每当收到新的 SQE 并且需要准备请求时使用。
为此提交队列创建的队列对 (QP) 由 NVMe-oF 目标创建,用于执行命令。
提交队列的创建是异步的,因为当我们创建完成队列 (CQ) 时,我们也会初始化 DPA。但是,在创建 SQ 时,DPA 已经在运行,因此我们需要更新 DPA 关于新添加的 SQ。直接修改其状态可能会导致同步问题,这就是我们使用通信通道的原因,从而使该过程异步。
DOCA DMA 池
如前所述,提交队列 (SQ) 包括 nvmf_doca_dma_pool
结构,该结构管理主机和 DPU 之间双向的数据传输操作。它由以下元素组成

为本地数据缓冲区分配的内存
本地数据缓冲区的内存映射区域
本地数据缓冲区池
提供对主机数据缓冲区访问权限的内存映射
用于分配主机数据缓冲区的清单
用于在主机和 DPU 之间传输数据的 DMA 上下文
每当创建 SQ 时,都会初始化此结构。本地数据内存的大小由 DMA 复制操作的最大数量乘以每个 DMA 复制操作的最大大小(以字节为单位)来确定。所有这些本地缓冲区都在 SQ 创建期间分配。DMA 上执行的关键操作是
nvmf_doca_sq_get_dpu_buffer
– 检索 DPU 内存中的缓冲区,从而实现主机和 DPU 之间的数据传输。nvmf_doca_sq_get_host_buffer
– 检索指向主机内存的缓冲区,也用于主机和 DPU 之间的数据传输。nvmf_doca_sq_copy_data
– 在主机和 DPU 之间复制数据。此操作是异步的,完成后,它会调用nvmf_doca_io::copy_data_cb
回调函数。
DOCA NVMe-oF 请求
NVMe-oF 目标利用请求来处理传入的命令。当任何传输(不限于 DOCA 传输)收到新命令时,它会创建 spdk_nvmsf_request
结构的实例。此结构包含各种元素,包括 NVMe 命令、命令所属的提交队列 (SQ)、队列对 (qpair)、IO 向量 (IOV) 和其他相关数据。在我们的设计中,我们引入了一个名为 nvmf_doca_request
的新包装器结构,它封装了 NVMe-oF 请求结构以及特定于 DOCA 传输的附加字段。此结构中包含的主要字段

其中
SPDK 请求是
spdk_nvmf_request
结构的实例,它表示正在处理的命令。主机和 DPU DOCA 缓冲区是指向位于主机或 DPU 的数据缓冲区的指针,其中包含与此命令关联的数据。
DOCA 请求回调是在请求完成时调用的函数,接收相应的 DOCA 请求回调参数。
对请求执行的关键操作包括
nvmf_doca_request_pool_create
– 创建 SQ 时,会分配一个请求池,其大小与 SQ 深度匹配。nvmf_doca_request_pool_destroy
– 此函数在删除 SQ 时销毁请求池。nvmf_doca_request_get
– 从与特定 SQ 关联的池中检索 NVMe-oF 请求对象,并在从主机获取 SQE 后调用。nvmf_doca_request_complete
– 通过调用其回调来完成 NVMe-oF 请求,然后将请求释放回池中。
控制路径流程
DOCA 传输监听
热插拔和热移除
从
add_listener
RPC 触发器开始 – 当调用add_listener
RPC 时,流程开始。然后 SPDK 暂停所有轮询组。按 VUID 查找表示器 – 传输使用给定的 VUID 搜索表示器。
创建模拟 PCIe 设备 – 找到表示器后,传输创建一个模拟 PCIe 设备并将其分配给轮询组。
初始化内存映射 (mmap) – 对于每个轮询组,传输设置一个表示主机内存的内存映射区域。

在此阶段,NVMe 驱动程序检测到新热插拔的设备。
控制器寄存器事件
初始化

控制器初始化 – 该过程首先配置控制器寄存器
NVMe 驱动程序写入 ASQ 和 ACQ 寄存器以设置管理提交队列和管理完成队列。
驱动程序通过写入控制器配置 (CC) 寄存器来配置控制器,设置参数,例如内存页面大小、仲裁和超时值。
驱动程序将 CC 寄存器中的 CC.EN 位设置为 1,将控制器从
Disabled
转换为Enabled
。NVMe 驱动程序等待 CC.RDY(控制器状态寄存器 - 就绪位)变为 1。这表示控制器已成功完成其初始化并准备好处理命令。
状态区域回调触发器 – 此时,PCI 模拟设备触发对状态区域的回调。此回调通过检查启用位中的更改来检测主机的初始化过程。此回调可能会多次发生,但仅当启用位已更改时才会继续。
CQ 和 DPU 设置 – 回调继续创建完成队列 (CQ) 资源并在 DPU 上设置 DPA 线程。DPA 线程配备了两个消息队列:一个用于发送,一个用于接收。
将 CQ 门铃绑定到 DB 完成 – 发送 RPC 以将 CQ 门铃绑定到 DB 完成上下文。这是在 DPA 尚未活动时完成的,以防止同步问题。
SQ 资源创建 – 创建提交队列 (SQ) 资源,包括 SQE 池和将 SQE 从主机复制到 DPU 所需的本地缓冲区大小。DOCA DMA 用于数据传输操作。
SQ 绑定 DB 消息 – SQ 向 DPA 发送“绑定 DB”消息。
DPA 接收绑定 DB 消息 – DPA 处理“绑定 DB”消息,并将 SQ 的门铃信息发送到 DB 完成上下文。
SQ 发送绑定完成消息 – SQ 向 DPU 发送“绑定完成”消息。
启动 SQ DB – DPU 接收“绑定完成”消息并启动 SQ DB。
NVMe-oF QPair 和请求池创建 – 创建 NVMe-oF QPair 和 NVMe-oF 请求池。
异步 QPair 创建 – NVMe-oF 库启动异步操作以创建 QPair。
NVMe-oF 库调用 –
库创建 QPair 并调用传输以获取最佳轮询组。
然后,它在选定的线程上调用
poll_group_add
。
NVMe-oF 连接请求 – 传输发送 NVMe-oF 连接请求。
设置属性请求 – 连接请求完成后,发送
set_property
请求以更新主机在初始化期间提供的控制器配置。回调触发 – 一旦
set_property
请求完成,NVMe-oF 触发回调。更新状态区域 – 传输更新状态区域,将
CSTS.RDY
位设置为 1。主机轮询解除阻止 – 将 CSTS.RDY 设置为 1 后,主机轮询现在解除阻止,完成初始化过程。
重置和关闭流程
NVMe 中使用 SPDK 的重置流程对于维护存储子系统的完整性和稳定性至关重要,以便系统随后能够优雅地恢复。
重置过程可以由以下方式启动
主机:主机可以通过配置控制器寄存器空间中的特定寄存器来启动 NVMe 控制器的关闭或重置。在这种情况下,将触发
handle_controller_register_events()
函数。配置关闭通知 (SHN):主机可以写入 CC(控制器配置)寄存器,特别是 SHN 字段,以指定控制器应如何处理关闭
正常关闭 –
SPDK_NVME_SHN_NORMAL
– 允许正常关闭,控制器可以完成未完成的命令。突然关闭 –
SPDK_NVME_SHN_ABRUPT
– 强制立即关闭,而不完成未完成的命令。
通过将 CC.enablebit 设置为零来重置 NVMe 控制器。在这种情况下,也会触发
handle_controller_register_events()
函数。主机可以使用功能级重置 (FLR) 来启动重置 NVMe 控制器。在这种情况下,将触发
flr_event_handler_cb()
。
当 DOCA 传输在初始化流程期间检测到内部错误情况时,可以通过
nvmf_doca_on_initialization_error()
启动重置流程。
一旦请求关闭或重置,传输将继续销毁与控制器关联的所有资源。如果是关闭请求,则相应地更新关闭状态。当主机执行功能级重置 (FLR) 或控制器重置时,传输必须采取以下几个操作:它销毁指定设备的所有轮询组中的所有提交队列和完成队列 (SQ 和 CQ),通过管理线程销毁管理 SQ 和 CQ,停止 PCIe 设备,然后重新启动它。
无论重置流程的原因是什么,它都从 nvmf_doca_pci_dev_admin_reset()
开始。此函数标志着异步流程的开始,用于重置 PCIe 设备 NVMe-oF 上下文。该流程由按顺序触发的回调组成,以跟踪每个流程的完成情况并继续进行下一个流程。现在让我们说明流程的其余部分
如果管理 QP 存在,则该过程首先检查是否有任何 I/O 提交队列 (SQ)。
如果找到 I/O SQ,则开始异步流程以停止所有 I/O SQ。
对于与管理队列关联的每个 I/O SQ,它检索负责销毁其特定 SQ 的相应轮询组,因为没有其他轮询组可以执行此操作。
一旦所有 I/O SQ 都停止,如果仍有任何 I/O 完成队列 (CQ),则会向每个轮询组发送消息,指示它们删除其 I/O CQ。
在销毁所有 I/O 队列后,流程继续销毁管理 CQ 和 SQ。
在此流程完成后,它将移动到 nvmf_doca_pci_dev_admin_reset_continue()
以完成重置流程
如果通过配置 NVMe 控制器寄存器发出重置,则将
CSTS.BITS.SHST
设置为SPDK_NVME_SHST_COMPLETE
,并将CSTS.BITS.RDY
设置为 0如果重置是由 FLR 触发的,则停止 PCIe 设备上下文:
doca_ctx_stop(doca_devemu_pci_dev_as_ctx())
I/O QP 创建/销毁
I/O 队列对 (QPairs) 的创建和销毁过程从启动器(主机)在传输初始化完成后向 NVMe-oF 目标发送 NVMe-oF 连接命令开始。主机在传输完成其初始化后向 NVMe-oF 目标发送 NVMe-oF 连接命令。NVMe-oF 目标接收管理命令并开始通过 nvmf_doca_on_fetch_sqe_complete()
对其进行处理。根据命令操作码,执行以下步骤
创建 I/O 完成队列 –
SPDK_NVME_OPC_CREATE_IO_CQ
–handle_create_io_cq()
:首先,系统选择一个负责创建 CQ 的轮询组。
目标在选定的轮询组中搜索
nvmf_doca_pci_dev_poll_group
实体。如果未找到,则表示这是与此轮询组管理的特定设备关联的第一个队列,因此需要创建一个新实体。接下来,目标使用命令中提供的属性和数据(例如 CQ 大小、CQ ID、CQ 地址和 CQ MSI-X)分配一个 DOCA IO (
nvmf_doca_io
)。创建 DMA 上下文、消息队列、消费者和生产者句柄以及 DPA 线程。
一旦异步分配和设置完成,目标就会发布一个完成队列条目 (CQE),以指示操作已成功。它是通过静态 void
nvmf_doca_poll_group_create_io_cq_done()
完成的。
创建 I/O 提交队列 –
SPDK_NVME_OPC_CREATE_IO_SQ
–handle_create_io_sq()
:根据命令中提供的 CQ ID 参数,系统首先识别新的提交队列 (SQ) 应添加到的 I/O 实体。从该实体中,它检索关联的
nvmf_doca_pci_dev_poll_group
。接下来,使用命令中指定的属性和数据(包括 SQ 大小、SQ ID 和 SQ 地址)分配一个 DOCA 提交队列
nvmf_doca_sq
。此分配在nvmf_doca_poll_group_create_io_sq()
函数中处理。还创建了 DMA 上下文和 DB 完成。
一旦创建 SQ 的异步过程完成,就会通过
nvmf_doca_poll_group_create_io_sq_done()
函数发布完成队列条目 (CQE)。接下来,目标开始 QPair 分配,这将创建一个新的队列对,其中包含提交队列 (SQ) 和完成队列 (CQ)。
然后,它通过调用
get_optimall_poll()
组来确定新 QPair 的最佳轮询组,确保连接到它的 CQ 和 SQ 在同一轮询组上运行。在确定适当的轮询组后,目标使用
nvmf_doca_poll_group_add()
将新创建的 QPair 添加到其中,从而启用 QPair 事件和 I/O 操作的管理。建立连接后,启动器可以开始通过新创建的 QPair 发送 I/O 命令。
销毁 I/O 完成队列 –
SPDK_NVME_OPC_DELETE_IO_CQ
–handle_delete_io_cq()
:该过程首先从 NVMe 请求中获取需要删除的队列的标识符。
检索到标识符后,将找到相应的
nvmf_doca_io
实体。然后从 I/O 中提取关联的轮询组,因为它负责销毁与其关联的 CQ。检索到的线程使用
spdk_thread_send_msg
计划执行nvmf_doca_pci_dev_poll_group_stop_io_cq()
。调用
nvmf_doca_io_stop()
函数以启动停止过程。如果此 I/O 中有不空闲的 CQ,则会触发nvmf_doca_io_stop_continue()
以推进序列。然后,此流程执行一系列异步回调函数,以确保每个步骤在开始下一个步骤之前完全完成,执行以下操作停止 DOCA devemu PCIe 设备门铃,以防止在关联的门铃完成上下文中触发完成
停止 NVMe-oF DOCA DMA 池
停止与完成队列关联的 DMA 上下文,并释放 NVMe-oF DOCA 完成队列的所有元素。
停止 NVMe-oF DOCA DPA 通信通道,停止接收和发送消息队列。
停止并销毁 PCIe 设备 DB。
停止 MSI-X。
停止并销毁 DB 完成。
销毁通信通道。
销毁 DPA 线程。
最后,目标发布一个完成队列条目 (CQE) 以指示操作已成功。
销毁 I/O 提交队列 –
SPDK_NVME_OPC_DELETE_IO_SQ
–handle_delete_io_sq()
:该过程首先从 NVMe 请求中获取需要删除的队列的标识符。
检索到标识符后,将找到相应的
nvmf_doca_sq
实体。然后从 I/O 中提取关联的轮询组,因为它负责销毁与其关联的 SQ。检索到的线程使用
spdk_thread_send_msg
计划执行nvmf_doca_pci_dev_poll_group_stop_io_sq()
。调用
nvmf_doca_sq_stop()
函数以启动停止过程。停止过程首先调用
spdk_nvmf_qpair_disconnect()
以断开 NVMe-oF 队列对 (QP) 的连接,清理关联的资源并终止连接。此步骤完成后,
nvmf_doca_sq_stop_continue()
被触发以继续执行一系列异步回调函数,确保每个步骤在移动到下一步之前完成,执行以下操作断开 NVMe-oF 队列对 (QP) 的连接,清理关联的资源并终止连接。
停止 DOCA devemu PCIe 设备门铃,以防止在关联的门铃完成上下文中触发完成。
向 DPA 发送取消绑定 SQ 门铃消息。
停止 NVMe-oF DOCA DMA 池
停止与提交队列关联的 DMA 上下文,并释放 NVMe-oF DOCA 提交队列的所有元素。
销毁与 SQ 关联的资源:DMA 池、队列和请求池。
最后,目标发布一个提交队列条目 (CQE) 以指示操作已成功。
数据路径流程
从主机的角度来看,它正在与标准 NVMe 设备通信。为了创建这种体验,DOCA 传输使用可用的 NVMe-oF API 来有效地模拟真实 NVMe 设备的行为。
数据路径流程涉及一系列步骤,这些步骤处理主机和 NVMe-oF 目标之间的命令传输和处理。它从主机将命令写入提交队列 (SQ) 条目开始,指定 NVMe-oF 目标处理的读取或写入请求等操作。
下图提供了整体视图和数据路径步骤

主机写入提交队列条目 (SQE)。
主机响铃(DB),DPA 接收到 DB 值。
生产者将 DB 值转发到 ARM 处理器。
系统从主机读取 SQE。
SQE 被处理。
完成队列条目 (CQE) 被写回。
如果启用了 MSI-X,生产者会触发 MSI-X 中断。
DPU 的消费者向主机发出 MSI-X 中断。
以下各步骤将详细描述
检索门铃
该过程首先检索门铃值,门铃值指示主机提交的新命令,从而使系统能够识别和处理待处理的命令
DPA 唤醒并检查激活原因——可能是 DPU 已为 DPA 消费者发布了一些内容,或者需要将新的门铃值传递给 DPU。在本例中,DPA 检测到一个新的门铃值,并通过消息队列将其发送到 DPU。然后,DPU 计算需要从主机的提交队列中获取的提交队列条目 (SQE) 的数量,并使用 DMA 通过 nvmf_doca_sq_update_pi()
函数检索命令。
从主机获取 SQE
检索提交队列条目 (SQE) 后,系统必须将此命令转换为 NVMe-oF 目标可以理解和处理的请求对象。SQE 包含需要执行的关键命令详细信息,例如读取或写入操作。此过程涉及填充 spdk_nvmf_request
结构,其中包括
从 SQE 中提取的命令参数。
关联的数据缓冲区位置(如果任何数据要读取或写入)。
处理命令所需的元数据和其他信息。
对于管理命令,SQE 由 nvmf_doca_on_fetch_sqe_complete()
函数处理,而 I/O NVMe-oF 命令由 nvmf_doca_on_fetch_nvm_sqe_complete()
函数管理。这两个函数都负责填充 nvmf_doca_request
结构。
从 SQ 请求池中获取请求,如前所述,并通过根据正在发出的特定命令设置各种属性来填充该请求。这些属性可能包括命名空间 ID、请求的长度以及请求所属的队列。
接下来,命令中处理了数据方向的三个选项
无数据传输
准备好请求后,系统将回调函数设置为
post_cqe_from_response()
然后,它使用
spdk_nvmf_request_exec()
执行请求最后,系统发布 CQE 以指示完成
从主机到 DPU 的数据传输
准备好请求后,系统从 SQ 池中检索缓冲区,并使用要从主机复制的数据的详细信息初始化它们
它调用
nvmf_doca_sq_copy_data()
,该函数执行从主机到 DPU 的 DMA 复制一旦异步复制完成,将调用
spdk_nvmf_request_exec()
以继续处理最后,系统发布 CQE 以指示完成
从 DPU 到主机的数据传输
准备好请求后,系统从 SQ 池中检索缓冲区,并使用要复制的数据和主机上的目标地址初始化它们。
它调用
nvmf_doca_sq_copy_data()
以执行从 DPU 到主机的 DMA 复制一旦异步复制完成,将调用
spdk_nvmf_request_exec()
以完成处理然后,系统发布 CQE 以指示操作完成
虽然 NVMe 命令和管理命令的总体流程相似,但在传输实现上存在细微差异,以满足每种命令类型的独特要求。对于读取和写入等 I/O 命令,系统可能涉及大量数据块传输。PRP(物理区域页)在此处发挥作用,因为它们用于描述要读取或写入的数据的内存位置。PRP 提供 NVMe 设备用于直接访问主机内存的物理地址列表。
在这种情况下,可能需要多次 DMA 操作来复制数据。因此,在准备好请求后,调用函数 nvme_cmd_map_prps()
以迭代整个 PRP 列表,准备从池中检索的缓冲区,并使用相应的数据和目标地址初始化它们。一旦缓冲区设置正确,将调用函数 buffer_ready_copy_data_host_to_dpu()
,该函数迭代所有缓冲区并为每个缓冲区调用 nvmf_doca_sq_copy_data()
。只有在缓冲区的所有异步复制任务完成后,才会调用函数 nvmf_doca_request_complete()
以指示请求处理结束。
将命令分派到 NVMe-oF 目标
构建请求后,将调用函数 spdk_nvmf_request_exec()
来执行它。spdk_nvmf_request_exec()
通过确定命令类型并将其分派到 NVMe-oF 目标内的相应处理程序进行处理,从而启动请求的处理。NVMe-oF 目标解释命令,分配必要的资源,并执行请求的操作。
当通过 spdk_nvmf_request_exec()
将请求分派到 NVMe-oF 目标以执行时,通常会配置完成回调函数。一旦请求被完全处理,就会调用此回调,从而向传输层指示 NVMe-oF 目标已完成处理请求。
将 CQE 发布到主机
命令处理完成后,我们创建一个完成队列条目 (CQE),并将其发布回主机,以指示命令的完成状态。此条目包含有关操作成功或遇到的任何错误的详细信息。
引发 RMSI-X
如果发布 CQE 的 DMA 成功完成,则会向主机发出 MSIX 中断,以通知主机已发生完成事件,从而允许主机读取 CQE 并处理请求的结果。
在这种情况下,DPU 调用 nvmf_doca_io_raise_msix()
, 这反过来又通过 nvmf_doca_dpa_msgq_send()
发送消息。此操作会促使 DPA 唤醒并尝试检索消费者完成上下文。然后,DPA 接收来自 DPU 的消息,指示其发出 MSIX 中断。
流程图说明了从获取 SQE 和准备请求到发布 CQE 的步骤,重点介绍了三种可能的数据场景(不涉及 PRP 的情况)

局限性
支持的 SPDK 版本
支持的 SPDK 版本为 23.01。
支持的管理命令
目前,并非所有管理命令都受支持。传输支持以下命令:SPDK_NVME_OPC_CREATE_IO_CQ
、SPDK_NVME_OPC_DELETE_IO_CQ
、SPDK_NVME_OPC_CREATE_IO_SQ
、SPDK_NVME_OPC_DELETE_IO_SQ
、SPDK_NVME_OPC_ASYNC_EVENT_REQUEST
、SPDK_NVME_OPC_IDENTIFY
、SPDK_NVME_OPC_GET_LOG_PAGE
、SPDK_NVME_OPC_GET_FEATURES
、SPDK_NVME_OPC_SET_FEATURES
。
支持的 NVM 命令
目前,传输仅支持以下 NVMe 命令:SPDK_NVME_OPC_FLUSH
、SPDK_NVME_OPC_WRITE
、SPDK_NVME_OPC_READ
。
SPDK 停止监听流程
停止侦听器流程 (spdk_nvmf_stop_listen
) 可以通过 remove_listener
RPC 启动。当前版本的 SPDK 在异步处理 remove_listener
请求方面存在限制。因此,建议仅在释放内存缓冲区和队列对等资源后才调用停止侦听器函数。这可以通过在主机端发出取消绑定脚本来完成
python3 samples/doca_devemu/devemu_pci_vfio_bind.py --unbind 0000
:62
:00.0
需要 BlueField-3 DPU
需要 SPDK 版本 23.0
有关如何安装 BlueField 相关软件的详细信息,请参阅 DOCA Linux 安装指南。
DOCA 参考应用程序的安装包包含应用程序的源代码以及匹配的编译说明。这允许“按原样”编译应用程序,并提供修改源代码,然后编译新版本应用程序的能力。
有关应用程序以及开发和编译技巧的更多信息,请参阅 DOCA 参考应用程序页面。
应用程序的源代码可以在应用程序的目录下找到:/opt/mellanox/doca/applications/nvme_emulation/.
编译所有应用程序
所有 DOCA 应用程序都在单个 meson 项目下定义。因此,默认情况下,编译包括所有应用程序。
要一起构建所有应用程序,请运行
cd /opt/mellanox/doca/applications/ meson /tmp/build ninja -C /tmp/build
doca_nvme_emulation
在/tmp/build/applications/nvme_emulation/
下创建或者,可以在
meson_options.txt
文件中设置所需的标志,而不是在编译命令行中提供它们编辑
/opt/mellanox/doca/applications/meson_options.txt
中的以下标志将
enable_all_applications
设置为false
将
enable_nvme_emulation
设置为true
应使用相同的编译命令,如上一节所示
cd /opt/mellanox/doca/applications/ meson /tmp/build ninja -C /tmp/build
使用自定义 SPDK 进行编译
如果您计划使用自定义或替代的 SPDK 版本,请通过 Meson 更新以下变量中的路径
spdk_lib_path
spdk_incl_path
spdk_dpdk_lib_path
spdk_isal_prefix
故障排除
如果您在编译 DOCA 应用程序时遇到任何问题,请参阅 NVIDIA BlueField 平台软件故障排除指南。
先决条件
从 DPU 上的服务器
用户需要分配巨页,然后运行持续运行的应用程序,保持活动状态并处理传入的 RPC 请求。
$ echo 1024
> /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages $ sudo mount -t hugetlbfs -o pagesize=2M nodev /mnt/huge
$ sudo /tmp/ariej_build/applications/nvme_emulation/doca_nvme_emulation
从 DPU 上的客户端
用户可以在应用程序执行期间向其发送各种 RPC 请求。例如,要删除侦听器,用户可以发送以下命令
$ sudo PYTHONPATH=/doca/applications/nvme_emulation/ /usr/bin/spdk_rpc.py nvmf_subsystem_remove_listener nqn.2016
-06
.io.spdk:cnode1 -t doca -a MT2306XZ00AYGES1D0F0
应用程序执行
NvMR 仿真应用程序以源代码形式提供,因此在执行应用程序之前需要进行编译。
应用程序使用说明
Usage: doca_nvme_application [DOCA Flags] [Program Flags] DOCA Flags: -h, --help Print a help synopsis -v, --version Print program version information -l, --log-level Set the (numeric) log level
for
the program <10
=DISABLE,20
=CRITICAL,30
=ERROR,40
=WARNING,50
=INFO,60
=DEBUG,70
=TRACE> --sdk-log-level Set the SDK (numeric) log levelfor
the program <10
=DISABLE,20
=CRITICAL,30
=ERROR,40
=WARNING,50
=INFO,60
=DEBUG,70
=TRACE> -j, --json <path> Parse all command flags from an input json file Program Flags: -p, --pci-addr DOCA Comm Channel device PCIe address -r, --rep-pci DOCA Comm Channel device representor PCIe address -f, --file File to send by the client / File to write by the server -t, --timeout Application timeoutfor
receiving file content messages,default
is5
sec -c, --config <config> JSON config file (default
none) --json <config> JSON config file (default
none) --json-ignore-init-errors don't exit on invalid config entry -d, --limit-coredumpdo
not set max coredump size to RLIM_INFINITY -g, --single-file-segments force creating just one hugetlbfs file -h, --help showthis
usage -i, --shm-id <id> shared memory ID (optional) -m, --cpumask <mask or list> core mask (like0xF
) or core list of'[]'
embraced (like [0
,1
,10
])for
DPDK -n, --mem-channels <num> channel number of memory channels usedfor
DPDK -p, --main-core <id> main (primary) corefor
DPDK -r, --rpc-socket <path> RPC listen address (default
/var/tmp/spdk.sock) -s, --mem-size <size> memory size in MBfor
DPDK (default
: 0MB) --disable-cpumask-locks Disable CPU core lock files. --silence-noticelog disable notice level logging to stderr --msg-mempool-size <size> global message memory pool size in count (default
:262143
) -u, --no-pci disable PCIe access --wait-for
-rpc waitfor
RPCs to initialize subsystems --max-delay <num> maximum reactor delay (in microseconds) -B, --pci-blocked <bdf> PCIe addr to block (can be used more than once) -R, --huge-unlink unlink huge files after initialization -v, --version print SPDK version -A, --pci-allowed <bdf> PCIe addr to allow (-B and -A cannot be used at the same time) --huge-dir <path> use a specific hugetlbfs mount to reserve memory from --iova-mode <pa/va> set IOVA mode ('pa'
for
IOVA_PA and'va'
for
IOVA_VA) --base-virtaddr <addr> the base virtual addressfor
DPDK (default
:0x200000000000
) --num-trace-entries <num> number of trace entriesfor
each core, must be power of2
, setting0
to disable trace (default
32768
) --rpcs-allowed comma-separated list of permitted RPCS --env-context Opaque contextfor
use of the env implementation --vfio-vf-token VF token (UUID) shared between SR-IOV PF and VFsfor
vfio_pci driver -L, --logflag <flag> enable log flag (all, accel, aio, app_config, app_rpc, bdev, bdev_concat, bdev_ftl, bdev_group, bdev_malloc, bdev_null, bdev_nvme, bdev_raid, bdev_raid0, bdev_raid1, bdev_raid5f, blob, blob_esnap, blob_rw, blobfs, blobfs_bdev, blobfs_bdev_rpc, blobfs_rw, ftl_core, ftl_init, gpt_parse, json_util, log, log_rpc, lvol, lvol_rpc, notify_rpc, nvme, nvme_vfio, nvmf, nvmf_tcp, opal, rdma, reactor, rpc, rpc_client, sock, sock_posix, thread, trace, uring, vbdev_delay, vbdev_gpt, vbdev_lvol, vbdev_opal, vbdev_passthru, vbdev_split, vbdev_zone_block, vfio_pci, vfio_user, virtio, virtio_blk, virtio_dev, virtio_pci, virtio_user, virtio_vfio_user, vmd) -e, --tpoint-group <group-name>[:<tpoint_mask>] group_name - tracepoint group namefor
spdk trace buffers (bdev, nvmf_rdma, nvmf_tcp, blobfs, thread, nvme_pcie, nvme_tcp, bdev_nvme, nvme_nvda_tcp, all) tpoint_mask - tracepoint maskfor
enabling individual tpoints inside a tracepoint group. First tpoint inside a group can be enabled by setting tpoint_mask to1
(e.g. bdev:0x1
). Groups and masks can be combined (e.g. thread,bdev:0x1
). All available tpoints can be found in /include/spdk_internal/trace_defs.h注意可以使用
-h
(或--help
)选项将上述用法打印到命令行/tmp/build/applications/nvme_emulation/doca_nvme_emulation -h
命令行标志
该应用程序使用与 SPDK 相同的命令行标志,从而允许类似于标准 SPDK 应用程序的配置和行为控制。
有关更多详细信息,请参阅 SPDK 官方 存储性能开发套件文档。
故障排除
如果您在安装或执行 DOCA 应用程序时遇到任何问题,请参阅 DOCA 故障排除。
参考
/opt/mellanox/doca/applications/nvme_emulation/
/opt/mellanox/doca/applications/nvme_emulation/build_device_code.sh
/opt/mellanox/doca/applications/nvme_emulation/dependencies
/opt/mellanox/doca/applications/nvme_emulation/device
/opt/mellanox/doca/applications/nvme_emulation/host
/opt/mellanox/doca/applications/nvme_emulation/meson.build
/opt/mellanox/doca/applications/nvme_emulation/nvme_emulation.c
/opt/mellanox/doca/applications/nvme_emulation/rpc_nvmf_doca.py