Linux の汎用 SCSI ドライバー

Linux の汎用 SCSI ドライバーの API を掘り下げ、その使用例を学ぶ

コンピューターでは、SCSI デバイスを制御するために SCSI コマンドを使用し、また SCSI コマンドを使用して SCSI デバイスにデータを転送します。この記事では、SCSI コマンドをいくつか紹介するとともに、Linux® で SCSI API を使用して SCSI コマンドを実行する方法について説明します。最初に SCSI のクライアント/サーバー・モデルとストレージ用の SCSI コマンドに関する背景を説明します。次に Linux の汎用 SCSI ドライバーの API について説明し、汎用のドライバーを使って inquiry コマンドのみを実行するシステムの例を説明します。

Mao Yuan Tao, Software Engineer, IBM

mao tao author photoMao Yuan Tao は IBM の CSTL (China System and Technology Lab) のソフトウェア技術者です。彼は技術者として、ストレージ製品のテストから SAN 環境 (FC スイッチ、バックエンド・ストレージ、ホストなど) の構成まで、さらにはテスト・ツールの開発まで広範に経験してきています。



2009年 2月 25日

SCSI のクライアント/サーバー・モデル

ホストとストレージとの間で通信を行う際、通常はホストが SCSI イニシエーターとして動作します。コンピューターのストレージにおいて、SCSI イニシエーターは SCSI セッションを開始するエンドポイントです。つまり SCSI イニシエーターは SCSI コマンドを送信します。ストレージは多くの場合、SCSI コマンドを受信して処理する SCSI ターゲットとして動作します。SCSI ターゲットはイニシエーターのコマンドを待ち、コマンドを受け取ったら必要な入出力データの転送を行います。

ターゲットは通常、1 つ以上の LUN (Logical Unit Number: 論理ユニット番号) をイニシエーターに提供します。コンピューターのストレージでは、LUN は論理ユニットに割り当てられた単なる数字にすぎません。論理ユニットは SCSI プロトコル・エンティティーであり、実際の I/O 操作でアドレス指定されるのはこの論理ユニットになります。各 SCSI ターゲットには 1 つ以上の論理ユニットがあり、実際に I/O 操作を実行するのは SCSI ターゲット自身というよりは、ターゲットに含まれる特定の論理ユニットになります。

ストレージで使われる LUN は、多くの場合 SCSI ディスクを表し、このディスクに対してホストが読み書き操作を行います。図 1 は SCSI のクライアント/サーバー・モデルの動作を示しています。

図 1. SCSI のクライアント/サーバー・モデル
SCSI のクライアント/サーバー・モデル

イニシエーターは最初にターゲットにコマンドを送信し、ターゲットはコマンドをデコードした後、イニシエーターに対してデータを要求するか、あるいはイニシエーターにデータを送信します。それが終わるとターゲットはイニシエーターにステータスを送信します。ステータスが問題の存在を示している場合には、イニシエーターはターゲットに対して request sense コマンドを送信します。ターゲットはセンス (sense) データを送り返し、何が問題なのかを知らせます。

それでは次に、ストレージに関連する SCSI コマンドについて説明しましょう。


ストレージ関連の SCSI コマンド

ストレージ関連の SCSI コマンドは主に、SAM (SCSI Architecture Model)、SPC (SCSI Primary Commands)、SBC (SCSI Block Commands) によって定義されます。

  • SAM では、SCSI システムのモデル、一連の SCSI 標準の機能区分、そして SCSI の実装と実装標準のすべて、に適用される要件を定義します。
  • SPC では、すべての SCSI デバイス・モデルに共通の動作を定義します。
  • SBC では、SCSI ダイレクト・アクセス・ブロック・デバイスの操作を容易にするためのコマンド・セット拡張機能を定義します。

各 SCSI コマンドの内容は CDB (Command Descriptor Block) と呼ばれるブロックに記述され、このブロックの中にその SCSI デバイスが実行する操作が定義されています。SCSI コマンドには、SCSI デバイスとの間でのデータの転送に使用されるデータ・コマンドと、SCSI デバイスの構成パラメーターを要求または設定する非データ・コマンドがあります。表 1 は最も一般的に使用されるコマンドを示しています。

