Help us understand the problem. What is going on with this article?

Windowsの標準NVMeドライバでNVMe SSDにアクセスする(Read/Write/Dataset Management)

More than 1 year has passed since last update.

はじめに

 最近では、インターフェースプロトコルにNVMeを採用したSSDが容易に入手できるようになりました。一方で、NVMeについて、具体的にどのようなことができるプロトコルなのか、ということは、ユーザの視点ではあまり認知されていない印象です。
 もちろん、ユーザの視点では、プロトコルのことなど意識せずにデータの読み書きができれば良い、とも言えますが...

 それでも、NVMe SSDを使用するにあたって、NVMeで定義されている機能(仕様)、そしてユーザからどんなことができるのか、ということが明らかになっていることには意義があると考えます。

 その一環として、Windows 10(以降Windowsと表記します)のユーザ空間で動作するアプリケーションプログラムから、Windowsの標準NVMeデバイスドライバ1を使用して、NVMe SSDに対してNVMeの仕様で規定されている各種コマンドが発行可能かどうかを調査しました。
 ユーザ空間で動作するアプリケーションプログラムは誰でも作成できますから、この方法でできることは、NVMe SSDさえあれば誰でもできることになります。

 調査の結果、いくつかのコマンドが発行可能であることがわかりました。
 また、発行したいコマンドによって3つの方法から適切なものを選択しなければならないこともわかりました。

 この記事では、この調査内容についてまとめます。

 なお、作成したプログラムはGitHubに置いてあります(こちら)。記事の中ではコード片のみ示しますので、完全なコードをご覧になる場合はGitHubに置いたコードを参照してください。

まとめ

  • Windowsの標準NVMeデバイスドライバを使ってNVMe SSDにコマンドを発行する際は、発行したいコマンドに合わせて適切な方法を選択する必要がある
  • Read, Write, Dataset Management (Deallocate)が発行できる!
    • 「ドライブ丸ごとDeallocate (TRIM)」もできる!

動作確認環境

 この記事に記載したプログラムの開発と動作確認に用いた環境は以下の通りです。

項目 内容 備考
マザーボード ASUS PRIME Z370-A
CPU Intel Core i5-8400 2.8GHz
DRAM 8 GB
SSD ADATA ASX8200PNP-256GT-C NVMe SSD (M.2 2280)
項目 内容 備考
OS Windows 10 Professional 64bit Version 1809, Build 17763.475
NVMe デバイスドライバ Microsoft製標準NVMeデバイスドライバ (stornvme.sys) Version 10.0.17763.404 (WinBuild 160101.0800)
DRAM 8 GB
開発環境 Microsoft Visual Studio Community 2017 Version 15.9.11
Microsoft Visual C++ 2017 00369-60000-00001-AA495
Windows Driver Kit 10.0.17740.1000

 なお、Windows Driver KitはVisual Studio Installerではインストールできません。このため、Webページ[8]に記載の手順に従ってインストールする必要があります。

Windowsのストレージアーキテクチャ

 具体的なアクセス方法の説明の前に、Windowsのストレージアーキテクチャについて、一部推測も含みますが、簡単にまとめます。

 Windowsのストレージアーキテクチャは、デバイスドライバを含めた各種プログラムやライブラリが階層構造を構成しています[5][6][7][9][10][13]

Windowsのストレージアーキテクチャ

 通常、ユーザランドで動作するアプリケーションプログラムがストレージにアクセスする場合、アクセスの対象としてファイルを指定します。例えばファイルの読み書き処理です。
 これらの処理要求は、アクセス対象のファイルを管理するファイルシステム、ファイルシステムが存在するボリュームやパーティションの管理プログラム(マネージャ)、などを経由してストレージへのアクセス要求に変換され、デバイスドライバに到達します(図中の「ファイル読み書きなどのアクセス」のパス)。

 これとは別に、アクセスの対象としてファイルではなくデバイスを指定して直接デバイスを操作するインターフェースも存在します。これはUNIX系OSでは一般的にioctl()というインターフェースであり、WindowsではDeviceIoControl()というインターフェースが該当します(図中の「DeviceIoControl()によるアクセス」のパス)。

 この記事で対象とするアクセスは、後者のインターフェースを使って実現されます。

 クラスドライバよりデバイス側を拡大すると下図のようになります。

