アプリケーションからSCSIコマンドを発行する方法
皆さん、こんにちは。A寿です。
突然ですが、皆さんはワイングラスの種類によってワインの味が変わる、という体験をしたことはありますか?・・・とお話しだすと、今回は少し長くなりますので、(もっと長い)本編が終わった後の【閑話休題】で述べさせていただきたいと思います。
さて、本日は、アプリケーションからSCSIコマンドを発行する方法をご紹介したいと思います。WDKのサンプルである、SPTI(SCSI Pass Through Interface)は、以下のフォルダにあります。
C:\WinDDK\6001.18002\src\storage\tools\spti
このフォルダのファイルの一覧を見てみますと、以下の5つのファイルがあります。
C:\WinDDK\6001.18002\src\storage\tools\spti>dir ドライブ C のボリューム ラベルがありません。 ボリューム シリアル番号は 0836-2E4B です
C:\WinDDK\6001.18002\src\storage\tools\spti のディレクトリ
2008/11/27 16:57 <DIR> . 2008/11/27 16:57 <DIR> .. 2008/01/18 21:53 273 makefile // 変更不可のファイルです 2008/01/18 21:53 136 sources // ビルド用のマクロ定義ファイルです 2008/01/18 21:53 26,954 spti.c // SPTIサンプルのソースコードです 2008/01/18 21:53 5,159 spti.h // SPTIサンプルのヘッダファイルです 2008/01/03 10:59 109,034 spti.htm // SPTIサンプルのヘルプです 5 個のファイル 141,556 バイト 2 個のディレクトリ 97,009,762,304 バイトの空き領域
C:\WinDDK\6001.18002\src\storage\tools\spti> |
SCSIコマンドを発行する処理を把握するために、spti.cの74行目以降のmain()を見てみましょう。main()自体は、425行目まで、およそ350行くらいあり、main()から呼び出される関数も多数あるため、初めて見ると圧倒される方もいらっしゃるかもしれません。しかし、アプリケーションからSCSIコマンドを発行する方法としては、
(1) CreateFile()で、SCSIコマンド発行の対象となるデバイスのハンドルをオープン
(2) DeviceIoControl()で、I/O Control(IOCTL)を発行することにより、SCSIコマンドを発行
(3) CloseHandle()で、対象デバイスのハンドルをクローズ
と非常に簡単です。あとは、それぞれの関数の各引数に何を指定すればよいのかがわかれば、すぐに使えます。そのため、この3つの流れにポイントを絞って、spti.cを見ていきましょう。
(1) CreateFile() で、 SCSIコマンド発行の対象となるデバイスのハンドルをオープン
CreateFile()の定義は、
CreateFile
https://msdn.microsoft.com/ja-jp/library/cc429198.aspx
を見ますと、
HANDLE CreateFile( LPCTSTR lpFileName, // ファイル名 DWORD dwDesiredAccess, // アクセスモード DWORD dwShareMode, // 共有モード LPSECURITY_ATTRIBUTES lpSecurityAttributes, // セキュリティ記述子 DWORD dwCreationDisposition, // 作成方法 DWORD dwFlagsAndAttributes, // ファイル属性 HANDLE hTemplateFile // テンプレートファイルのハンドル ); |
となっています。
一方、spti.cでは、
137 fileHandle = CreateFile(string, 138 accessMode, 139 shareMode, 140 NULL, 141 OPEN_EXISTING, 142 0, 143 NULL); |
となっています。(左側の数字は行番号をつけました。)
変数で指定されている1st~3rd parameterをそれぞれ見ていきましょう。
1st parameterのstringには、
107 StringCbPrintf(string, sizeof(string), "\\\\.\\%s", argv[1]); |
とあるように、「\\.\」の後にドライブレターなどの文字列を入れます。具体的には、
97 if ((argc < 2) || (argc > 3)) { 98 printf("Usage: %s <port-name> [-mode]\n", argv[0] ); 99 printf("Examples:\n"); 100 printf(" spti g: (open the disk class driver in SHARED READ/WRITE mode)\n"); 101 printf(" spti Scsi2: (open the miniport driver for the 3rd host adapter)\n"); 102 printf(" spti Tape0 w (open the tape class driver in SHARED WRITE mode)\n"); 103 printf(" spti i: c (open the CD-ROM class driver in SHARED READ mode)\n"); 104 return; 105 } |
とあるように、「\\.\g:」などの文字列を入れます。
2nd parameterのaccessModeには、
110 accessMode = GENERIC_WRITE | GENERIC_READ; // default |
のように入れます。spti.htmにはアクセスモードに GENERIC_WRITE | GENERIC_READ を指定することは
必須だと書いてあります。
3rd parameterのshareModeには、
109 shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; // default |
をこのまま使ってもいいですし、サンプルでは、
112 if (argc == 3) { 113 114 switch(tolower(argv[2][0])) { 115 case 'r': 116 shareMode = FILE_SHARE_READ; 117 break; 118 119 case 'w': 120 shareMode = FILE_SHARE_WRITE; 121 break; 122 123 case 'c': 124 shareMode = FILE_SHARE_READ; 125 sectorSize = 2048; 126 break; 127 128 default: 129 printf("%s is an invalid mode.\n", argv[2]); 130 puts("\tr = read"); 131 puts("\tw = write"); 132 puts("\tc = read CD (2048 byte sector mode)"); 133 return; 134 } 135 } |
と書かれているように、目的に応じてshareModeを変更することができます。
(2) DeviceIoControl() で、 I/O Control(IOCTL) を発行することにより、 SCSIコマンドを発行
DeviceIoControlの定義は、
DeviceIoControl
https://msdn.microsoft.com/ja-jp/library/cc429164.aspx
を見ますと、
BOOL DeviceIoControl ( HANDLE hDevice, // デバイス、ファイル、ディレクトリいずれかのハンドル DWORD dwIoControlCode, // 実行する動作の制御コード LPVOID lpInBuffer, // 入力データを供給するバッファへのポインタ DWORD nInBufferSize, // 入力バッファのバイト単位のサイズ LPVOID lpOutBuffer, // 出力データを受け取るバッファへのポインタ DWORD nOutBufferSize, // 出力バッファのバイト単位のサイズ LPDWORD lpBytesReturned, // バイト数を受け取る変数へのポインタ LPOVERLAPPED lpOverlapped // 非同期動作を表す構造体へのポインタ ); |
となっています。
一方、spti.cでは、DeviceIoControlでSCSIコマンドを発行しているところはmain()に7箇所あります。具体的なSCSIのOperation Codeとして、MODE SENSEの例が4つ、TEST UNIT READY、WRITE BUFFER、READ BUFFERの例がそれぞれ1つずつ書かれています。今回は、最初のMODE SENSEの例を取り上げてみたいと思います。
195 status = DeviceIoControl(fileHandle, 196 IOCTL_SCSI_PASS_THROUGH, 197 &sptwb, 198 sizeof(SCSI_PASS_THROUGH), 199 &sptwb, 200 length, 201 &returned, 202 FALSE); |
1st parameterのハンドルには、言うまでもなく、(1)で取得したハンドルが入ります。
2nd parameterには、IOCTL_SCSI_PASS_THROUGHが入っていますが、IOCTL_SCSI_PASS_THROUGH_DIRECTを使うこともできます。今回は詳しく説明しませんが、IOCTL_SCSI_PASS_THROUGHとIOCTL_SCSI_PASS_THROUGH_DIRECTの違いは、以下の表のようになります。
I/O Control Code |
IOCTL_SCSI_PASS_THROUGH |
IOCTL_SCSI_PASS_THROUGH_DIRECT |
システムバッファ |
バッファのコピーに使用 |
使用しない。デバイスが直接ユーザモードのバッファにアクセス。(※) |
転送データ量 |
< 16K |
> 16K |
構造体 |
SCSI_PASS_THROUGH_WITH_BUFFERS |
SCSI_PASS_THROUGH_DIRECT_WITH_BUFFER |
※デバイスがユーザモードのバッファを利用するので、spti.cのGetAlignmentMaskForDevice()
のようにアラインメントを考える必要がある。
これらのIOCTLの違いの詳細については、それぞれ以下のドキュメントをご参照ください。
IOCTL_SCSI_PASS_THROUGH
https://msdn.microsoft.com/en-us/library/ms803657.aspx
IOCTL_SCSI_PASS_THROUGH_DIRECT
https://msdn.microsoft.com/en-us/library/ms803668.aspx
3rd parameterには、入力バッファとして、SCSI_PASS_THROUGH_WITH_BUFFERS sptwbへのポインタを指定しています。
一般に、DeviceIoControlを使用するときは、2nd parameterに指定したIOCTLに対応する構造体のアドレスを3rd parameter(入力バッファへのポインタ)や5th parameter(出力バッファへのポインタ)に指定する必要があります。
SCSI_PASS_THROUGH_WITH_BUFFERS構造体の定義は、spti.hの以下のコードを見てください。
23 typedef struct _SCSI_PASS_THROUGH_WITH_BUFFERS { 24 SCSI_PASS_THROUGH spt; 25 ULONG Filler; // realign buffers to double word boundary 26 UCHAR ucSenseBuf[SPT_SENSE_LENGTH]; 27 UCHAR ucDataBuf[SPTWB_DATA_LENGTH]; 28 } SCSI_PASS_THROUGH_WITH_BUFFERS, *PSCSI_PASS_THROUGH_WITH_BUFFERS; |
SCSI_PASS_THROUGH構造体の定義は、WDKではC:\WinDDK\6001.18002\inc\api\ntddscsi.hにありますが、
SCSI_PASS_THROUGH
https://msdn.microsoft.com/en-us/library/ms810309.aspx
を見ていただいた方が詳しいコメントが載っています。
SCSI_PASS_THROUGH構造体の定義は、
typedef struct _SCSI_PASS_THROUGH { USHORT Length; UCHAR ScsiStatus; UCHAR PathId; UCHAR TargetId; UCHAR Lun; UCHAR CdbLength; UCHAR SenseInfoLength; UCHAR DataIn; ULONG DataTransferLength; ULONG TimeOutValue; ULONG_PTR DataBufferOffset; ULONG SenseInfoOffset; UCHAR Cdb[16]; }SCSI_PASS_THROUGH, *PSCSI_PASS_THROUGH; |
となっています。いよいよSCSIコマンドMODE SENSEを発行するために必要なパラメータの設定です。sptwbの初期化の方法は、DeviceIoControlの手前にある以下のコードを見てください。
174 ZeroMemory(&sptwb,sizeof(SCSI_PASS_THROUGH_WITH_BUFFERS)); 175 176 sptwb.spt.Length = sizeof(SCSI_PASS_THROUGH); 177 sptwb.spt.PathId = 0; 178 sptwb.spt.TargetId = 1; 179 sptwb.spt.Lun = 0; 180 sptwb.spt.CdbLength = CDB6GENERIC_LENGTH; 181 sptwb.spt.SenseInfoLength = SPT_SENSE_LENGTH; 182 sptwb.spt.DataIn = SCSI_IOCTL_DATA_IN; 183 sptwb.spt.DataTransferLength = 192; 184 sptwb.spt.TimeOutValue = 2; 185 sptwb.spt.DataBufferOffset = 186 offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS,ucDataBuf); 187 sptwb.spt.SenseInfoOffset = 188 offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS,ucSenseBuf); 189 sptwb.spt.Cdb[0] = SCSIOP_MODE_SENSE; 190 sptwb.spt.Cdb[2] = MODE_SENSE_RETURN_ALL; 191 sptwb.spt.Cdb[4] = 192; |
このsptwb.sptのsptが上記のSCSI_PASS_THROUGH構造体です。この各メンバ変数のうち、最も重要なのは、Cdb[]です。CDBとはCommand Descriptor Blockの略で、Cdb[]には、SPC-2の以下の表の各値に対応するように値を入れていきます。(この表の例ではSPC-2を使っていますが、SPC-3でも表の内容に若干の違いがありますが、同様です。要は、対象デバイスがサポートするSCSIコマンドの仕様にあわせて設定します。)
MODE SENSE(6) command の表:
例えば、表の0バイト目が、Cdb[0]に対応しているため、Operation Code=1Ahが、Cdb[0]にSCSIOP_MODE_SENSE(=0x1A、scsi.h参照)が入ります。Cdb[2]やCdb[4]も同様にSPCの表を見て指定します。
次に重要なのは、CdbLengthです。表の通りMODE SENSEのコマンドが6バイトであるため、CDB6GENERIC_LENGTH(=6、scsi.h参照)が入ります。
DataInの値のセットも重要です。ここには、データの読み取りを行う場合はSCSI_IOCTL_DATA_IN、データの書き込みを行う場合はSCSI_IOCTL_DATA_OUTを指定します。(SCSI_PASS_THROUGHのドキュメントを参照してください。)
MODE SENSEのSCSIコマンドはデータの取得を行うため、SCSI_IOCTL_DATA_INを指定しています。
4th parameterでは、入力バッファのサイズとして、sizeof(SCSI_PASS_THROUGH)を指定しています。
SCSI_PASS_THROUGH_WITH_BUFFERSのサイズじゃないの?と思われる方もおられるかもしれませんが、ここまでその中のSCSI_PASS_THROUGHのパラメータ設定しかしていないことから、SCSI_PASS_THROUGH_WITH_BUFFERSの先頭アドレスからSCSI_PASS_THROUGH構造体のサイズで十分であることがお分かりいただけるかと思います。
5th parameterでは、出力バッファとして、入力バッファと同じSCSI_PASS_THROUGH_WITH_BUFFERS sptwbへのポインタを指定しています。IOCTL_SCSI_PASS_THROUGHでは、データの入出力にSCSI_PASS_THROUGH_WITH_BUFFERSのucDataBuf[]を使います。
6th parameterでは、出力バッファのサイズとして、lengthを指定していますが、これは以下のコードのように、
192 length = offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS,ucDataBuf) + 193 sptwb.spt.DataTransferLength; |
となっています。ここでも、SCSI_PASS_THROUGH_WITH_BUFFERSのサイズはそのまま使っていません。
ucDataBuf[]の配列のサイズSPTWB_DATA_LENGTHは512とspti.hに定義されていますが、sptwb.spt.DataTransferLengthはすでに示したように192でした。IOCTL_SCSI_PASS_THROUGHでは、使用するバッファを全てシステムバッファにコピーしますので、使わない320バイト(=512-192)をコピーするのは無駄なオーバーヘッドになるのです。
6th parameterには、DeviceIoControlを実行した結果、どれだけのサイズ(バイト数)のデータを受け取れたかが入る変数へのポインタを指定します。
7nd parameterには、DeviceIoControlを非同期的に完了したい場合はTRUEを入れます。spti.cではFALSEが入っていますが、これにより、DeviceIoControlの完了時に出力バッファにデータを受け取ることができます。
以上のパラメータを指定してDeviceIoControlでSCSIコマンドを発行した後は、spti.cのように取得したデータを表示したり、同じハンドルで、繰り返し、異なるSCSIコマンドを発行して、必要な処理が全て終わったら、
(3) CloseHandle() で、対象デバイスのハンドルをクローズ
を以下のように行って終了です。
424 CloseHandle(fileHandle); |
最後に、SPTIサンプルのビルドは、本説明の冒頭にてC:\WinDDK\6001.18002\src\storage\tools\sptiの下にsourcesファイルがあることから、なおきお~さんが書かれた「ドライバのビルド方法」と同様に、bldやbczなどでビルドすることができます。(もちろん、アプリケーションですので、ライブラリの設定などを工夫していただくことで、Visual Studioでビルドしていただくことも可能です。)
以上で、アプリケーションからSCSIコマンドを発行することができます。本ブログが、SPTIサンプルをご理解され、読者の方の製品の開発のご参考にしていただく上で、少しでもお役に立てたら幸いです。
――――――――――――――――
【閑話休題】 皆さんはワイングラスの種類によってワインの味が変わる、という体験をしたことはありますか?
それでは、冒頭にお約束しておりました、閑話休題です。
先日、さなえすさんからご紹介いただいた際に、私は「ワインのテイスティング」ができる、というようにかなり良く書いていただきましたが、実際のところ、ビール一杯で酔っ払うほどお酒に弱い私ができるのは、飲んで酔っ払うことだけです。しかも、体質的に顔色が全く変わらないので、一見何の変化も見られないのですが、酔うと、微妙に体が横に揺れ始めたり、立つと千鳥足で歩き始めるので、すぐにわかります。そんな調子ですので、カルチャーセンターのワイン教室に計12回ほど通いました(一般のワイン教室よりも、場所によってはカルチャーセンターの方が安い気がします)が、試飲でベロベロになってしまい、肝心のワインの知識については、さっぱり覚えられませんでした。そんな私なのですが、懲りもせず、山梨のあるワイナリーに見学に行った時、「ワインごとの味わいに適したワイングラスを作っているメーカーがある」という話を聞きました。その後、恵比寿のあるワインショップで、そのワイングラスメーカーの出張ワイングラス教室があるのを見かけ、早速行ったところ、・・・すごかったです。本当に、同じワインが、グラスの形状でこんなに味が変わるとは思いませんでした。種明かしをすると、ワイングラスの口をつける部分の弧がゆるやかになっているか急になっているかで、そのグラスを傾けた時に、ワインが舌のどこを通るかが変わるのです。文字にしてしまうと「本当かよ!?」と疑いたくなる方が大半だと思いますし、私もそう思っていましたが、実際にやってみて私個人は非常に感動しましたので、もしご興味がある方は、この感動を味わってみてはいかがでしょうか。