使用 NVMe 驱动器

适用于:

  • Windows 10
  • Windows Server 2016

了解如何从 Windows 应用程序使用高速 NVMe 设备。 设备访问是通过 StorNVMe.sys 启用的,这是 Windows Server 2012 R2 和 Windows 8.1 中首次引入的内置驱动程序。 还通过 KB 热修补程序向 Windows 7 设备提供了 StorNVMe.sys。 在 Windows 10 中,引入了一些新功能,包括供应商特定的 NVMe 命令的直通机制和对现有 IOCTL 的更新。

本主题概述了可用于访问 Windows 10 中的 NVMe 驱动器的常规用途 API。 还介绍了:

用于处理 NVMe 驱动器的 API

可以使用以下常规用途 API 访问 Windows 10 中的 NVMe 驱动器。 对于用户模式应用程序,可以在 winioctl.h 中找到这些 API,而对于内核模式驱动程序,则可以在 ntddstor.h 中找到这些 API。 有关头文件的详细信息,请参阅头文件

  • IOCTL_STORAGE_PROTOCOL_COMMAND:将此 IOCTL 与 STORAGE_PROTOCOL_COMMAND 结构一起使用以发出 NVMe 命令。 此 IOCTL 支持 NVMe 直通,并支持 NVMe 中的命令效果日志。 可以将其与特定于供应商的命令一起使用。 有关详细信息,请参阅直通机制

  • STORAGE_PROTOCOL_COMMAND:此输入缓冲区结构包括一个 ReturnStatus 字段,可用于报告以下状态值。

    • STORAGE_PROTOCOL_STATUS_PENDING
    • STORAGE_PROTOCOL_STATUS_SUCCESS
    • STORAGE_PROTOCOL_STATUS_ERROR
    • STORAGE_PROTOCOL_STATUS_INVALID_REQUEST
    • STORAGE_PROTOCOL_STATUS_NO_DEVICE
    • STORAGE_PROTOCOL_STATUS_BUSY
    • STORAGE_PROTOCOL_STATUS_DATA_OVERRUN
    • STORAGE_PROTOCOL_STATUS_INSUFFICIENT_RESOURCES
    • STORAGE_PROTOCOL_STATUS_NOT_SUPPORTED
  • IOCTL_STORAGE_QUERY_PROPERTY:将此 IOCTL 与 STORAGE_PROPERTY_QUERY 结构一起使用以检索设备信息。 有关详细信息,请参阅特定于协议的查询温度查询

  • STORAGE_PROPERTY_QUERY:此结构包括 PropertyId 和 AdditionalParameters 字段,用于指定要查询的数据。 在 PropertyId 文件中,使用 STORAGE_PROPERTY_ID 枚举指定数据类型。 使用 AdditionalParameters 字段指定更多详细信息,具体取决于数据类型。 对于特定于协议的数据,请使用 AdditionalParameters 字段中的 STORAGE_PROTOCOL_SPECIFIC_DATA 结构。 对于温度数据,请使用 AdditionalParameters 字段中的 STORAGE_TEMPERATURE_INFO 结构。

  • STORAGE_PROPERTY_ID:此枚举包括若干新值,这些新值允许 IOCTL_STORAGE_QUERY_PROPERTY 检索特定于协议和温度信息。

    • StorageAdapterProtocolSpecific 属性:如果 ProtocolType = ProtocolTypeNvme 且 DataType = NVMeDataTypeLogPage,则调用方应请求 512 字节的数据块。
    • StorageDeviceProtocolSpecificProperty

    将这些特定于协议的某个属性 ID 与 STORAGE_PROTOCOL_SPECIFIC_DATA 结合使用,用于在 STORAGE_PROTOCOL_DATA_DESCRIPTOR 结构中检索特定于协议的数据。

    • StorageAdapterTemperatureProperty
    • StorageDeviceTemperatureProperty

    使用这些温度属性 ID 之一检索 STORAGE_TEMPERATURE_DATA_DESCRIPTOR 结构中的温度数据。

  • STORAGE_PROTOCOL_SPECIFIC_DATA:当此结构用于 STORAGE_PROPERTY_QUERY 的 AdditionalParameters 字段并指定了 STORAGE_PROTOCOL_NVME_DATA_TYPE 枚举值时,将检索特定于 NVMe 的数据。 在 STORAGE_PROTOCOL_SPECIFIC_DATA 结构的 DataType 字段中使用以下 STORAGE_PROTOCOL_NVME_DATA_TYPE 值之一:

    • 使用 NVMeDataTypeIdentify 获取标识控制器数据或标识命名空间数据。
    • 使用 NVMeDataTypeLogPage 获取日志页(包括 SMART/health 数据)。
    • 使用 NVMeDataTypeFeature 获取 NVMe 驱动器的功能。
  • STORAGE_TEMPERATURE_INFO:此结构用于保存特定的温度数据。 此结构在 STORAGE_TEMERATURE_DATA_DESCRIPTOR 中用于返回温度查询的结果。

  • IOCTL_STORAGE_SET_TEMPERATURE_THRESHOLD:将此 IOCTL 与 STORAGE_TEMPERATURE_THRESHOLD 结构配合使用,以设置温度阈值。 有关详细信息,请参阅行为更改命令

  • STORAGE_TEMPERATURE_THRESHOLD:此结构用作输入缓冲器来指定温度阈值。 OverThreshold 字段(布尔值)指定 Threshold 字段是否算作超过阈值(如果不算,则是低于阈值)。