表 1. 最も一般的に使用される SCSI コマンド
コマンド説明
Inquiryターゲット・デバイスに関する一般的な情報を要求します。
Test/Unit/Ready転送操作の準備ができているかどうかターゲット・デバイスをチェックします。
READSCSI ターゲット・デバイスからデータを転送します。
WRITESCSI ターゲット・デバイスにデータを転送します。
Request Sense最後のコマンドのセンス・データを要求します。
Read Capacityストレージ容量の情報を要求します。

すべての SCSI コマンドの最初のバイトは操作コード (Operation code) でなければなりません。操作コードは、そのコマンドによって行う操作を表します。また、すべての SCSI コマンドには制御バイトも含める必要があります。制御バイトは通常、コマンドの最後のバイトであり、ベンダー固有の情報その他の目的に使用されます。

それでは次のセクションで、汎用の SCSI ドライバーを調べてみましょう。


Linux の汎用 SCSI ドライバー

Linux での SCSI デバイスには多くの場合、ユーザーがそのデバイスを認識しやすいような名前が付けられます。例えば最初の SCSI CD-ROMは /dev/scd0 です。SCSI ディスクには /dev/sda、/dev/sdb、/dev/sdc のようなラベルが付けられます。デバイスの初期化が終わると、Linux の SCSI ディスク・ドライバー・インターフェース (sd) が SCSI READSCSI WRITE コマンドのみを送信します。

これらの SCSI デバイスは汎用的な名前すなわちインターフェース (/dev/sg0、/dev/sg1、/dev/sga、/dev/sgb など) を持っている場合があります。これらの汎用ドライバー・インターフェースを利用すると、SCSI デバイスに直接 SCSI コマンドを送信することができ、通常 SCSI ディスク上に作成されてディレクトリー配下にマウントされるファイルシステムを使う必要がなくなります。図 2 は、さまざまなアプリケーションが SCSI デバイスと通信する様子を示しています。

図 2. SCSI デバイスと通信するためのさまざまな方法
SCSI デバイスと通信するためのさまざまな方法

Linux の汎用ドライバー・インターフェースを利用して構築するアプリケーションでは、より多くの種類の SCSI コマンドを SCSI デバイスに送信することができます。つまり選択肢があるということです。例えば、どの SCSI デバイスがどの sg インターフェースを表すかを判断する場合、以下に示すようにその対応関係を sg_map コマンドを使って表示することができます。

まず、sg ドライバーがロードされていることを確認します。

[root@taomaoy ~]# lsmod | grep sg
sg                     31028  0 
...

grep を実行しても sg ドライバーに関する情報が何も表示されない場合には、次のように手動で sg ドライバーをロードします。

[root@taomaoy ~]# modprobe sg

そして sg_map を使って対応関係を表示します。

[root@taomaoy ~]# sg_map -i
/dev/sg0  /dev/sda  ATA       ST3160812AS       3.AA
/dev/sg1  /dev/scd0  HL-DT-ST  RW/DVD GCC-4244N  1.02

Red Hat または Fedora の場合には sg3_utils がインストールされている必要があります。では、典型的な SCSI システム呼び出しコマンドを実行する方法を調べることにしましょう。


典型的な SCSI 汎用ドライバー・コマンド

SCSI 汎用ドライバーは、文字デバイス用の典型的なシステム・コールを数多くサポートしています (open()close()read()write()poll()ioctl() など)。特定の SCSI デバイスに SCSI コマンドを送信する手順も以下のように非常に単純です。

  1. SCSI 汎用デバイス・ファイル (sg1 など) を開き、SCSI デバイスのファイル記述子を取得します。
  2. SCSI コマンドを用意します。
  3. 関連するメモリー・バッファーを設定します。
  4. ioctl() 関数を呼び出し、その SCSI コマンドを実行します。
  5. デバイス・ファイルを閉じます。

典型的な ioctl() 関数は ioctl(fd,SG_IO,p_io_hdr); のように書くことができます。