Windowsのストレージアーキテクチャ(拡大)

 この図の通り、クラスドライバが受け付けたI/O Request Packet (IRP)形式の要求は、一旦SCSI Request Block (SRB)という形式に変換されて、適切なポートドライバに送信されます。

 Windowsが標準で備えるDirect Attached Storage (DAS)用のポートドライバは、SCSIポートドライバ、Storportドライバ、ATAポートドライバ、の3種類であり[7][13]、NVMeデバイスドライバはポートドライバではなくミニポートドライバです。

 ミニポートドライバは、デバイスドライバインターフェイスを使用せずポートドライバインターフェースを使用するため[13]、NVMeデバイスドライバを使用するには、クラスドライバおよびポートドライバ(インターフェース)を経てミニポートドライバに要求を届ける必要があります。

 送受信するデータの構造などがNVMeに特有のコマンドは、要求がNVMeデバイス向けであることを明示することで、ポートドライバが要求内容の解釈をせずにミニポートドライバであるNVMeデバイスドライバに要求を送信します。このようなコマンドには、ベンダ固有コマンド(Vendor Specific commands)も含まれます。
 Windowsでは、このようなコマンドに専用のIOCTLコードが定義されています。

 一方、NVMeデバイスドライバを間接的に使用することで発行が可能なコマンドも存在します。

 そのようなコマンドは、SCSIなど他のプロトコルにも類似コマンドが定義されていて、かつコマンドで送受信するデータの構造がプロトコル間でほとんど変わらない、一部のコマンドです。

 それらのコマンドの発行を要求する際は、要求がNVMeデバイス向けであることを明示しなくても、ポートドライバが要求内容の解釈を行い、アクセス先デバイスがNVMeデバイスであることを判断して当該デバイスに対応するミニポートドライバであるNVMeデバイスドライバに要求を送信します。
 Windowsの場合、このようなコマンドは、SCSI Pass-through機構を用いるか、専用のIOCTLコードを用いることで、発行が可能です。

 以上をまとめると、Windows標準のNVMeデバイスドライバを使用してNVMe SSDにコマンドを発行する方法は、以下の3つとなります。

  1. 専用IOCTLコードを用いてNVMeデバイスドライバにコマンド発行を要求するもの
    • コマンド具体例:Identify、Get Log Page、ベンダコマンド
  2. 専用IOCTLコードを用いてポートドライバにコマンド発行を要求するもの
    • コマンド具体例:Deallocate (Dataset Management)
  3. SCSI Pass-through機構を使用してポートドライバにコマンド発行を要求するもの
    • コマンド具体例:Read, Write, Flush

コマンド発行方法

 さて、前置きが長くなりました。早速、Windows標準NVMeデバイスドライバを使用してNVMe SSDにコマンドを発行する方法を、具体的にコード片を示しながら説明します。

 発行方法を説明するコマンドは、Write、Read、Dataset Managementの3つです。なお、Dataset ManagementコマンドについてはDeallocate処理のみ説明します。

Readコマンド

 ここでは、LBA = 0のセクタのデータを読み出すReadコマンドを発行する方法を説明します。

 前節で説明した通り、Readコマンド発行にはSCSI Pass-throughという機構を用います。

 Readコマンドを発行する処理のエラー処理を除くフローチャートは以下のようになります。

Readコマンド発行処理フローチャート(エラー処理除く)

 まずデバイスのハンドルを取得します。この処理は、Windowsにおいてデバイス(ファイル含む)を扱うための基本的な処理です。Unix系OSでのopen()インターフェースに相当します。このデバイスのハンドル取得処理についてはこちらを参照してください。

 次に、適切なSCSI Pass-through要求用構造体を選択するため、SCSI Request Block (SRB)の型チェックを行います。このチェックは参考文献[2]に記載の通りに行えばOKです。

 そして、型チェックの結果に従って適切な構造体を選択し、選択したSCSI Pass-through要求用構造体に必要な情報を設定し、その構造体を指定してDeviceIoControl()を呼び出し、コマンド発行を要求します。

 コード片で示すと以下のようになります。コード中の行番号は説明のために付与したものです。

01: typedef struct _SCSI_PASS_THROUGH_WITH_BUFFERS_EX
02: {
03:     SCSI_PASS_THROUGH_EX spt;
04:     UCHAR                ucCdbBuf[SPT_CDB_LENGTH - 1]; // SPT_CDB_LENGTH == 32
05:     ULONG                Filler;      // realign buffers to double word boundary
06:     STOR_ADDR_BTL8       StorAddress;
07:     UCHAR                ucSenseBuf[SPT_SENSE_LENGTH]; // SPT_SENSE_LENGTH == 32
08:     UCHAR                ucDataBuf[SPTWB_DATA_LENGTH]; // SPTWB_DATA_LENGTH == 512
09: } SCSI_PASS_THROUGH_WITH_BUFFERS_EX, *PSCSI_PASS_THROUGH_WITH_BUFFERS_EX;

10: SCSI_PASS_THROUGH_WITH_BUFFERS_EX sptwb_ex;
11: ZeroMemory(&sptwb_ex, sizeof(SCSI_PASS_THROUGH_WITH_BUFFERS_EX));