直通机制

NVMe 规范中未定义的命令是主机 OS 最难处理的命令 - 主机无法深入了解命令对目标设备的影响、公开的基础结构 (命名空间/块大小) 及其行为。

为了更好地通过 Windows 存储堆栈传递此类特定于设备的命令,新的直通机制允许通过管道传递特定于供应商的命令。 此直通管道还有助于开发管理和测试工具。 但是,此直通机制需要使用命令效果日志。 此外,StoreNVMe.sys 要求在命令效果日志 (Command Effects Log) 中介绍所有命令,而不仅仅是直通命令。

重要

如果命令效果日志中未描述,StorNVMe.sys 和 Storport.sys 将阻止向设备发出任何命令。

 

支持命令效果日志

命令效果日志(如 NVMe 规范 1.2 的第 5.10.1.5 节支持的命令和效果中所述)允许描述特定于供应商的命令的效果以及规范定义的命令。 这有利于命令支持验证和命令行为优化,因此应针对设备支持的整个命令集实现。 以下条件描述如何根据命令效果日志条目发送命令的结果。

对于命令效果日志中所述的任何特定命令...

While:

  • 命令支持 (CSUPP) 设置为“1”,表示控制器支持该命令(位 01)

    注意

    当 CSUPP 设置为“0”(表示不支持该命令)时,该命令将被阻止

     

如果设置了以下任一项:

  • 控制器功能更改 (CCC) 设置为“1”,表示命令可能会更改控制器功能(位 04)

  • 命名空间清单更改 (NIC) 设置为“1”,表示命令可能会更改多个命名空间(位 03)

  • 命名空间功能更改 (NCC) 设置为“1”,表示命令可能会更改单个命名空间的功能(位 02)

  • 命令提交和执行 (CSE) 设置为 001b 或 010b,表示当没有其他未完成的命令时,该命令可以提交到相同或任何命名空间,并且在此命令完成之前,不得将另一个命令提交到相同或任何命名空间(位 18:16)

然后,命令将作为唯一未完成的命令发送到适配器。

Else if

  • 命令提交和执行 (CSE) 设置为 001b,表示当没有其他未完成的命令时,该命令可以提交到相同命名空间,并且在此命令完成之前,不得将另一个命令提交到相同命名空间(位 18:16)

然后,该命令将作为唯一未完成的命令发送到逻辑单元号对象 (LUN)。

否则,该命令将随其他未完成的命令一起发送,而不会受到抑制。 例如,如果将特定于供应商的命令发送到设备来检索未规范定义的统计信息,则更改设备的行为或执行 I/O 命令的能力应该没有风险。 此类请求可以与 I/O 并行处理,无需暂停/恢复。

使用 IOCTL_STORAGE_PROTOCOL_COMMAND 发送命令

直通可以使用 Windows 10 中引入的 IOCTL_STORAGE_PROTOCOL_COMMAND 进行。 此 IOCTL 旨在具有与现有 SCSI 和 ATA 直通 IOCTL 类似的行为,以便将嵌入的命令发送到目标设备。 通过此 IOCTL,直通可以发送到存储设备,包括 NVMe 驱动器。

例如,在 NVMe 中,IOCTL 将允许发送以下命令代码。

  • 供应商特定的管理员命令 (C0h – FFh)
  • 供应商特定的 NVMe 命令 (80h – FFh)

与所有其他 IOCTL 一样,使用 DeviceIoControl 向下发送直通 IOCTL。 将使用 ntddstor.h 中 STORAGE_PROTOCOL_COMMAND 输入缓冲区结构来填充 IOCTL。 使用特定于供应商的命令填充 Command 字段。

typedef struct _STORAGE_PROTOCOL_COMMAND {

    ULONG   Version;                        // STORAGE_PROTOCOL_STRUCTURE_VERSION
    ULONG   Length;                         // sizeof(STORAGE_PROTOCOL_COMMAND)

    STORAGE_PROTOCOL_TYPE  ProtocolType;
    ULONG   Flags;                          // Flags for the request

    ULONG   ReturnStatus;                   // return value
    ULONG   ErrorCode;                      // return value, optional

    ULONG   CommandLength;                  // non-zero value should be set by caller
    ULONG   ErrorInfoLength;                // optional, can be zero
    ULONG   DataToDeviceTransferLength;     // optional, can be zero. Used by WRITE type of request.
    ULONG   DataFromDeviceTransferLength;   // optional, can be zero. Used by READ type of request.

    ULONG   TimeOutValue;                   // in unit of seconds

    ULONG   ErrorInfoOffset;                // offsets need to be pointer aligned
    ULONG   DataToDeviceBufferOffset;       // offsets need to be pointer aligned
    ULONG   DataFromDeviceBufferOffset;     // offsets need to be pointer aligned

    ULONG   CommandSpecific;                // optional information passed along with Command.
    ULONG   Reserved0;

    ULONG   FixedProtocolReturnData;        // return data, optional. Some protocol, such as NVMe, may return a small amount data (DWORD0 from completion queue entry) without the need of separate device data transfer.
    ULONG   Reserved1[3];

    _Field_size_bytes_full_(CommandLength) UCHAR Command[ANYSIZE_ARRAY];

} STORAGE_PROTOCOL_COMMAND, *PSTORAGE_PROTOCOL_COMMAND;