この場合の ioctl() 関数には以下の 3 つのパラメーターが必要です。

  1. fd はデバイス・ファイルのファイル記述子です。open() を呼び出してデバイス・ファイルが正常に開くと、このパラメーターを取得することができます。
  2. SG_IO は、ioctl() 関数の 3 番目のパラメーターとして sg_io_hdr オブジェクトが渡され、SCSI コマンドの実行が終わると sg_io_hdr オブジェクトが返されることを示しています。
  3. p_io_hdrsg_io_hdr オブジェクトへのポインターです (sg_io_hdr オブジェクトは SCSI コマンドその他の設定を含んでいます)。

SCSI 汎用ドライバーにとって最も重要なデータ構造体は struct sg_io_hdr です。この構造体は scsi/sg.h で定義され、その SCSI コマンドを使用するための情報を含んでいます。リスト 1 はこのデータ構造体の定義を示しています。

リスト 1. struct sg_io_hdr の定義
typedef struct sg_io_hdr
{
    int interface_id;               /* [i] 'S' (required) */
    int dxfer_direction;            /* [i] */
    unsigned char cmd_len;          /* [i] */
    unsigned char mx_sb_len;        /* [i] */
    unsigned short iovec_count;     /* [i] */
    unsigned int dxfer_len;         /* [i] */
    void * dxferp;                  /* [i], [*io] */
    unsigned char * cmdp;           /* [i], [*i]  */
    unsigned char * sbp;            /* [i], [*o]  */
    unsigned int timeout;           /* [i] unit: millisecs */
    unsigned int flags;             /* [i] */
    int pack_id;                    /* [i->o] */
    void * usr_ptr;                 /* [i->o] */
    unsigned char status;           /* [o] */
    unsigned char masked_status;    /* [o] */
    unsigned char msg_status;       /* [o] */
    unsigned char sb_len_wr;        /* [o] */
    unsigned short host_status;     /* [o] */
    unsigned short driver_status;   /* [o] */
    int resid;                      /* [o] */
    unsigned int duration;          /* [o] */
    unsigned int info;              /* [o] */
} sg_io_hdr_t;  /* 64 bytes long (on i386) */

この構造体のすべてのフィールドが必要なわけではないため、一般的に使用されるフィールドのみを以下に挙げます。

  • interface_id: 常に S でなければなりません。
  • dxfer_direction: データ転送の方向を示すために使われます。値は以下のうちのいずれか 1 つです。
    • SG_DXFER_NONE: データ転送は必要ありません (例えば SCSI Test Unit Ready コマンドなど)。
    • SG_DXFER_TO_DEV: デバイスにデータが転送されます (SCSI WRITE コマンドなど)。
    • SG_DXFER_FROM_DEV: デバイスからデータが転送されます (SCSI READ コマンドなど)。
    • SG_DXFER_TO_FROM_DEV: データは双方向に転送されます。
    • SG_DXFER_UNKNOWN: データ転送の方向は不明です。
  • cmd_len: SCSI コマンドを指す cmdp のバイト長。
  • mx_sb_len: sense_buffer が出力された場合に sbp に書き戻される最大サイズ。
  • dxfer_len: データ転送のためのユーザー・メモリーの長さ。
  • dxferp: データ転送のための、少なくとも dxfer_len バイトの長さを持つユーザー・メモリーへのポインター。
  • cmdp: 実行が必要な SCSI コマンドへのポインター。
  • sbp: センス・バッファーへのポインター。
  • timeout: 指定されたコマンドのタイムアウトを設定するために使われます。
  • status: SCSI 標準で定義される SCSI ステータス・バイト。

要約すると、この方法を使ってデータを転送する際には、cmdp は SCSI CDB を指す必要があり (CDB の長さは cmd_len に保存されています)、sbp はユーザー・メモリー (最大長は mx_sb_len) を指す必要がある、ということです。エラーが発生した場合には、センス・データはこの sbp に書き込まれます。dxferp は転送データの格納先メモリーの場所を指しており、データは dxfer_direction の値に応じて SCSI デバイスから送信される場合と SCSI デバイスに送信される場合があります。

では最後に、inquiry コマンドと、汎用ドライバーを使って inquiry コマンドを実行する方法について調べてみましょう。


inquiry コマンドを実行する例