12: sptwb_ex.spt.Version               = 0;
13: sptwb_ex.spt.Length                = sizeof(SCSI_PASS_THROUGH_EX);
14: sptwb_ex.spt.ScsiStatus            = 0;
15: sptwb_ex.spt.CdbLength             = CDB10GENERIC_LENGTH; // == 10
16: sptwb_ex.spt.StorAddressLength     = sizeof(STOR_ADDR_BTL8);
17: sptwb_ex.spt.SenseInfoLength       = SPT_SENSE_LENGTH;
18: sptwb_ex.spt.DataOutTransferLength = 0;
19: sptwb_ex.spt.DataInTransferLength  = 512; // data size to be read
20: sptwb_ex.spt.DataDirection         = SCSI_IOCTL_DATA_IN;
21: sptwb_ex.spt.TimeOutValue          = 2;
22: sptwb_ex.spt.StorAddressOffset     = offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS_EX, StorAddress);
23: sptwb_ex.StorAddress.Type          = STOR_ADDRESS_TYPE_BTL8;
24: sptwb_ex.StorAddress.Port          = 0;
25: sptwb_ex.StorAddress.AddressLength = STOR_ADDR_BTL8_ADDRESS_LENGTH;
26: sptwb_ex.StorAddress.Path          = 0;
27: sptwb_ex.StorAddress.Target        = 0;
28: sptwb_ex.StorAddress.Lun           = 0;
29: sptwb_ex.spt.SenseInfoOffset       = offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS_EX, ucSenseBuf);
30: sptwb_ex.spt.DataOutBufferOffset   = 0;
31: sptwb_ex.spt.DataInBufferOffset    = offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS_EX, ucDataBuf);
32: sptwb_ex.spt.Cdb[0]                = SCSIOP_READ;
33: sptwb.spt.Cdb[5]                   = 0; // Starting LBA
34: sptwb.spt.Cdb[8]                   = 1; // TRANSFER LENGTH

35: length = sizeof(SCSI_PASS_THROUGH_WITH_BUFFERS_EX);

36: status = DeviceIoControl(
       _hDevice,
       IOCTL_SCSI_PASS_THROUGH_EX,
       &sptwb_ex,
       length,
       &sptwb_ex,
       length,
       &returned,
       FALSE);

 1行目で定義している構造体のメンバの中のSCSI_PASS_THROUGH_EX構造体が、SCSI Pass-through要求用構造体です。この構造体はヘッダファイルntddscsi.h内で定義されています。このSCSI Pass-through要求用構造体に情報を設定しているのが12行目から22行目です。

 この中で重要なのは、15行目でCommand Descriptor Block (CDB)長として10を指定していること、19行目で512(バイト)を指定していること、そして20行目でデータ転送の方向を指定していることです。

 今回、READコマンドとしてSCSIのREAD(10)を使っているため、14行目でCDB長に10を、32行目でコマンドオペコードとしてREAD(10)のオペコード(SCSIOP_READ)を設定しています。また、転送サイズは1セクタなので、19行目で512バイトを、34行目で1(セクタ数)を設定しています。

 CDBに値を設定する際は、SCSIの仕様に従って適切なフィールドに値を設定する必要があります。今回のREAD(10)の場合、CDBの仕様は以下の図のようになっています[11]ので、これを参考に設定します。

SCSI READ(10)のCDB仕様

 ちなみに、CDB長が10バイトだからREAD(10)なんですよね。他にもCDB長が16バイトのREAD(16)などがあります。READコマンドの場合、CDB長が最も短いのは6バイトのREAD(6)になります。

 そして、20行目でReadコマンドのデータ転送方向(ホストから見てデータが入力される方向、つまりDATA_IN)を指定します。

 データ転送については31行目と35行目の内容が重要です。

 31行目では、NVMe SSDから読み出したデータが格納されるバッファの先頭アドレスを設定しています。
 offsetofマクロを使うと、構造体先頭から特定メンバ先頭までのオフセットを取得できます。ここでは、このマクロを使用し、構造体SCSI_PASS_THROUGH_WITH_BUFFERS_EXの先頭から、SSDから読み出したデータの格納先であるメンバucDataBufまでのオフセットを計算して、設定しています。

 35行目でDeviceIoControl()の引数とするバッファサイズを計算しています。このサイズは、SSDから転送するデータのサイズではなく、DeviceIoControl()で渡すデータ構造のサイズであることに注意が必要です。つまり、構造体SCSI_PASS_THROUGH_WITH_BUFFERS_EXのサイズとなります。

 このように、SCSI Pass-throughを用いてNVMe SSDにコマンドを発行する場合、NVMe特有の仕様を記述する必要がありません。

 実際にこの方法で、Windowsのシステムドライブとして使用しているNVMe SSD(動作確認環境とは別のもの)にReadコマンドを発行し、先頭セクタ(LBA = 0)を読み出した結果を以下に示します。

      00  01  02  03  04  05  06  07   08  09  0A  0B  0C  0D  0E  0F
      ---------------------------------------------------------------
 000  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 010  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 020  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 030  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 040  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 050  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 060  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 070  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 080  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 090  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0A0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0B0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0C0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0D0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0E0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0F0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 100  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 110  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 120  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 130  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 140  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 150  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 160  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 170  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 180  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 190  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1A0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1B0  00  00  00  00  00  00  00  00   EB  D8  15  07  00  00  00  00   ........ ........
 1C0  02  00  EE  FE  FF  33  01  00   00  00  FF  FF  FF  FF  00  00   .....3.. ........
 1D0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1E0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1F0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  55  AA   ........ ......U.

