NVMe 드라이브 작업

적용 대상:

  • Windows 10
  • Windows Server 2016

Windows 애플리케이션에서 고속 NVMe 디바이스로 작업하는 방법을 알아봅니다. 디바이스 액세스는 Windows Server 2012 R2 및 Windows 8.1에서 처음 도입된 기본 제공 드라이버인 StorNVMe.sys를 통해 사용할 수 있습니다. KB 핫 픽스를 통해 Windows 7 디바이스에서도 사용할 수 있습니다. Windows 10에서는 공급업체별 NVMe 명령의 통과 메커니즘과 기존 IOCTL의 업데이트를 포함하여 여러 가지 새로운 기능이 도입되었습니다.

이 토픽에서는 Windows 10에서 NVMe 드라이브에 액세스하는 데 사용할 수 있는 범용 API에 대해 간략하게 설명합니다. 또한 다음을 설명합니다.

NVMe 드라이브 작업에 사용되는 API

다음 범용 API를 사용하여 Windows 10에서 NVMe 드라이브에 액세스할 수 있습니다. 이러한 API는 사용자 모드 애플리케이션인 경우 winioctl.h에서, 커널 모드 드라이버인 경우 ntddstor.h에서 찾을 수 있습니다. 헤더 파일에 대한 자세한 내용은 헤더 파일을 참조하세요.

  • 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: 이 구조에는 쿼리할 데이터를 지정하는 PropertyIdAdditionalParameters 필드가 포함됩니다. PropertyId 필드에서 STORAGE_PROPERTY_ID 열거형을 사용하여 데이터 형식을 지정합니다. 데이터 형식에 따라 AdditionalParameters 필드를 사용하여 세부 정보를 지정할 수 있습니다. 프로토콜 관련 데이터의 경우 AdditionalParameters 필드의 STORAGE_PROTOCOL_SPECIFIC_DATA 구조를 사용합니다. 온도 데이터의 경우 AdditionalParameters 필드의 STORAGE_TEMPERATURE_INFO 구조를 사용합니다.

  • STORAGE_PROPERTY_ID: 이 열거형에는 IOCTL_STORAGE_QUERY_PROPERTY가 프로토콜 관련 정보 및 온도 정보를 검색할 수 있는 새 값이 포함됩니다.

    • StorageAdapterProtocolSpecificProperty: If 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_QUERYAdditionalParameters 필드에 사용되고 STORAGE_PROTOCOL_NVME_DATA_TYPE 열거형 값이 지정된 경우 NVMe 관련 데이터를 검색합니다. 다음 STORAGE_PROTOCOL_NVME_DATA_TYPE 값 중 하나를 STORAGE_PROTOCOL_SPECIFIC_DATA 구조의 DataType 필드에 사용합니다.

    • NVMeDataTypeIdentify를 사용하여 컨트롤러 식별 데이터 또는 네임스페이스 식별 데이터를 가져옵니다.
    • NVMeDataTypeLogPage를 사용하여 로그 페이지(SMART/상태 데이터 포함)를 가져옵니다.
    • 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를 사용하려면 통과 명령뿐 아니라 모든 명령을 명령 효과 로그에 설명해야 합니다.

중요

StorNVMe.sys 및 Storport.sys는 디바이스에 대한 명령 중에서 명령 효과 로그에 설명되지 않은 명령을 모두 차단합니다.

 

명령 효과 로그 지원

명령 효과 로그(지원되는 명령 및 효과의 섹션 5.10.1.5 NVMe 사양 1.2에서 설명)를 사용하면 사양 정의 명령과 함께 공급업체별 명령의 효과를 설명할 수 있습니다. 이렇게 하면 명령 지원 유효성 검사와 명령 동작 최적화가 모두 가능하므로 디바이스가 지원하는 전체 명령 세트에서 이렇게 구현해야 합니다. 다음 조건은 명령 효과 로그 항목을 기반으로 명령을 보내는 방법의 결과를 설명합니다.

명령 효과 로그에 설명된 특정 명령에서...