需要发送的特定于供应商的命令应填充在上面突出显示的字段中。 再次请注意,必须为直通命令实现命令效果日志。 具体而言,需要在命令效果日志中将这些命令报告为受支持(有关详细信息,请参阅上一节)。 另请注意,PRP 字段特定于驱动程序,因此发送命令的应用程序可以将它们保留为 0。

最后,此直通 IOCTL 用于发送特定于供应商的命令。 若要发送其他管理员或非供应商特定的 NVMe 命令(如 Identify),不应使用此直通OCTL。 例如,IOCTL_STORAGE_QUERY_PROPERTY 应用于标识或获取日志页。 有关详细信息,请参阅下一节特定于协议的查询

不要通过直通更新固件

不应使用直通发送固件下载和激活命令。 IOCTL_STORAGE_PROTOCOL_COMMAND 应仅用于特定于供应商的命令。

请改用以下常规存储 IOCTL(在 Windows 10 中引入)来避免应用程序直接使用固件 IOCTL 的 SCSI_miniport 版本。 存储驱动程序会将 IOCTL 转换为 SCSI 命令,或将 IOCTL 的 SCSI_miniport 版本转换为微型端口。

建议使用这些 IOCTL 在 Windows 10 和 Windows Server 2016 中开发固件升级工具:

为了获取存储信息和更新固件,Windows 还支持 PowerShell cmdlet 来快速执行此操作:

  • Get-StorageFirmwareInfo
  • Update-StorageFirmware

注意

若要在 Windows 8.1 中更新 NVMe 上的固件,请使用 IOCTL_SCSI_MINIPORT_FIRMWARE。 此 IOCTL 未向后移植到 Windows 7。 有关详细信息,请参阅在 Windows 8.1 中升级 NVMe 设备的固件

 

通过直通返回错误

与 SCSI 和 ATA 直通 IOCTL 类似,当命令/请求发送到微型端口或设备时,IOCTL 将返回此命令/请求是否成功。 在 STORAGE_PROTOCOL_COMMAND 结构中,IOCTL 通过 ReturnStatus 字段返回状态。

示例:发送特定于供应商的命令

在此示例中,任意特定于供应商的命令 (0xFF) 通过直通发送到 NVMe 驱动器。 以下代码分配缓冲区,初始化查询,然后通过 DeviceIoControl 将命令向下发送到设备。

    ZeroMemory(buffer, bufferLength);  
    protocolCommand = (PSTORAGE_PROTOCOL_COMMAND)buffer;  

    protocolCommand->Version = STORAGE_PROTOCOL_STRUCTURE_VERSION;  
    protocolCommand->Length = sizeof(STORAGE_PROTOCOL_COMMAND);  
    protocolCommand->ProtocolType = ProtocolTypeNvme;  
    protocolCommand->Flags = STORAGE_PROTOCOL_COMMAND_FLAG_ADAPTER_REQUEST;  
    protocolCommand->CommandLength = STORAGE_PROTOCOL_COMMAND_LENGTH_NVME;  
    protocolCommand->ErrorInfoLength = sizeof(NVME_ERROR_INFO_LOG);  
    protocolCommand->DataFromDeviceTransferLength = 4096;  
    protocolCommand->TimeOutValue = 10;  
    protocolCommand->ErrorInfoOffset = FIELD_OFFSET(STORAGE_PROTOCOL_COMMAND, Command) + STORAGE_PROTOCOL_COMMAND_LENGTH_NVME;  
    protocolCommand->DataFromDeviceBufferOffset = protocolCommand->ErrorInfoOffset + protocolCommand->ErrorInfoLength;  
    protocolCommand->CommandSpecific = STORAGE_PROTOCOL_SPECIFIC_NVME_ADMIN_COMMAND;  

    command = (PNVME_COMMAND)protocolCommand->Command;  

    command->CDW0.OPC = 0xFF;  
    command->u.GENERAL.CDW10 = 0xto_fill_in;  
    command->u.GENERAL.CDW12 = 0xto_fill_in;  
    command->u.GENERAL.CDW13 = 0xto_fill_in;  

    //  
    // Send request down.  
    //  

    result = DeviceIoControl(DeviceList[DeviceIndex].Handle,  
                             IOCTL_STORAGE_PROTOCOL_COMMAND,  
                             buffer,  
                             bufferLength,  
                             buffer,  
                             bufferLength,  
                             &returnedLength,  
                             NULL 
                             );  