オフセット0x1FEからの2バイトがブートシグニチャである0xAA55となっていて(リトルエンディアン表記なので格納順は逆)、有効なマスターブートレコード(Master Boot Record; MBR)が読み出せていることがわかります。

Writeコマンド

 次に、LBA = 0のセクタにデータを書き込むWriteコマンドを発行する方法を説明します。
 前節のReadコマンド同様、Writeコマンド発行にもSCSI Pass-through機構を用います。
 Writeコマンドを発行する処理のフローは、Writeするデータを準備すること以外、Read処理のフローと変わりません。

 コード片で示すと以下のようになります。コード中の行番号は説明のために付与したものです。

01: typedef struct _SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EX
02: {
03:     SCSI_PASS_THROUGH_DIRECT_EX sptd;
04:     UCHAR                       ucCdbBuf[SPT_CDB_LENGTH - 1]; // SPT_CDB_LENGTH == 32
05:     ULONG                       Filler;
06:     STOR_ADDR_BTL8              StorAddress;
07:     UCHAR                       ucSenseBuf[SPT_SENSE_LENGTH]; // SPT_SENSE_LENGTH == 32
08: } SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EX, *PSCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EX;

09: SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EX sptdwb_ex;

10: ZeroMemory(databuffer, 512);
11: for (int i = 0; i < 512 / 8; i++)
12: {
13:     databuffer[i * 8] = 'D';
14:     databuffer[i * 8 + 1] = 'E';
15:     databuffer[i * 8 + 2] = 'A';
16:     databuffer[i * 8 + 3] = 'D';
17:     databuffer[i * 8 + 4] = 'B';
18:     databuffer[i * 8 + 5] = 'E';
19:     databuffer[i * 8 + 6] = 'E';
20:     databuffer[i * 8 + 7] = 'F';
21: }

22: ZeroMemory(&sptdwb_ex, sizeof(SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EX));
23: sptdwb_ex.sptd.Version               = 0;
24: sptdwb_ex.sptd.Length                = sizeof(SCSI_PASS_THROUGH_DIRECT_EX);
25: sptdwb_ex.sptd.ScsiStatus            = 0;
26: sptdwb_ex.sptd.CdbLength             = CDB10GENERIC_LENGTH;
27: sptdwb_ex.sptd.StorAddressLength     = sizeof(STOR_ADDR_BTL8);
28: sptdwb_ex.sptd.SenseInfoLength       = SPT_SENSE_LENGTH;
29: sptdwb_ex.sptd.DataOutBuffer         = (PVOID)databuffer;
30: sptdwb_ex.sptd.DataOutTransferLength = 512;
31: sptdwb_ex.sptd.DataInTransferLength  = 0;
32: sptdwb_ex.sptd.DataDirection         = SCSI_IOCTL_DATA_OUT;
33: sptdwb_ex.sptd.TimeOutValue          = 5;
34: sptdwb_ex.sptd.StorAddressOffset     = offsetof(SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EX, StorAddress);
35: sptdwb_ex.StorAddress.Type           = STOR_ADDRESS_TYPE_BTL8;
36: sptdwb_ex.StorAddress.Port           = 0;
37: sptdwb_ex.StorAddress.AddressLength  = STOR_ADDR_BTL8_ADDRESS_LENGTH;
38: sptdwb_ex.StorAddress.Path           = 0;
39: sptdwb_ex.StorAddress.Target         = 0;
40: sptdwb_ex.StorAddress.Lun            = 0;
41: sptdwb_ex.sptd.SenseInfoOffset       = offsetof(SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EX, ucSenseBuf);
42: sptdwb_ex.sptd.Cdb[0]                = SCSIOP_WRITE;
43: sptdwb_ex.sptd.Cdb[5]                = 0; // Starting LBA
44: sptdwb_ex.sptd.Cdb[8]                = 1; // TRANSFER LENGTH