While:

  • CSUPP(Command Supported)는 명령이 컨트롤러에서 지원됨을 의미하는 '1'로 설정됨(비트 01)

    참고

    CSUPP가 '0'(명령이 지원되지 않음을 의미)으로 설정되면 명령이 차단됩니다.

     

And if 다음 중 하나로 설정되면:

  • CCC(Controller Capability Change)는 명령이 컨트롤러 기능을 변경할 수 있음을 나타내는 '1'로 설정됨(비트 04)

  • NIC(Namespace Inventory Change)는 명령이 여러 네임스페이스의 수 또는 기능을 변경할 수 있음을 나타내는 '1'로 설정됨(비트 03)

  • NCC(Namespace Capability Change)는 명령이 단일 네임스페이스의 기능을 변경할 수 있음을 나타내는 '1'로 설정됨(비트 02)

  • CSE(Command Submission and Execution)는 동일한 네임스페이스 또는 그 어떤 네임스페이스에도 다른 미해결 명령이 없으면 명령을 제출할 수 있으며 이 명령이 완료되기 전에는 다른 명령을 동일한 네임스페이스 또는 그 어떤 네임스페이스에도 제출하면 안 된다는 것을 의미하는 001b 또는 010b로 설정됨(비트 18:16).

Then 명령이 어댑터에 대한 유일한 미해결 명령으로 전송됩니다.

Else if:

  • CSE(Command Submission and Execution)는 동일한 네임스페이스에 다른 미해결 명령이 없으면 명령을 제출할 수 있으며 이 명령이 완료되기 전에는 다른 명령을 동일한 네임스페이스에 제출하면 안 된다는 것을 의미하는 001b로 설정됨(비트 18:16).

Then 명령이 LUN(논리 단위 번호 개체)에 대한 유일한 미해결 명령으로 전송됩니다.

Otherwise, 명령이 억제 없이 다른 미해결 명령과 함께 전송됩니다. 예를 들어 사양이 정의되지 않은 통계 정보를 검색하기 위해 공급업체별 명령이 디바이스로 전송되면 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을 전송합니다. IOCTL은ntddstor.h에 있는 STORAGE_PROTOCOL_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의 목적은 공급업체별 명령을 보내는 것입니다. Identify 등의 다른 관리자 또는 비 공급업체 관련 특정 NVMe 명령을 보내려면 이 통과 IOCTL을 사용하면 안 됩니다. 예를 들어 IOCTL_STORAGE_QUERY_PROPERTY는 Identify 또는 Get Log Pages에 사용해야 합니다. 자세한 내용은 다음 섹션인 프로토콜 관련 쿼리를 참조하세요.

통과 메커니즘을 통해 펌웨어를 업데이트하지 마세요.

펌웨어 다운로드 및 활성화 명령은 통과를 사용하여 전송하면 안 됩니다. IOCTL_STORAGE_PROTOCOL_COMMAND는 공급업체별 명령에만 사용해야 합니다.

애플리케이션이 SCSI_miniport 버전의 펌웨어 IOCTL을 직접 사용하지 않도록 다음과 같은 일반 스토리지 IOCTL(Windows 10에서 도입됨)을 사용하세요. 스토리지 드라이버는 IOCTL을 SCSI 명령으로 변환하거나 SCSI_miniport 버전의 IOCTL을 미니포트로 변환합니다.

다음은 Windows 10 및 Windows Server 2016에서 펌웨어 업그레이드 도구를 개발하는 데 권장되는 IOCTL입니다.

스토리지 정보를 가져오고 펌웨어를 업데이트할 수 있도록 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은 Get Log Pages, Get FeaturesIdentify와 같은 일반적으로 요청되는 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_PROPERTY_QUERYSTORAGE_PROTOCOL_SPECIFIC_DATA 구조를 모두 포함할 수 있는 버퍼를 할당합니다.

  • PropertyID 필드를 각각 컨트롤러 또는 디바이스/네임스페이스 요청에 대한 StorageAdapterProtocolSpecificProperty 또는 StorageDeviceProtocolSpecificProperty로 설정합니다.

  • QueryType 필드를 PropertyStandardQuery로 설정합니다.

  • STORAGE_PROTOCOL_SPECIFIC_DATA 구조를 원하는 값으로 채웁니다. STORAGE_PROTOCOL_SPECIFIC_DATA의 시작은 STORAGE_PROPERTY_QUERYAdditionalParameters 필드입니다.

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/상태 데이터 포함)를 가져옵니다.
    • NVMeDataTypeFeature를 사용하여 NVMe 드라이브의 기능을 가져옵니다.