在此示例中,如果成功对设备执行了命令,我们应该收到 protocolCommand->ReturnStatus == STORAGE_PROTOCOL_STATUS_SUCCESS

特定于协议的查询

Windows 8.1 引入了用于数据检索的 IOCTL_STORAGE_QUERY_PROPERTY。 在 Windows 10 中,IOCTL 已得到增强,以支持常见请求的 NVMe 功能,例如“获取日志页”、“获取功能”和“标识”。 这允许检索 NVMe 特定信息,以便进行监视和清点。

此处显示了 IOCTL STORAGE_PROPERTY_QUERY(来自 Windows 10)的输入缓冲区。

typedef struct _STORAGE_PROPERTY_QUERY {
    STORAGE_PROPERTY_ID PropertyId;
    STORAGE_QUERY_TYPE QueryType;
    UCHAR  AdditionalParameters[1];
} STORAGE_PROPERTY_QUERY, *PSTORAGE_PROPERTY_QUERY;

使用 IOCTL_STORAGE_QUERY_PROPERTY 检索 STORAGE_PROTOCOL_DATA_DESCRIPTOR 中特定于 NVMe 协议的信息时,请按如下所示配置 STORAGE_PROPERTY_QUERY 结构:

此处显示了 STORAGE_PROTOCOL_SPECIFIC_DATA 结构(来自 Windows 10)。

typedef struct _STORAGE_PROTOCOL_SPECIFIC_DATA {

    STORAGE_PROTOCOL_TYPE ProtocolType;
    ULONG   DataType;                 

    ULONG   ProtocolDataRequestValue;
    ULONG   ProtocolDataRequestSubValue;

    ULONG   ProtocolDataOffset;         
    ULONG   ProtocolDataLength;

    ULONG   FixedProtocolReturnData;   
    ULONG   Reserved[3];

} STORAGE_PROTOCOL_SPECIFIC_DATA, *PSTORAGE_PROTOCOL_SPECIFIC_DATA;

若要指定特定于 NVMe 协议的信息类型,请配置 STORAGE_PROTOCOL_SPECIFIC_DATA 结构,如下所示:

  • 将 ProtocolType 字段设置为 ProtocolTypeNVMe。

  • 将 DataType 字段设置为 STORAGE_PROTOCOL_NVME_DATA_TYPE 定义的枚举值:

    • 使用 NVMeDataTypeIdentify 获取标识控制器数据或标识命名空间数据。
    • 使用 NVMeDataTypeLogPage 获取日志页(包括 SMART/health 数据)。
    • 使用 NVMeDataTypeFeature 获取 NVMe 驱动器的功能。

将 ProtocolTypeNVMe 用作 ProtocolType 时,可以与 NVMe 驱动器上的其他 I/O 并行检索协议特定信息的查询。

重要

对于 IOCTL_STORAGE_QUERY_PROPERTY,其使用 StorageAdapterProtocolSpecificProperty 的 STORAGE_PROPERTY_ID 并且其 STORAGE_PROTOCOL_SPECIFIC_DATASTORAGE_PROTOCOL_SPECIFIC_DATA_EXT 结构设置为 ProtocolType=ProtocolTypeNvmeDataType=NVMeDataTypeLogPage 的,请将同一结构的 ProtocolDataLength 成员设置为最小值 512(字节)。

以下示例演示特定于 NVMe 协议的查询。

示例:NVMe 标识查询