45: length = sizeof(SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EX);

46: status = DeviceIoControl(_hDevice, IOCTL_SCSI_PASS_THROUGH_DIRECT_EX, &sptdwb_ex, length
    &sptdwb_ex, length, &returned, FALSE); 

1行目で定義している構造体のメンバの中のSCSI_PASS_THROUGH_DIRECT_EX構造体が、今回使用するSCSI Pass-through要求用構造体です。この構造体もヘッダファイルntddscsi.h内で定義されています。このSCSI Pass-through要求用構造体に情報を設定しているのが23行目から44行目です。

 なお、Readコマンド発行時に使用したSCSI_PASS_THROUGH_EX構造体と今回Writeコマンド発行で使用するSCSI_PASS_THROUGH_DIRECT_EX構造体の違いは、後者はデータ転送に用いるバッファが構造体の外に存在する場合に用いる、ということです。今回のような小サイズではなく大サイズのデータ転送を行う場合は、後者を使ったほうが良いと考えられます。

 その他にReadコマンド発行時と異なる点は2点あります。

 ひとつは、10行目から21行目で書き込むデータを準備している部分です。今回示したこのコード片では、データが正しく書き込めたことを確認しやすいように、DEADBEEFという文字列を設定しました。そして準備したバッファの先頭アドレスを、29行目で転送データのバッファ先頭アドレスとして指定してしました。

 もうひとつは、32行目でWriteコマンドのデータ転送方向(ホストから見てデータが出力される方向、つまりDATA_OUT)を指定していることです。

 今回、WRITEコマンドとしてSCSIのWRITE(10)を使うため、26行目でCDB長に10を、42行目でコマンドオペコードとしてWRITE(10)のオペコード(SCSIOP_WRITE)を設定しています。また、転送サイズは1セクタなので、30行目で512バイトを、44行目で1(セクタ数)を設定しています。

 Readコマンド発行時同様、CDBに値を設定する際は、SCSIの仕様に従い適切なフィールドに値を設定する必要があります。今回のWRITE(10)の場合、CDBの仕様は下図のようになっているので、これを参考に設定します。

SCSI WRITE(10)のCDB仕様

 46行目でDeviceIoControl()を発行する際の引数のうち、サイズに関するものは、Readコマンド発行時と同様にあくまでDeviceIoControl()で渡すデータ構造のサイズです。このため、Writeコマンドで転送するデータのサイズ(512バイト)は含まず、構造体SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER_EXのサイズを設定します。

 このように、Writeコマンドの発行を要求する場合でも、SCSI Pass-through機構を用いる場合はNVMe特有の仕様を記述する必要がありません。

 実際にこの方法で、動作確認環境のSSDのLBA = 0にデータを書き込み、前節で説明したReadコマンドを使用してデータを読み出した結果が以下のようになります。

      00  01  02  03  04  05  06  07   08  09  0A  0B  0C  0D  0E  0F
      ---------------------------------------------------------------
 000  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 010  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 020  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 030  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 040  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 050  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 060  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 070  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 080  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 090  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 0A0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 0B0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 0C0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 0D0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 0E0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 0F0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 100  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 110  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 120  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 130  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 140  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 150  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 160  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 170  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 180  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 190  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 1A0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 1B0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 1C0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 1D0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 1E0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF
 1F0  44  45  41  44  42  45  45  46   44  45  41  44  42  45  45  46   DEADBEEF DEADBEEF

 DEADBEEFと書き込まれていることが確認できました。

Dataset Managementコマンド

 最後に、Dataset Managementコマンドを発行して、セクタのDeallocateを行う方法を説明します。

 なお、NVMe仕様[4]において、Dataset Managementコマンドはオプションですので、この機能を試す際はSSDがDataset Managementコマンドをサポートしているかどうかを予め確認する必要があります。SSDのDataset Managementコマンドサポート有無を確認するには、製品仕様を確認するか、もしくはこちらの方法があります。動作確認環境のSSDがDataset Managementコマンドをサポートしていることは、事前に確認済みです。

 NVMe SSDにDataset Managementコマンドを発行する方法は、これまでに説明したReadコマンドやWriteコマンドと異なり、SCSI Pass-through機構を使用しません。しかし、NVMe特有の記述は不要で、Windowsのストレージアーキテクチャにおけるポートドライバに向けて要求を送ることになります。

 NVMeにおけるDeallocateと同様の機能は、SATAを含むATA (ATA Command Set[12])ではDataset ManagementコマンドによるTrim処理、SCSI (SBC-3)ではUNMAPコマンド、として規定されています。
 また、前述したいずれのプロトコルにおいても、それらの機能の引数は処理対象LBA領域の先頭LBAとセクタ数のリストです。したがって、要求の構造を一般化し、ポートドライバで当該処理要求を受領したうえで適切なミニポートドライバに要求を送信する、という設計になっているものと推測されます[13]

 Dataset Managementコマンドを発行する処理のエラー処理を除くフローチャートは以下のようになります。