ProtocolTypeNVMeProtocolType으로 사용되는 경우 프로토콜 관련 정보 쿼리를 NVMe 드라이브의 다른 I/O와 병렬로 검색할 수 있습니다.

중요

StorageAdapterProtocolSpecificPropertySTORAGE_PROPERTY_ID를 사용하고 STORAGE_PROTOCOL_SPECIFIC_DATA 또는 STORAGE_PROTOCOL_SPECIFIC_DATA_EXT 구조가 ProtocolType=ProtocolTypeNvmeDataType=NVMeDataTypeLogPage로 설정된 IOCTL_STORAGE_QUERY_PROPERTY의 경우 동일한 구조의 ProtocolDataLength 멤버를 최솟값인 512(바이트)로 설정합니다.

다음 예제에서는 NVMe 프로토콜 관련 쿼리를 보여줍니다.

예제: NVMe Identify 쿼리

이 예제에서는 Identify 요청을 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"));
        }
    }

  

중요

StorageAdapterProtocolSpecificPropertySTORAGE_PROPERTY_ID를 사용하고 STORAGE_PROTOCOL_SPECIFIC_DATA 또는 STORAGE_PROTOCOL_SPECIFIC_DATA_EXT 구조가 ProtocolType=ProtocolTypeNvmeDataType=NVMeDataTypeLogPage로 설정된 IOCTL_STORAGE_QUERY_PROPERTY의 경우 동일한 구조의 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 Get Log Pages 쿼리

이 예제에서는 이전 예제를 기반으로 Get Log Pages 요청을 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_DATA 또는 STORAGE_PROTOCOL_SPECIFIC_DATA_EXT 구조가 공급업체별 데이터의 512바이트 청크를 요청하도록 ProtocolDataRequestValue=VENDOR_SPECIFIC_LOG_PAGE_IDENTIFIER 설정되어 있습니다.

예제: NVMe Get Features 쿼리

이 예제에서는 이전 예제를 기반으로 Get Features 요청을 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"));
    }

프로토콜별 set

Windows 10 19H1부터 NVMe Set Features를 지원하도록 IOCTL_STORAGE_SET_PROPERTY가 향상되었습니다.

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 feature set를 보여줍니다.

예제: NVMe Set Features

이 예제에서는 Set Features 요청을 NVMe 드라이브로 전송합니다. 다음 코드는 set data 구조를 준비한 다음, 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 명령은 동작 변경 명령의 좋은 예입니다. 이 명령은 중재 메커니즘의 변경 및 온도 임계값 설정을 허용합니다. 동작에 영향을 주는 set 명령이 전송될 때 진행 중인 데이터가 위험에 노출되지 않도록, Windows는 NVMe 디바이스, 드레이닝 큐 및 플러시 버퍼에 대한 모든 I/O를 일시 중지합니다. set 명령이 성공적으로 실행되면 I/O가 다시 시작됩니다(가능한 경우). I/O를 다시 시작할 수 없는 경우 디바이스 초기화가 필요할 수 있습니다.

온도 임계값 설정

Windows 10에서는 온도 임계값을 가져오고 설정하는 IOCTL인 IOCTL_STORAGE_SET_TEMPERATURE_THRESHOLD가 도입되었습니다. 디바이스의 현재 온도를 가져오는 데 사용할 수도 있습니다. 이 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 사용자 모드 애플리케이션에 대한 스토리지 API를 비롯한 전체 Win32 IOCTL 정의에 대한 헤더 파일입니다.