Windowsの標準NVMeドライバでNVMe SSDにアクセスする(Identify)

# はじめに
　別記事<sup>[1]</sup>にて、Windows 10（以降Windowsと記載します）の標準NVMeデバイスドライバを使用して、ユーザ空間で動作するプログラムからNVMe SSDにコマンドを発行する方法を紹介しました。

　その記事でも書きましたが、NVMe SSDを色々いじるにあたって、対象となるNVMe SSDがサポートするコマンドや機能を予め知っておく必要があります。

　この記事では、上記「NVMe SSDがサポートするコマンドや機能」の情報取得に使用するコマンドの一つである`Identify`コマンドを、Windowsの標準NVMeデバイスドライバを使用して、ユーザ空間で動作するプログラムから発行する方法についてまとめます。

　なお、この記事でまとめた内容の動作確認環境は、別記事<sup>[1]</sup>の動作確認環境と同一ですので、そちらを参照してください。また、完全なコードは[Github](https://github.com/ken-yossy/nvmetool-win)に置いてありますのでそちらをご覧ください。

# まとめ
* `Identify`コマンドとは、NVMe SSDがサポートするコマンドや機能といった最も基本的な情報を取得するためのコマンドの一つである
* `Identify`コマンド発行時は、情報を取得する対象を指定する必要がある
    * コントローラや特定ネームスペースを指定して情報を取得する
* `Identify`コマンドは、Windowsの標準NVMeデバイスドライバで発行することができる！

# NVMeのIdentifyコマンドとは
　NVMe仕様<sup>[2]</sup>における`Identify`コマンドとは、NVMeデバイス（面倒なので、以降「デバイス」＝「SSD」として"SSD"と記載します）の基本情報やSSDが備える機能に関する情報を取得するコマンドです。例えば、SSDのモデル名やファームウェアバージョン、特定コマンドや機能のサポート有無、さらにはSSDが備える低消費電力モードの数や具体的な消費電力の値、などが含まれます。

　例えばSSDメーカーは、自社製品向けのユーティリティソフトウェアを配布していることが多く、そのソフトウェアにはたいてい「ファームウェアアップデート機能」がついています。ファームウェアをアップデートするには、対象のSSDで動作しているファームウェアのバージョンを取得する必要があります。対象のSSDがNVMe SSDの場合はこのような場面で`Identify`コマンドを使うことが考えられます。

　取得できる内容に違いはありますが、このコマンドは、SATA SSD/HDDなどで使用できるATA Command Set (ACS)<sup>[3]</sup>で定義されている`IDENTIFY DEVICE`コマンドや、SCSI Primary Commands (SPC)<sup>[4]</sup>で定義されている`INQUIRY`コマンドと同様の機能を持つコマンドだと言えます。

　したがって、NVMe SSDの素性を調べるには、さらにはNVMe SSDのことをより深く知る（？）ためには、まずは`Identify`コマンドを発行してその結果を受け取ることから始める必要があります。

# コマンド発行方法
　早速、`Identify`コマンドを発行してその結果を受け取る方法の説明に移ります。

　ただその前に、`Identify`コマンド発行時に指定が必要な引数である「情報取得対象」の種類と設定方法について説明します。

## 情報取得対象の種類と指定方法
　NVMeの`Identify`コマンドは、発行時に"Controller or Namespace Structure (CNS)"と呼ばれるパラメータを設定する必要があります。このパラメータを与えることで、コマンドで情報を取得する対象を細かく設定することができます。

　具体的なCNS Valueの値は"5.15.1 Identify command overview"節のFigure 109に記載されています。結構多くて複雑です（下図は仕様書Figure 109の一部を抜粋したもの）。とはいえ、とりあえず必要になるのは、コントローラを指定する01hと、ネームスペースを指定する00hです。私たちが普通に入手できるNVMe SSDであれば、この2つの機能の情報が取得できれば十分です。

![IdentifyコマンドのCNS値（抜粋）](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439683/eeb083b0-6ba2-3d72-8875-5ed6d527f533.png "IdentifyコマンドのCNS値（抜粋）")

## コントローラのIdentify情報取得
　まずは、コントローラの情報を取得する方法について説明します。

　NVMe仕様における「コントローラ」は、物理的なモノ（ハードウェア）という意味合いが強いです。ですので、コントローラを指定した`Identify`コマンドで取得できる情報は、コントローラがサポートするコマンドや機能、コントローラを最適に使用する際の設定、コントローラが備える省電力モードとその仕様、などです。

　一方、SSDが備える記憶領域を論理的に分割した各論理領域毎の情報は、コントローラを指定した`Identify`コマンドでは取得できません。NVMeでは、この「SSDが備える記憶領域を論理的に分割した各論理領域」のことを「ネームスペース(Namespace)」と呼びます。ネームスペースの情報を取得する方法については次の節で説明します。

　以下のフローチャートは、Windowsの標準NVMeデバイスドライバを使用してNVMe SSDに`Identify`コマンドを発行する手順です。

![Identifyコマンド発行フローチャート](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439683/6b8b2683-ec0f-0fbd-ca77-ecd4093dfe00.png "Identifyコマンド発行フローチャート")

　デバイスハンドルを取得する方法は、別記事<sup>[1]</sup>で説明した通りですので、説明は省略します。

　「コマンド発行用構造体作成」以降の処理について、以下にコード片を示します。行頭の行番号は、説明のために付与したものです。エラー処理に関するコードは省略しています。

　なお、このコードは、Microsoftの説明<sup>[5]</sup>にしたがって書き起こしたものです。

```c
 1: PSTORAGE_PROPERTY_QUERY query = NULL;
 2: PSTORAGE_PROTOCOL_SPECIFIC_DATA protocolData = NULL;
 3: PSTORAGE_PROTOCOL_DATA_DESCRIPTOR protocolDataDescr = NULL;
 4: PNVME_IDENTIFY_CONTROLLER_DATA pData = NULL;

 5: bufferLength = offsetof(STORAGE_PROPERTY_QUERY, AdditionalParameters)
                + sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA)
                + sizeof(NVME_IDENTIFY_CONTROLLER_DATA);
 6: buffer = malloc(bufferLength);
 7: ZeroMemory(buffer, bufferLength);

 8: query = (PSTORAGE_PROPERTY_QUERY)buffer;
 9: protocolDataDescr = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;
10: protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;

11: query->PropertyId = StorageAdapterProtocolSpecificProperty;
12: query->QueryType = PropertyStandardQuery;

13: protocolData->ProtocolType = ProtocolTypeNvme;
14: protocolData->DataType = NVMeDataTypeIdentify;
15: protocolData->ProtocolDataRequestValue = NVME_IDENTIFY_CNS_CONTROLLER;
16: protocolData->ProtocolDataRequestSubValue = 0;
17: protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
18: protocolData->ProtocolDataLength = sizeof(NVME_IDENTIFY_CONTROLLER_DATA);

19: iResult = DeviceIoControl(_hDevice, IOCTL_STORAGE_QUERY_PROPERTY,
           buffer, bufferLength, buffer, bufferLength, &returnedLength, NULL);
20: pData = (PNVME_IDENTIFY_CONTROLLER_DATA)((PCHAR)protocolData + protocolData->ProtocolDataOffset);
```

　5行目に登場する構造体`STORAGE_PROPERTY_QUERY`は、ストレージに問い合わせ（クエリ）をするようなコマンドを発行する際に使用するデータ構造です。別記事<sup>[1]</sup>で紹介したReadコマンドやWriteコマンドの場合とは異なり、クエリタイプの設定（12行目）が必要です。

　また、SATA (ACS)やSCSI (SPC)に類似コマンドが存在するとはいえ、NVMeの`Identify`コマンドは少なくともデータフォーマットは独自ですから、発行するコマンドで取得する内容がプロトコル特有であることを示した上で（11行目）、そのプロトコルがNVMeであることを明示的に示す必要があります（13行目）。

　そして、問い合わせた結果受け取るデータが`Identify`コマンドのデータ構造であること（14行目）と、`Identify`コマンドでの情報取得対象がコントローラであること（15行目、CNS Value = 01h）を設定します。

　一方、5行目でバッファサイズを計算して6行目でメモリを確保しているのが、コマンド発行用構造体です。

　`Identify`コマンドによる情報取得対象がコントローラの場合は、SSDが返してくるデータ構造を定義した構造体`NVME_IDENTIFY_CONTROLLER_DATA`の分を加算したサイズのメモリを確保する必要があります。

　構造体`NVME_IDENTIFY_CONTROLLER_DATA`は、Windows Driver Kit (WDK)に含まれているヘッダファイル`nvme.h`内で定義されている、`Identify`コマンドで返されるデータの構造体です。
　なお、この構造体は、ヘッダファイル中には「NVMe v1.3に準拠しているよ（意訳）」と記載されているのですが、実際にはいくつかのフィールドが定義されていない（v1.3仕様に追随できていない）模様です。このため、取得した情報の解析時には注意が必要です。GitHubに置いてあるコードでは、自前で構造体を定義して使用しています。プログラムを1.3より前のバージョンや1.3より先のバージョンに対応させる場合は、バージョン違いの構造体を定義して使い分けるのが手っ取り早いと思います。

　これらの設定を行い、19行目で`DeviceIoControl()`を呼び出して、コマンド発行を要求します。

　`DeviceIoControl()`が成功すると、6行目で確保したメモリ領域内に、SSDから送信されたコントローラの情報が格納されますので、20行目のように構造体のポインタにキャストして参照します。

　受信したコントローラの情報は、仕様書"5.15.3 Identify Controller data structure (CNS 01h)"のFigure 112に記載されているデータ構造にしたがって解析します。巨大なデータ構造ですが、とにかく仕様書に記載されている通りに解釈すればOKです。

　具体的には、以下のように情報を取得できます（抜粋。フィールド名や説明文はプログラム中で付与したものであってSSDから受信したものではありません）。これはとあるノートPCに装着されていたNVMe SSDの情報です。

```
[M] Model Number (MN): WDC PC SN720 SDAQNTW-512G-1001
[M] Firmware Revision (FR): 10160101
```

　他には、例えばサポートされるネームスペース数(NN)や、Dataset Managementコマンド（Deallocateを行うために必要）のサポート状況（ONCSフィールドのビット2）、そして揮発性のライトキャッシュの有無(VWC)、などがわかります（下図）。

```
[M] Number of Namespaces (NN): 1
[M] Optional NVM Command Support (ONCS):
        bit [      6] 0 = The controller does not support the Timestamp feature
        bit [      5] 0 = The controller does not support reservations
        bit [      4] 1 = The controller supports the Save field set to a non-zero value in the Set Features command and the Select field set to a non-zero value in the Get Features command
        bit [      3] 1 = The controller supports the Write Zeroes command
        bit [      2] 1 = The controller supports the Dataset Management command
        bit [      1] 1 = The controller supports the Write Uncorrectable command
        bit [      0] 1 = The controller supports the Compare command
[M] Volatile Write Cache (VWC):
        bit [      0] 1 = A volatile write cache is present
```

　この結果を見ると、このSSDのコントローラは、ネームスペースは1つのみサポート、Dataset Managementコマンドはサポート、揮発性ライトキャッシュもサポート、という仕様のようです。

　NVMeのIdentify Controller Dataのフィールド数は（他のプロトコル同様）とても多いので、この記事では説明しません（説明しきれません）。今後NVMeの紹介記事（？）の中で説明できれば、と考えています。

## ネームスペースのIdentify情報取得
　次に、ネームスペースの情報を取得する方法について説明します。

　前節で説明したように、ネームスペースとは、SSDが備える記憶領域を論理的に分割したときの各領域のことを指します。いわゆる「パーティション」に近い概念ですが、パーティションよりもより独立した機能で、より柔軟な設定が可能です。ネームスペースの詳細についてもこの記事では触れません。

　`Identify`コマンドを使用してネームスペースの情報を取得する方法は、前節で説明した方法とほとんど変わりません。フローチャートは同じで、`Identify`コマンドの引数をいくつか修正する程度です。

　以下にエラー処理等を省略したコード片を示します。

```c
 1: PSTORAGE_PROPERTY_QUERY query = NULL;
 2: PSTORAGE_PROTOCOL_SPECIFIC_DATA protocolData = NULL;
 3: PSTORAGE_PROTOCOL_DATA_DESCRIPTOR protocolDataDescr = NULL;
 4: PNVME_IDENTIFY_NAMESPACE_DATA pData = NULL;

 5: bufferLength = offsetof(STORAGE_PROPERTY_QUERY, AdditionalParameters)
	+ sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA)
	+ sizeof(NVME_IDENTIFY_NAMESPACE_DATA);
 6: buffer = malloc(bufferLength);

 7: ZeroMemory(buffer, bufferLength);

 8: query = (PSTORAGE_PROPERTY_QUERY)buffer;
 9: protocolDataDescr = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;
10: protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;

11: query->PropertyId = StorageAdapterProtocolSpecificProperty;
12: query->QueryType = PropertyStandardQuery;

13: protocolData->ProtocolType = ProtocolTypeNvme;
14: protocolData->DataType = NVMeDataTypeIdentify;
15: protocolData->ProtocolDataRequestValue = NVME_IDENTIFY_CNS_SPECIFIC_NAMESPACE;
16: protocolData->ProtocolDataRequestSubValue = 1; // target NSID
17: protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
18: protocolData->ProtocolDataLength = sizeof(NVME_IDENTIFY_NAMESPACE_DATA);

19: iResult = IssueDeviceIoControl(_hDevice, IOCTL_STORAGE_QUERY_PROPERTY, buffer,
        bufferLength, buffer, bufferLength, &returnedLength, NULL);

20: pData = (PNVME_IDENTIFY_NAMESPACE_DATA)((PCHAR)protocolData + protocolData->ProtocolDataOffset));
```

　コントローラの情報を取得する場合と異なるのは以下の点です。

* コマンドの結果を取得するための受け皿になる構造体が`NVME_IDENTIFY_NAMESPACE_DATA`であること
* CNS Valueに`NVME_IDENTIFY_CNS_SPECIFIC_NAMESPACE`を指定すること（15行目）
* 情報を取得する対象のネームスペースの識別子(NSID)を指定すること（16行目、ここでは1を指定している）
    * NVMeのNSIDは「0から」ではなく「1から」なので注意

　他の部分は、コントローラの情報を取得した際と基本的に同じです。

　具体的には、以下のように情報を取得できます（抜粋。フィールド名や説明文はプログラム中で付与したものであって、SSDから受信したものではありません）。これはとあるノートPCに装着されていたNVMe SSDの情報です。

　取得できた情報の中には、ネームスペースのセクタ数(NSZE)、LBA（セクタ）のフォーマット(LBAF0)、Deallocateされたセクタに関する仕様(DLFEAT)、などがあります（下記）。

```
[M] Namespace Size (NSZE): 1000215216 (sectors)
[M] LBA Format 0 Support (LBAF0):
        bit [ 25: 24] 2 = Relative Performance (RP): Good performance
        bit [ 23: 16] 9 = LBA Data Size (LBADS): 512 bytes / sector
        bit [ 15:  0] 0 = Metadata Size (MS): 0 bytes
[O] Deallocate Logical Block Features (DLFEAT):
        bit [      4] 0 = The Guard field for the deallocated logical blocks that contain protection information is set to FFFFh.
        bit [      3] 1 = The controller supports the Deallocate bit in the Write Zeroes command for this namespace.
        bit [  2:  0] 1 = A deallocated logical block returns all bytes cleared to 0h
```

　この結果を見ると、NSID=1のネームスペースは、サイズが512 GB[^1]、セクタのサイズは512バイトでメタデータはなし、Deallocateされたままのセクタをリードした場合はオールゼロを返す、という仕様であることがわかります。

[^1]: JEDECの定義に基づいて計算。(1000215216 - 21168)÷1953504 = 512

# まとめ
　この記事では、NVMe SSDの最も基本的な情報を取得するためのコマンドである`Identify`コマンドを、Windowsの標準NVMeデバイスドライバを使用して発行する方法をまとめました。

　`Identify`コマンドの結果を見ることでNVMe SSDの機能を深く知ることができます。また、複数製品の`Identify`コマンドの結果を比較することで、メーカー間でどんな機能（仕様）が同じでどんな機能が異なるのか、という知見を得ることもできます。

　この記事で紹介した方法は、Windowsの標準NVMeデバイスドライバを使用していますので、「自分でコードを書いてNVMe SSDにアクセスする」という目的に対して、とても手軽に試せることが特徴です。

　次の記事では、Windowsの標準NVMeデバイスドライバを使用して`Get Log Page`というコマンドを発行し、NVMe SSDからS.M.A.R.T (Self-Monitoring, Analysis and Reporting Technology)情報を含む各種ログ情報を取得する方法についてまとめたいと思います。

# 参考文献
[1] ["Windowsの標準NVMeドライバでNVMe SSDにアクセスする(Read/Write/Dataset Management)"](https://qiita.com/ken-yossy/items/30f0dd206713aba770f7), July 3, 2019 
[2] NVM Express, _"NVM ExpressTM Base Specification"_, Revision 1.3d, March 20, 2019
[3] T13 / International Committee for Information Technology Standards (INCITS), _"ATA Command Set - 4 (ACS-4)"_, Revision 14, October 14, 2016
[4] T10 / International Committee for Information Technology Standards (INCITS), _"SCSI Primary Commands - 4 (SPC-4)"_, Revision 23, February 9, 2010
[5] Microsoft, [_"Working with NVMe drives"_](https://docs.microsoft.com/en-us/windows/desktop/fileio/working-with-nvme-devices), 最終閲覧日2019年5月14日

# ライセンス表記
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="クリエイティブ・コモンズ・ライセンス" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br />この記事は <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンス</a>の下に提供されています。