Dataset Managementコマンド(Deallocate)発行処理フローチャート

 デバイスのハンドルを取得する処理と、DeviceIoControl()でコマンド発行を要求する処理は、これまでのReadコマンド発行やWriteコマンド発行の処理と同じです。
 異なるのは、Dataset Managementコマンド用の構造体を準備することと、Deallocate対象のLBA領域を設定することです。

 実際にコード片で示すと以下のようになります。

// --- defined in winioctl.h
01: typedef struct _DEVICE_MANAGE_DATA_SET_ATTRIBUTES
02: {
03:     DWORD Size;
04:     DEVICE_DSM_ACTION Action;
05:     DWORD Flags;
06:     DWORD ParameterBlockOffset;
07:     DWORD ParameterBlockLength;
08:     DWORD DataSetRangesOffset;
09:     DWORD DataSetRangesLength;
10: } DEVICE_MANAGE_DATA_SET_ATTRIBUTES, *PDEVICE_MANAGE_DATA_SET_ATTRIBUTES, DEVICE_DSM_INPUT, *PDEVICE_DSM_INPUT;

11: typedef struct _DEVICE_DATA_SET_RANGE
12: {
13:     LONGLONG StartingOffset;
14:     DWORDLONG LengthInBytes;
15: } DEVICE_DATA_SET_RANGE, *PDEVICE_DATA_SET_RANGE, DEVICE_DSM_RANGE, *PDEVICE_DSM_RANGE;
// --- 

16: PDEVICE_MANAGE_DATA_SET_ATTRIBUTES pAttr = NULL;
17: PDEVICE_DSM_RANGE pRange = NULL;

18: bufferLength = sizeof(DEVICE_MANAGE_DATA_SET_ATTRIBUTES) + sizeof(DEVICE_DSM_RANGE);
19: buffer       = malloc(bufferLength);
20: ZeroMemory(buffer, bufferLength);

21: pAttr                       = (PDEVICE_MANAGE_DATA_SET_ATTRIBUTES)buffer;
22: pAttr->Action               = DeviceDsmAction_Trim;
23: pAttr->Flags                = DEVICE_DSM_FLAG_TRIM_NOT_FS_ALLOCATED; // for native deallocate (not file-level trimming)
24: pAttr->ParameterBlockOffset = 0; // TRIM does not need additional parameters
25: pAttr->ParameterBlockLength = 0;
26: pAttr->DataSetRangesOffset  = sizeof(DEVICE_MANAGE_DATA_SET_ATTRIBUTES);
27: pAttr->DataSetRangesLength  = sizeof(DEVICE_DSM_RANGE); //  only one range

28: pRange = (PDEVICE_DSM_RANGE)((ULONGLONG)pAttr + sizeof(DEVICE_MANAGE_DATA_SET_ATTRIBUTES));
29: pRange->StartingOffset = 0; // LBA = 0
30: pRange->LengthInBytes  = 512; // 1 sector