在此示例中,“标识”请求将发送到 NVMe 驱动器。 以下代码初始化查询数据结构,然后通过 DeviceIoControl 将命令向下发送到设备。

    BOOL    result;
    PVOID   buffer = NULL;
    ULONG   bufferLength = 0;
    ULONG   returnedLength = 0;

    PSTORAGE_PROPERTY_QUERY query = NULL;
    PSTORAGE_PROTOCOL_SPECIFIC_DATA protocolData = NULL;
    PSTORAGE_PROTOCOL_DATA_DESCRIPTOR protocolDataDescr = NULL;

    //
    // Allocate buffer for use.
    //
    bufferLength = FIELD_OFFSET(STORAGE_PROPERTY_QUERY, AdditionalParameters) + sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) + NVME_MAX_LOG_SIZE;
    buffer = malloc(bufferLength);

    if (buffer == NULL) {
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: allocate buffer failed, exit.\n"));
        goto exit;
    }

    //
    // Initialize query data structure to get Identify Controller Data.
    //
    ZeroMemory(buffer, bufferLength);

    query = (PSTORAGE_PROPERTY_QUERY)buffer;
    protocolDataDescr = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;
    protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;

    query->PropertyId = StorageAdapterProtocolSpecificProperty;
    query->QueryType = PropertyStandardQuery;

    protocolData->ProtocolType = ProtocolTypeNvme;
    protocolData->DataType = NVMeDataTypeIdentify;
    protocolData->ProtocolDataRequestValue = NVME_IDENTIFY_CNS_CONTROLLER;
    protocolData->ProtocolDataRequestSubValue = 0;
    protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
    protocolData->ProtocolDataLength = NVME_MAX_LOG_SIZE;

    //
    // Send request down.
    //
    result = DeviceIoControl(DeviceList[Index].Handle,
                             IOCTL_STORAGE_QUERY_PROPERTY,
                             buffer,
                             bufferLength,
                             buffer,
                             bufferLength,
                             &returnedLength,
                             NULL
                             );

    ZeroMemory(buffer, bufferLength);
    query = (PSTORAGE_PROPERTY_QUERY)buffer;  
    protocolDataDescr = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;  
    protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;  

    query->PropertyId = StorageDeviceProtocolSpecificProperty;  
    query->QueryType = PropertyStandardQuery;  

    protocolData->ProtocolType = ProtocolTypeNvme;  
    protocolData->DataType = NVMeDataTypeLogPage;  
    protocolData->ProtocolDataRequestValue = NVME_LOG_PAGE_HEALTH_INFO;  
    protocolData->ProtocolDataRequestSubValue = 0;  
    protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);  
    protocolData->ProtocolDataLength = sizeof(NVME_HEALTH_INFO_LOG);  

    //  
    // Send request down.  
    //  
    result = DeviceIoControl(DeviceList[Index].Handle,  
                             IOCTL_STORAGE_QUERY_PROPERTY,  
                             buffer,  
                             bufferLength,  
                             buffer, 
                             bufferLength,  
                             &returnedLength,  
                             NULL  
                             );  

    //
    // Validate the returned data.
    //
    if ((protocolDataDescr->Version != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR)) ||
        (protocolDataDescr->Size != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR))) {
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: Get Identify Controller Data - data descriptor header not valid.\n"));
        return;
    }

    protocolData = &protocolDataDescr->ProtocolSpecificData;

    if ((protocolData->ProtocolDataOffset < sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA)) ||
        (protocolData->ProtocolDataLength < NVME_MAX_LOG_SIZE)) {
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: Get Identify Controller Data - ProtocolData Offset/Length not valid.\n"));
        goto exit;
    }

    //
    // Identify Controller Data 
    //
    {
        PNVME_IDENTIFY_CONTROLLER_DATA identifyControllerData = (PNVME_IDENTIFY_CONTROLLER_DATA)((PCHAR)protocolData + protocolData->ProtocolDataOffset);

        if ((identifyControllerData->VID == 0) ||
            (identifyControllerData->NN == 0)) {
            _tprintf(_T("DeviceNVMeQueryProtocolDataTest: Identify Controller Data not valid.\n"));
            goto exit;
        } else {
            _tprintf(_T("DeviceNVMeQueryProtocolDataTest: ***Identify Controller Data succeeded***.\n"));
        }
    }

  

重要

对于 IOCTL_STORAGE_QUERY_PROPERTY,其使用 StorageAdapterProtocolSpecificProperty 的 STORAGE_PROPERTY_ID 并且其 STORAGE_PROTOCOL_SPECIFIC_DATASTORAGE_PROTOCOL_SPECIFIC_DATA_EXT 结构设置为 ProtocolType=ProtocolTypeNvmeDataType=NVMeDataTypeLogPage 的,请将同一结构的 ProtocolDataLength 成员设置为最小值 512(字节)。

请注意,调用方需要分配单个缓冲区,其中包含 STORAGE_PROPERTY_QUERY 且大小为 STORAGE_PROTOCOL_SPECIFIC_DATA。 在此示例中,调用方使用相同的缓冲区进行属性查询的输入和输出。 这就是为什么分配的缓冲区的大小为“FIELD_OFFSET(STORAGE_PROPERTY_QUERY, AdditionalParameters) + sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) + NVME_MAX_LOG_SIZE”。 尽管可以为输入和输出分配单独的缓冲区,但我们建议使用单个缓冲区来查询 NVMe 相关信息。

identifyControllerData->NN 是命名空间数 (NN)。 Windows 将命名空间检测为物理驱动器。

示例:NVMe 获取日志页查询