inquiry コマンドは、すべての SCSI デバイスが実装する最も一般的な SCSI コマンドです。このコマンドは SCSI デバイスの基本的な情報を要求するために使われ、また SCSI デバイスがオンラインかどうかを調べるための ping 操作にもよく使われます。表 2 は SCSI 標準がどのように定義されているかを示しています。

表 2. inquiry コマンドのフォーマット定義
bit 7bit 6bit 5bit 4bit 3bit 2bit 1bit 0
byte 0Operation code = 12h
byte 1LUNReservedEVPD
byte 2Page code
byte 3Reserved
byte 4Allocation length
byte 5Control

EVPD (Enable Vital Product Data) パラメーター・ビットが 0 で、Page code パラメーターのバイトが 0 の場合には、ターゲットは標準の inquiry データを返します。EVPD パラメーターが 1 の場合には、ターゲットは Page code フィールドに従ってベンダー固有のデータを返します。

リスト 2 は、SCSI の汎用 API を使ったソースコードからの抜粋です。まず、sg_io_hdr を設定する例を見てみましょう。

リスト 2. sg_io_hdr の設定
struct  sg_io_hdr * init_io_hdr() {
  struct sg_io_hdr * p_scsi_hdr = (struct sg_io_hdr *)malloc(sizeof(struct sg_io_hdr));
  memset(p_scsi_hdr, 0, sizeof(struct sg_io_hdr));
  if (p_scsi_hdr) {
   p_scsi_hdr->interface_id = 'S'; /* this is the only choice we have! */
    /* this would put the LUN to 2nd byte of cdb*/
    p_scsi_hdr->flags = SG_FLAG_LUN_INHIBIT; 
  }
  return p_scsi_hdr;
}

void destroy_io_hdr(struct sg_io_hdr * p_hdr) {
    if (p_hdr) {
        free(p_hdr);
    }
}

void set_xfer_data(struct sg_io_hdr * p_hdr, void * data, unsigned int length) {
    if (p_hdr) {
        p_hdr->dxferp = data;
        p_hdr->dxfer_len = length;
    }
}

void set_sense_data(struct sg_io_hdr * p_hdr, unsigned char * data,
        unsigned int length) {
    if (p_hdr) {
        p_hdr->sbp = data;
        p_hdr->mx_sb_len = length;
    }
}

これらの関数は sg_io_hdr オブジェクトを設定するために使われます。sg_io_hdr オブジェクトのメンバーにはユーザー空間メモリーを指すポインターがあり、SCSI コマンド inquiry を実行した結果出力されるデータはポインター dxferp が指すメモリーにコピーされます。もしエラーが発生してセンス・データが要求されると、センス・データはポインター sbp が指す場所にコピーされます。SCSI ターゲットに inquiry コマンドを送信する例をリスト 3 に示します。

リスト 3. SCSI ターゲットへの inquiry コマンドの送信
int execute_Inquiry(int fd, int page_code, int evpd, struct sg_io_hdr * p_hdr) {
    unsigned char cdb[6];
    /* set the cdb format */
    cdb[0] = 0x12; /*This is for Inquery*/
    cdb[1] = evpd & 1;
    cdb[2] = page_code & 0xff;
    cdb[3] = 0;
    cdb[4] = 0xff;
    cdb[5] = 0; /*For control filed, just use 0 */
    
    p_hdr->dxfer_direction = SG_DXFER_FROM_DEV;
    p_hdr->cmdp = cdb;
    p_hdr->cmd_len = 6;

    int ret = ioctl(fd, SG_IO, p_hdr);
    if (ret<0) {
        printf("Sending SCSI Command failed.\n");
        close(fd);
        exit(1);
    }
    return p_hdr->status;
}

この関数は、まず inquiry の標準フォーマットに従って CDB を作成し、次に ioctl() 関数を呼び出してファイル記述子 SG_IOsg_io_hdr オブジェクトを渡します。返されたステータスは sg_io_hdr オブジェクトの status メンバーに保存されます。

今度はこの関数を使って inquiry コマンドを実行するアプリケーション・プログラムの例を見てみましょう (リスト 4)。