31: result = IssueDeviceIoControl(_hDevice, IOCTL_STORAGE_MANAGE_DATA_SET_ATTRIBUTES,
    buffer, bufferLength, buffer, bufferLength, &returnedLength, NULL);

 1行目から10行目までの構造体DEVICE_MANAGE_DATA_SET_ATTRIBUTESが、Dataset Managementコマンドそのものに関する構造体であり、11行目から15行目までの構造体DEVICE_DSM_RANGEが、Dataset Managementコマンドの対象となるLBA領域を表現する構造体です。両構造体ともに、winioctl.h内で定義されています。

 このコードでは、19行目で2つの構造体を合わせたサイズのメモリを確保し、20行目で確保したメモリをゼロクリアしています。そして、21行目から27行目でDataset Managementコマンドに関する構造体を設定しています。具体的には、メンバActionには要求する処理がTrim (Deallocate)であることを設定し(22行目)、メンバFlagsには、このTrimがファイルとは関係ないことを設定します(23行目)。

 次に、メンバDataSetRangesOffsetにDeallocate対象のLBA領域を記述した構造体の先頭として、確保したメモリ領域のうちLBA領域を表現する構造体の先頭となるアドレスを設定し(26行目)、メンバDataSetRangesLengthにはLBA領域情報構造体1つ分のサイズを設定します(27行目)。

 そして、Deallocate対象のLBA領域の情報として、「先頭」に0を設定し(29行目)、領域サイズとして512(1セクタ分)を設定します。領域サイズの単位がバイトとなっていることから推測すると、領域情報の「先頭」を示すメンバStartingOffsetもその単位はバイトであると推測されます。LBAで指定しないように注意が必要です。

 最後に、Dataset Managementコマンドであることを指定してDeviceIoControl()を呼び出し、コマンド発行を要求します(31行目)。

 このように、Dataset Managementコマンドの発行に際しては、NVMe特有の仕様はおろか、SCSI特有の記述すら明示的に行う必要がありません。

 実際にこの方法を用いてDeallocateが行えることを、次の手順で確認しました。

 まずLBA=0にデータを書き込み、その後LBA=0のデータを読み出して、書き込んだデータが読めることを確認します。続いて本節で説明した方法でLBA=0をDeallocateし、再度LBA=0を読み出してDeallocateされたことを確認します。

 なお、Deallocateされたことの確認には、「Deallocateされたまま未書き込みのセクタをReadした時にゼロが読める」という機能を利用しています。これは、動作環境で示したSSDの仕様です。試そうとしているSSDがこの機能を持つかどうか確認する方法は、こちらを参照してください。

 この結果、上記(2)においてDEADBEEFなデータが読めた後、Deallocateし、下図のようにオールゼロが読めたことで、正しくDeallocateされたことを確認しました。

      00  01  02  03  04  05  06  07   08  09  0A  0B  0C  0D  0E  0F
      ---------------------------------------------------------------
 000  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 010  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 020  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 030  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 040  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 050  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 060  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 070  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 080  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 090  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0A0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0B0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0C0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0D0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0E0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 0F0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 100  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 110  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 120  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 130  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 140  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 150  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 160  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 170  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 180  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 190  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1A0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1B0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1C0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1D0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1E0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........
 1F0  00  00  00  00  00  00  00  00   00  00  00  00  00  00  00  00   ........ ........

おわりに

 この記事では、NVMe SSDに対して、Windowsの標準NVMeデバイスドライバを使用してコマンドを発行する具体的な方法を、一部のコマンドを用いて説明しました。

 今回はRead、Writeといったコマンドの発行方法を説明しましたが、NVMeにはまだまだ多くのコマンドや機能があります。しかも、それらのコマンドや機能の中には、今回紹介したコマンドのように、Windowsの標準NVMeデバイスドライバを使用して発行が可能なものがあります。例えば、デバイス(SSD)が備える機能や基本情報を取得するIdentifyコマンドなどです。
 それらのコマンドの発行方法については、また別の記事で紹介できれば、と考えています。

参考情報

 ここから先は、記事本文中で言及した参考情報をまとめます。

デバイスハンドル取得方法

 DeviceIoControl()の呼び出しにはアクセス先デバイスのハンドルが必要です。
 このアクセス先デバイスのハンドルを取得するには、CreateFile()というインターフェースを使用します。ここでは、CreateFile()を使ってアクセス先デバイスのハンドルを取得する方法を説明します。

 CreateFile()を使用してアクセス先デバイスのハンドルを取得するコード片は下図のようになります。

// --- routine for printing error
01: void PrintSystemError(ULONG _ErrorCode, LPCSTR _StrFunc)
02: {
03:     WCHAR lpMsgBuf[1024];
04:     ULONG count;

05:     count = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, _ErrorCode, 0x0409,
            (LPTSTR)&lpMsgBuf, sizeof(lpMsgBuf), NULL);
06:     if (count != 0)
07:     {
08:         fprintf(stderr, "[E] %s: (error code = %d) %ls\n", _StrFunc, _ErrorCode, lpMsgBuf);
09:     }
10:     else
11:     {
12:         fprintf(stderr, "[E] Format message failed.  Error: %d\n", GetLastError());
13:     }
14: }
// ---

15: HANDLE hDevice = CreateFile(
16:     _T("\\\\.\\PhysicalDrive1"),
17:     GENERIC_READ | GENERIC_WRITE,
18:     FILE_SHARE_READ | FILE_SHARE_WRITE,
19:     NULL,
20:     OPEN_EXISTING,
21:     FILE_ATTRIBUTE_DEVICE,
22:     NULL);

23: if (hDevice == INVALID_HANDLE_VALUE)
24: {
25:     PrintSystemError(GetLastError(), "CreateFile");
26: }

 CreateFile()は複数の引数をとりますが、重要なものは第1引数のファイル名(ここではデバイス名)です。Windowsの各種インターフェース(API)は、マルチバイトやUnicodeへの対応が進められており、キャラクタ型(char)などの単純な文字列では対応できません。したがって、このコードのように_T()マクロを使うなどの対策が必要です。

 なお、このコードでは第1引数にはPhysicalDrive1が含まれており、これは物理デバイス番号1のデバイスを指定していることになります。システムにデバイスが1つしか存在しない場合で、そのデバイスを指定する場合は、物理デバイス番号は0となりますので、第1引数にはPhysicalDrive0を含める必要があります。