在此示例中,根据上一个示例,“获取日志页”请求将发送到 NVMe 驱动器。 以下代码准备查询数据结构,然后通过 DeviceIoControl 将命令向下发送到设备。

    ZeroMemory(buffer, bufferLength);  

    query = (PSTORAGE_PROPERTY_QUERY)buffer;  
    protocolDataDescr = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;  
    protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;  

    query->PropertyId = StorageDeviceProtocolSpecificProperty;  
    query->QueryType = PropertyStandardQuery;  

    protocolData->ProtocolType = ProtocolTypeNvme;  
    protocolData->DataType = NVMeDataTypeLogPage;  
    protocolData->ProtocolDataRequestValue = NVME_LOG_PAGE_HEALTH_INFO;  
    protocolData->ProtocolDataRequestSubValue = 0;  // This will be passed as the lower 32 bit of log page offset if controller supports extended data for the Get Log Page.
    protocolData->ProtocolDataRequestSubValue2 = 0; // This will be passed as the higher 32 bit of log page offset if controller supports extended data for the Get Log Page.
    protocolData->ProtocolDataRequestSubValue3 = 0; // This will be passed as Log Specific Identifier in CDW11.
    protocolData->ProtocolDataRequestSubValue4 = 0; // This will map to STORAGE_PROTOCOL_DATA_SUBVALUE_GET_LOG_PAGE definition, then user can pass Retain Asynchronous Event, Log Specific Field.

    protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);  
    protocolData->ProtocolDataLength = sizeof(NVME_HEALTH_INFO_LOG);  

    //  
    // Send request down.  
    //  
    result = DeviceIoControl(DeviceList[Index].Handle,  
                             IOCTL_STORAGE_QUERY_PROPERTY,  
                             buffer,  
                             bufferLength,  
                             buffer, 
                             bufferLength,  
                             &returnedLength,  
                             NULL  
                             );  

    if (!result || (returnedLength == 0)) {
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: SMART/Health Information Log failed. Error Code %d.\n"), GetLastError());
        goto exit;
    }

    //
    // Validate the returned data.
    //
    if ((protocolDataDescr->Version != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR)) ||
        (protocolDataDescr->Size != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR))) {
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: SMART/Health Information Log - data descriptor header not valid.\n"));
        return;
    }

    protocolData = &protocolDataDescr->ProtocolSpecificData;

    if ((protocolData->ProtocolDataOffset < sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA)) ||
        (protocolData->ProtocolDataLength < sizeof(NVME_HEALTH_INFO_LOG))) {
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: SMART/Health Information Log - ProtocolData Offset/Length not valid.\n"));
        goto exit;
    }

    //
    // SMART/Health Information Log Data 
    //
    {
        PNVME_HEALTH_INFO_LOG smartInfo = (PNVME_HEALTH_INFO_LOG)((PCHAR)protocolData + protocolData->ProtocolDataOffset);

        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: SMART/Health Information Log Data - Temperature %d.\n"), ((ULONG)smartInfo->Temperature[1] << 8 | smartInfo->Temperature[0]) - 273);

        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: ***SMART/Health Information Log succeeded***.\n"));
    }

调用方可以使用 StorageAdapterProtocolSpecificPropertySTORAGE_PROPERTY_ID,其 STORAGE_PROTOCOL_SPECIFIC_DATASTORAGE_PROTOCOL_SPECIFIC_DATA_EXT 结构设置为 ProtocolDataRequestValue=VENDOR_SPECIFIC_LOG_PAGE_IDENTIFIER ,以请求供应商特定数据的 512 字节片段。

示例:NVMe 获取功能查询

在此示例中,根据上一个示例,“获取功能”请求将发送到 NVMe 驱动器。 以下代码准备查询数据结构,然后通过 DeviceIoControl 将命令向下发送到设备。

    //  
    // Initialize query data structure to Volatile Cache feature.  
    //  

    ZeroMemory(buffer, bufferLength);  


    query = (PSTORAGE_PROPERTY_QUERY)buffer;  
    protocolDataDescr = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;  
    protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;  

    query->PropertyId = StorageDeviceProtocolSpecificProperty;  
    query->QueryType = PropertyStandardQuery;  

    protocolData->ProtocolType = ProtocolTypeNvme;  
    protocolData->DataType = NVMeDataTypeFeature;  
    protocolData->ProtocolDataRequestValue = NVME_FEATURE_VOLATILE_WRITE_CACHE;  
    protocolData->ProtocolDataRequestSubValue = 0;  
    protocolData->ProtocolDataOffset = 0;  
    protocolData->ProtocolDataLength = 0;  

    //  
    // Send request down.  
    //  

    result = DeviceIoControl(DeviceList[Index].Handle,  
                             IOCTL_STORAGE_QUERY_PROPERTY,  
                             buffer,  
                             bufferLength,  
                             buffer,  
                             bufferLength,  
                             &returnedLength,  
                             NULL  
                             );  

    if (!result || (returnedLength == 0)) {  
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: Get Feature - Volatile Cache failed. Error Code %d.\n"), GetLastError());  
        goto exit;  
    }  

    //  
    // Validate the returned data.  
    //  

    if ((protocolDataDescr->Version != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR)) ||  
        (protocolDataDescr->Size != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR))) {  
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: Get Feature - Volatile Cache  - data descriptor header not valid.\n"));  
        return;                                           
    }  

    //
    // Volatile Cache 
    //
    {
        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: Get Feature - Volatile Cache - %x.\n"), protocolDataDescr->ProtocolSpecificData.FixedProtocolReturnData);

        _tprintf(_T("DeviceNVMeQueryProtocolDataTest: ***Get Feature - Volatile Cache succeeded***.\n"));
    }

特定于协议的集

从 Windows 10 19H1 开始,IOCTL_STORAGE_SET_PROPERTY 已得到增强,支持 NVMe 集功能。

IOCTL_STORAGE_SET_PROPERTY 的输入缓冲区如下所示:

typedef struct _STORAGE_PROPERTY_SET {

    //
    // ID of the property being retrieved
    //

    STORAGE_PROPERTY_ID PropertyId;

    //
    // Flags indicating the type of set property being performed
    //

    STORAGE_SET_TYPE SetType;

    //
    // Space for additional parameters if necessary
    //

    UCHAR AdditionalParameters[1];

} STORAGE_PROPERTY_SET, *PSTORAGE_PROPERTY_SET;

使用 IOCTL_STORAGE_SET_PROPERTY 设置 NVMe 功能时,请按如下所示配置 STORAGE_PROPERTY_SET 结构:

  • 分配可以同时包含 STORAGE_PROPERTY_SET 和 STORAGE_PROTOCOL_SPECIFIC_DATA_EXT 结构的缓冲区;
  • 分别将控制器或设备/命名空间请求的 PropertyID 字段设置为 StorageAdapterProtocolSpecificProperty 或 StorageDeviceProtocolSpecificProperty。
  • 使用所需的值填充 STORAGE_PROTOCOL_SPECIFIC_DATA_EXT 结构。 STORAGE_PROTOCOL_SPECIFIC_DATA_EXT 的开头是 STORAGE_PROPERTY_SET 的 AdditionalParameters 字段。

此处显示了 STORAGE_PROTOCOL_SPECIFIC_DATA_EXT 结构。

typedef struct _STORAGE_PROTOCOL_SPECIFIC_DATA_EXT {

    STORAGE_PROTOCOL_TYPE ProtocolType;
    ULONG   DataType;                   // The value will be protocol specific, as defined in STORAGE_PROTOCOL_NVME_DATA_TYPE or STORAGE_PROTOCOL_ATA_DATA_TYPE.

    ULONG   ProtocolDataValue;
    ULONG   ProtocolDataSubValue;      // Data sub request value

    ULONG   ProtocolDataOffset;         // The offset of data buffer is from beginning of this data structure.
    ULONG   ProtocolDataLength;

    ULONG   FixedProtocolReturnData;
    ULONG   ProtocolDataSubValue2;     // First additional data sub request value

    ULONG   ProtocolDataSubValue3;     // Second additional data sub request value
    ULONG   ProtocolDataSubValue4;     // Third additional data sub request value

    ULONG   ProtocolDataSubValue5;     // Fourth additional data sub request value
    ULONG   Reserved[5];
} STORAGE_PROTOCOL_SPECIFIC_DATA_EXT, *PSTORAGE_PROTOCOL_SPECIFIC_DATA_EXT;

若要指定要设置的 NVMe 功能类型,请配置 STORAGE_PROTOCOL_SPECIFIC_DATA_EXT 结构,如下所示:

  • 将 ProtocolType 字段设置为 ProtocolTypeNvme;
  • 将 DataType 字段设置为由 STORAGE_PROTOCOL_NVME_DATA_TYPE 定义的枚举值 NVMeDataTypeFeature;

以下示例演示 NVMe 功能集。

示例:NVMe 集功能

在此示例中,“设置功能”请求将发送到 NVMe 驱动器。 以下代码准备设置数据结构,然后通过 DeviceIoControl 将命令向下发送到设备。

            PSTORAGE_PROPERTY_SET                   setProperty = NULL;
            PSTORAGE_PROTOCOL_SPECIFIC_DATA_EXT     protocolData = NULL;
            PSTORAGE_PROTOCOL_DATA_DESCRIPTOR_EXT   protocolDataDescr = NULL;

            //
            // Allocate buffer for use.
            //
            bufferLength = FIELD_OFFSET(STORAGE_PROPERTY_SET, AdditionalParameters) + sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA_EXT);
            bufferLength += NVME_MAX_LOG_SIZE;

            buffer = new UCHAR[bufferLength];

            //
            // Initialize query data structure to get the desired log page.
            //
            ZeroMemory(buffer, bufferLength);

            setProperty = (PSTORAGE_PROPERTY_SET)buffer;

            setProperty->PropertyId = StorageAdapterProtocolSpecificProperty;
            setProperty->SetType = PropertyStandardSet;

            protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA_EXT)setProperty->AdditionalParameters;

            protocolData->ProtocolType = ProtocolTypeNvme;
            protocolData->DataType = NVMeDataTypeFeature;
            protocolData->ProtocolDataValue = NVME_FEATURE_HOST_CONTROLLED_THERMAL_MANAGEMENT;

            protocolData->ProtocolDataSubValue = 0; // This will pass to CDW11.
            protocolData->ProtocolDataSubValue2 = 0; // This will pass to CDW12.
            protocolData->ProtocolDataSubValue3 = 0; // This will pass to CDW13.
            protocolData->ProtocolDataSubValue4 = 0; // This will pass to CDW14.
            protocolData->ProtocolDataSubValue5 = 0; // This will pass to CDW15.

            protocolData->ProtocolDataOffset = 0;
            protocolData->ProtocolDataLength = 0;

            //
            // Send request down.
            //
            result = DeviceIoControl(m_deviceHandle,
                                     IOCTL_STORAGE_SET_PROPERTY,
                                     buffer,
                                     bufferLength,
                                     buffer,
                                     bufferLength,
                                     &returnedLength,
                                     NULL
            );