リスト 4. inquiry コマンドを実行するアプリケーション・プログラムの例
unsigned char sense_buffer[SENSE_LEN];
unsigned char data_buffer[BLOCK_LEN*256];
void test_execute_Inquiry(char * path, int evpd, int page_code) {
    struct sg_io_hdr * p_hdr = init_io_hdr();
    set_xfer_data(p_hdr, data_buffer, BLOCK_LEN*256);
    set_sense_data(p_hdr, sense_buffer, SENSE_LEN);
    int status = 0;
    int fd = open(path, O_RDWR);
    if (fd>0) {
        status = execute_Inquiry(fd, page_code, evpd, p_hdr);
        printf("the return status is %d\n", status);
        if (status!=0) {
            show_sense_buffer(p_hdr);
        } else{
            show_vendor(p_hdr);
            show_product(p_hdr);
            show_product_rev(p_hdr);
        }
    } else {
        printf("failed to open sg file %s\n", path);
    }
    close(fd);
    destroy_io_hdr(p_hdr);
}

この場合、SCSI コマンドを送信するプロセスは非常に単純です。まず、ユーザー空間のデータ・バッファーとセンス・バッファーを割り当て、sg_io_hdr オブジェクトを指すようにする必要があります。次にデバイス・ドライバーを開いてファイル記述子を取得します。これらのパラメーターが用意できると、ターゲット・デバイスに SCSI コマンドを送信することができます。そしてコマンドが終了すると、SCSI ターゲットからの出力をユーザー空間のバッファーにコピーすることができます。

リスト 5. パラメーターを使った、ターゲット・デバイスへの SCSI コマンドの送信
void show_vendor(struct sg_io_hdr * hdr) {
    unsigned char * buffer = hdr->dxferp;
    int i;
    printf("vendor id:");
    for (i=8; i<16; ++i) {
        putchar(buffer[i]);
    }
    putchar('\n');
}

void show_product(struct sg_io_hdr * hdr) {
    unsigned char * buffer = hdr->dxferp;
    int i;
    printf("product id:");
    for (i=16; i<32; ++i) {
        putchar(buffer[i]);
    }
    putchar('\n');
}

void show_product_rev(struct sg_io_hdr * hdr) {
    unsigned char * buffer = hdr->dxferp;
    int i;
    printf("product ver:");
    for (i=32; i<36; ++i) {
        putchar(buffer[i]);
    }
    putchar('\n');
}
int main(int argc, char * argv[]) {
    test_execute_Inquiry(argv[1], 0, 0);
    return EXIT_SUCCESS;
}

SCSI の inquiry コマンド (Page code フィールドと EVPD フィールドをすべて 0 に設定した場合) に対する標準的なレスポンスは複雑です。SCSI 標準によれば、ベンダー ID は 8 バイト目から 15 バイト目まで、製品 ID は 16 バイト目から 31 バイト目まで、製品バージョンは 32 バイト目から 35 バイト目までです。この情報を取得できると、コマンドが適切に実行されたかどうかをチェックすることができます。

この簡単な例を作成したら、これを /dev/sg0 (通常はローカルのハードディスク) に対して実行します。すると次のような結果が得られるはずです。

[root@taomaoy scsi_test]# ./scsi_test /dev/sg0
the return status is 0
vendor id:ATA
product id:ST3160812AS
product ver:3.AA

この結果は sg_map ツールでレポートされる結果と同じです。


まとめ

Linux には SCSI デバイスのための汎用のドライバーと API が用意されているため、SCSI デバイスに直接 SCSI コマンドを送信するアプリケーションを作成することができます。手動で SCSI コマンドを作成し、他の関連パラメーターを sg_io_hdr オブジェクトの中で設定し、ioctl() を呼び出してそれらの SCSI コマンドを実行すると、同じ sg_io_hdr オブジェクトから出力を得ることができます。


ダウンロード

内容ファイル名サイズ
Sample code for this articlescsi_test.zip3KB

参考文献

学ぶために

製品や技術を入手するために

  • developerWorks から直接ダウンロードできる IBM ソフトウェアの試用版を利用して皆さんの次期 Linux 開発プロジェクトを構築してください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=383639
ArticleTitle=Linux の汎用 SCSI ドライバー
publish-date=02252009