物理デバイス番号を確認する方法は次の節を参照してください。

物理デバイス番号確認方法

 アクセス対象のデバイスのハンドルを取得するには、アクセス対象のデバイスの「物理デバイス番号」を指定する必要があります。

 「物理デバイス番号」は、タスクバーのWindowsアイコンを右クリックし、「ディスクの管理」を選択してディスク管理ダイアログを出して確認します。
 実際に動作確認環境においてディスク管理ダイアログを出した状態が下図です。

動作確認環境での「ディスクの管理」ダイアログ

表示された「ディスクの管理」ダイアログの「ディスクN」のNの部分が「物理デバイス番号」です。したがって、この環境ではNは1となります。

Dataset Managementコマンドサポート確認方法

 NVMe仕様[4]において、デバイスがDataset Managementコマンドをサポートしているかどうかは、ControllerのIdentifyデータ(IdentifyコマンドをCNS Value=01hで実行した結果)に含まれるOptional NVM Command Support (ONCS)フィールドの値で確認できます。

NVMe 1.3dにおけるOptional NVM Command Support (ONCS)フィールドの仕様

この図の通り、ONCSフィールドのビット2が1であればDataset Managementコマンドをサポートしていることがわかります。

Deallocate後未書き込みセクタデータの確認方法

 NVMe仕様は、Deallocateされた後未書き込みの状態のセクタをReadした時にデバイスが返すデータは、オール0、オール1、Deallocateされる前のデータ、のいずれかのモードでなければならないと規定しています(参考文献[4]のSection 6.7.7.1)。

 デバイスがサポートするモードは、NamespaceのIdentifyデータ(IdentifyコマンドをCNS Value=00hで実行した結果)に含まれるDeallocate Logical Block Features (DLFEAT)フィールドの値で確認できます。

NVMe 1.3dにおけるDeallocate Logical Block Feature (DLFEAT)フィールドの仕様

この図の通り、DLFEATフィールドのビット2:0が、0b001であればオール0が、0b010であればオール1が読めることがわかります。

参考文献

[1] Microsoft, "Working with NVMe drives", 最終閲覧日2019年5月14日
[2] Microsoft, "SCSI Pass-Through Interface Tool", 最終閲覧日2019年5月14日
[3] Microsoft, "IOCTL_STORAGE_MANAGE_DATASET_ATTRIBUTES IOCTL"_, 最終閲覧日2019年5月14日
[4] NVM Express, "NVM ExpressTM Base Specification", Revision 1.3d, March 20, 2019
[5] Microsoft, "Windows Storage Driver Architecture", 最終閲覧日2019年5月14日
[6] Microsoft, "Introduction to Storage Class Drivers", 最終閲覧日2019年5月14日
[7] Microsoft, "Storage Port Drivers", 最終閲覧日2019年5月14日
[8] Microsoft, "Download the Windows Driver Kit (WDK)", 最終閲覧日2019年5月15日
[9] 打越、「Windows OS入門:第8回 Windowsのストレージアーキテクチャ」、最終閲覧日2019年5月15日
[10] Microsoft, "Life Cycle of a Storport Driver", 最終閲覧日2019年5月15日
[11] T10 / International Committee for Information Technology Standards (INCITS), "SCSI Block Commands-3 (SBC-3)", Revision 21, November 25, 2009
[12] T13 / International Committee for Information Technology Standards (INCITS), "ATA Command Set - 4 (ACS-4)", Revision 14, October 14, 2016
[13] Mark E. Russinovich, et al., "Windows Internals, Part 2", 6th Edition, October, 2012, Microsoft Press, ISBN: 978-0735665873 (株式会社クイープ(訳)、「インサイドWindows」、第6版、下巻、2013年5月、日経BP社、ISBN: 978-4822294717)

ライセンス表記

クリエイティブ・コモンズ・ライセンス
この記事はクリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。


  1. 「inboxドライバ」と呼ばれることもあります 

ken-yossy
ハギワラソリューションズ株式会社所属。NANDフラッシュメモリおよび新規メモリを使ったSolid State Drive (SSD)の開発にかかわって十数年。現在は、主に、産業機器向けSSDおよびその応用技術の開発をしています。ストレージに関する技術情報をポストします。※発信する内容は技術的知見に基づく個人の意見であり、所属する組織の公式見解ではありません。
https://www.hagisol.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away