温度查询

在 Windows 10 中,IOCTL_STORAGE_QUERY_PROPERTY 还可用于查询 NVMe 设备的温度数据。

若要从 STORAGE_TEMPERATURE_DATA_DESCRIPTOR 中的 NVMe 驱动器检索温度信息,请按如下所示配置 STORAGE_PROPERTY_QUERY 结构:

  • 分配可以包含 STORAGE_PROPERTY_QUERY 结构的缓冲区。

  • 分别将控制器或设备/命名空间请求的 PropertyID 字段设置为 StorageAdapterTemperatureProperty 或 StorageDeviceTemperatureProperty。

  • 将 QueryType 字段设置为 PropertyStandardQuery。

此处显示了 STORAGE_TEMPERATURE_INFO 结构(来自 Windows 10)。

typedef struct _STORAGE_TEMPERATURE_INFO {

    USHORT  Index;                      // Starts from 0. Index 0 may indicate a composite value.
    SHORT   Temperature;                // Signed value; in Celsius.
    SHORT   OverThreshold;              // Signed value; in Celsius.
    SHORT   UnderThreshold;             // Signed value; in Celsius.

    BOOLEAN OverThresholdChangable;     // Can the threshold value being changed by using IOCTL_STORAGE_SET_TEMPERATURE_THRESHOLD.
    BOOLEAN UnderThresholdChangable;    // Can the threshold value being changed by using IOCTL_STORAGE_SET_TEMPERATURE_THRESHOLD.
    BOOLEAN EventGenerated;             // Indicates that notification will be generated when temperature cross threshold.
    UCHAR   Reserved0;
    ULONG   Reserved1;

} STORAGE_TEMPERATURE_INFO, *PSTORAGE_TEMPERATURE_INFO;

行为更改命令

操作系统更难处理操纵设备属性或可能影响设备行为的命令。 如果在处理 I/O 时运行时设备属性发生更改,如果未正确处理,则可能会出现同步或数据完整性问题。

NVMe Set-Features 命令是行为更改命令的一个很好的示例。 它允许更改仲裁机制和设置温度阈值。 为了确保在发送影响行为的设置命令时,未完成的数据不会面临风险,Windows 会暂停与 NVMe 设备的所有 I/O,清空队列并刷新缓冲区。 成功执行 set 命令后,将恢复 I/O(如果可能)。 如果无法恢复 I/O,可能需要重置设备。

设置温度阈值

Windows 10引入了 IOCTL_STORAGE_SET_TEMPERATURE_THRESHOLD,这是用于获取和设置温度阈值的 IOCTL。 还可以使用它来获取设备的当前温度。 此 IOCTL 的输入/输出缓冲区是上一代码部分中 STORAGE_TEMPERATURE_INFO 结构。

示例:设置超阈值温度

在此示例中,设置了 NVMe 驱动器的超阈值温度。 以下代码准备命令,然后通过 DeviceIoControl 将其发送到设备。

    BOOL    result;  
    ULONG   returnedLength = 0;  
    
    STORAGE_TEMPERATURE_THRESHOLD setThreshold = {0};  

    setThreshold.Version = sizeof(STORAGE_TEMPERATURE_THRESHOLD); 
    setThreshold.Size = sizeof(STORAGE_TEMPERATURE_THRESHOLD);  
    setThreshold.Flags = STORAGE_TEMPERATURE_THRESHOLD_FLAG_ADAPTER_REQUEST;  
    setThreshold.Index = SensorIndex;  
    setThreshold.Threshold = Threshold;  
    setThreshold.OverThreshold = UpdateOverThreshold; 

    //  
    // Send request down.  
    //  

    result = DeviceIoControl(DeviceList[DeviceIndex].Handle,  
                             IOCTL_STORAGE_SET_TEMPERATURE_THRESHOLD,  
                             &setThreshold,  
                             sizeof(STORAGE_TEMPERATURE_THRESHOLD),  
                             NULL,  
                             0,  
                             &returnedLength,  
                             NULL  
                             ); 

设置特定于供应商的功能

如果没有命令效果日志,驱动程序便无法得知命令的结果。 这就是需要命令效果日志的原因。 它可帮助操作系统确定某个命令是否具有较大的影响,以及是否可以与其他命令并行发送到驱动器。

命令效果日志还不够精细,无法包含特定于供应商的 Set-Features 命令。 出于此原因,尚无法发送特定于供应商的 Set-Features 命令。 但是,可以使用前面讨论的直通机制来发送特定于供应商的命令。 有关详细信息,请参阅直通机制

头文件

以下文件与 NVMe 开发相关。 这些文件包含在 Microsoft Windows 软件开发工具包 (SDK) 中。

头文件 说明
ntddstor.h 定义用于从内核模式访问存储类驱动程序的常量和类型。
nvme.h 用于其他 NVMe 相关的数据结构。
winioctl.h 用于整体 Win32 IOCTL 定义,包括用户模式应用程序的存储 API。