アプリケーションから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.c74行目以降の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


    http://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);


 


となっています。(左側の数字は行番号をつけました。)


 


変数で指定されている1st3rd parameterをそれぞれ見ていきましょう。


1st parameterstringには、


 







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 parameteraccessModeには、


 







110    accessMode = GENERIC_WRITE | GENERIC_READ;       // default


 


のように入れます。spti.htmにはアクセスモードに GENERIC_WRITE | GENERIC_READ を指定することは


必須だと書いてあります。


3rd parametershareModeには、


 







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


  http://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では、DeviceIoControlSCSIコマンドを発行しているところはmain()7箇所あります。具体的なSCSIOperation Codeとして、MODE SENSEの例が4つ、TEST UNIT READYWRITE BUFFERREAD 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_THROUGHIOCTL_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.cGetAlignmentMaskForDevice()


    のようにアラインメントを考える必要がある。


 


これらのIOCTLの違いの詳細については、それぞれ以下のドキュメントをご参照ください。


  IOCTL_SCSI_PASS_THROUGH


  http://msdn.microsoft.com/en-us/library/ms803657.aspx


  IOCTL_SCSI_PASS_THROUGH_DIRECT


  http://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


  http://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.sptsptが上記の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(=0x1Ascsi.h参照)が入ります。Cdb[2]Cdb[4]も同様にSPCの表を見て指定します。


 


次に重要なのは、CdbLengthです。表の通りMODE SENSEのコマンドが6バイトであるため、CDB6GENERIC_LENGTH(=6scsi.h参照)が入ります。


 


DataInの値のセットも重要です。ここには、データの読み取りを行う場合はSCSI_IOCTL_DATA_IN、データの書き込みを行う場合はSCSI_IOCTL_DATA_OUTを指定します。(SCSI_PASS_THROUGHのドキュメントを参照してください。)


MODE SENSESCSIコマンドはデータの取得を行うため、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_BUFFERSucDataBuf[]を使います。


 


6th parameterでは、出力バッファのサイズとして、lengthを指定していますが、これは以下のコードのように、


 







192    length = offsetof(SCSI_PASS_THROUGH_WITH_BUFFERS,ucDataBuf) +


193       sptwb.spt.DataTransferLength;


 


となっています。ここでも、SCSI_PASS_THROUGH_WITH_BUFFERSのサイズはそのまま使っていません。


ucDataBuf[]の配列のサイズSPTWB_DATA_LENGTH512spti.hに定義されていますが、sptwb.spt.DataTransferLengthはすでに示したように192でした。IOCTL_SCSI_PASS_THROUGHでは、使用するバッファを全てシステムバッファにコピーしますので、使わない320バイト(=512-192)をコピーするのは無駄なオーバーヘッドになるのです。


 


6th parameterには、DeviceIoControlを実行した結果、どれだけのサイズ(バイト数)のデータを受け取れたかが入る変数へのポインタを指定します。


 


7nd parameterには、DeviceIoControlを非同期的に完了したい場合はTRUEを入れます。spti.cではFALSEが入っていますが、これにより、DeviceIoControlの完了時に出力バッファにデータを受け取ることができます。


 


以上のパラメータを指定してDeviceIoControlSCSIコマンドを発行した後は、spti.cのように取得したデータを表示したり、同じハンドルで、繰り返し、異なるSCSIコマンドを発行して、必要な処理が全て終わったら、


 


(3) CloseHandle()で、対象デバイスのハンドルをクローズ


 


を以下のように行って終了です。


 







424    CloseHandle(fileHandle);


 


最後に、SPTIサンプルのビルドは、本説明の冒頭にてC:\WinDDK\6001.18002\src\storage\tools\sptiの下にsourcesファイルがあることから、なおきお~さんが書かれた「ドライバのビルド方法」と同様に、bldbczなどでビルドすることができます。(もちろん、アプリケーションですので、ライブラリの設定などを工夫していただくことで、Visual Studioでビルドしていただくことも可能です。)


 


以上で、アプリケーションからSCSIコマンドを発行することができます。本ブログが、SPTIサンプルをご理解され、読者の方の製品の開発のご参考にしていただく上で、少しでもお役に立てたら幸いです。


 


 


――――――――――――――――


 


【閑話休題】 皆さんはワイングラスの種類によってワインの味が変わる、という体験をしたことはありますか?


 


それでは、冒頭にお約束しておりました、閑話休題です。


 


先日、さなえすさんからご紹介いただいた際に、私は「ワインのテイスティング」ができる、というようにかなり良く書いていただきましたが、実際のところ、ビール一杯で酔っ払うほどお酒に弱い私ができるのは、飲んで酔っ払うことだけです。しかも、体質的に顔色が全く変わらないので、一見何の変化も見られないのですが、酔うと、微妙に体が横に揺れ始めたり、立つと千鳥足で歩き始めるので、すぐにわかります。そんな調子ですので、カルチャーセンターのワイン教室に計12回ほど通いました(一般のワイン教室よりも、場所によってはカルチャーセンターの方が安い気がします)が、試飲でベロベロになってしまい、肝心のワインの知識については、さっぱり覚えられませんでした。そんな私なのですが、懲りもせず、山梨のあるワイナリーに見学に行った時、「ワインごとの味わいに適したワイングラスを作っているメーカーがある」という話を聞きました。その後、恵比寿のあるワインショップで、そのワイングラスメーカーの出張ワイングラス教室があるのを見かけ、早速行ったところ、・・・すごかったです。本当に、同じワインが、グラスの形状でこんなに味が変わるとは思いませんでした。種明かしをすると、ワイングラスの口をつける部分の弧がゆるやかになっているか急になっているかで、そのグラスを傾けた時に、ワインが舌のどこを通るかが変わるのです。文字にしてしまうと「本当かよ!?」と疑いたくなる方が大半だと思いますし、私もそう思っていましたが、実際にやってみて私個人は非常に感動しましたので、もしご興味がある方は、この感動を味わってみてはいかがでしょうか。


 


 

Skip to main content