Знакомимся с универсальным SCSI-драйвером в Linux

Изучаем API универсального SCSI-драйвера Linux и знакомимся с примером его использования

Компьютеры управляют SCSI-устройствами и обмениваются с ними данными с помощью команд SCSI. В этой статье автор познакомит вас с некоторыми командами SCSI и методами их выполнения с помощью имеющегося в Linux API для SCSI. Сначала рассказывается о клиент-серверной модели SCSI и командах SCSI для работы с устройствами хранения данных. Затем он расскажет об API универсального (generic) SCSI-драйвера Linux и предложит для изучения пример системы, выполняющей с помощью этого драйвера SCSI-команду inquiry.

Мао Юань Тао, инженер, IBM

Мао Юань Тао работает инженером в лаборатории IBM в Китае (CSTL - China System and Technology Lab). Он имеет опыт в тестировании устройств хранения данных, настройке сред SAN (включая коммутаторы FC, backend-устройства хранения и компьютеры) и разработке инструментов тестирования.



16.07.2009

Клиент-серверная модель SCSI

При взаимодействии компьютера и устройства хранения компьютер обычно выступает в роли инициатора SCSI сессии, посылающего SCSI-команды. Устройство хранения обычно действует как целевое устройство SCSI (далее просто цель), принимающее и обрабатывающее SCSI-команды. Цель SCSI ожидает команды от инициатора и затем обеспечивает затребованную передачу входящих или исходящих данных.

Цель обычно предоставляет инициатору один или несколько номеров логических устройств(LUN - logical unit number). В системах хранения LUN – это просто некоторый номер (адрес), назначаемый логическому устройству. Логическое устройство– это единичная сущность протокола SCSI, которой можно адресовать фактические операции ввода/вывода. Каждая SCSI-цель предоставляет один или несколько адресов LUN. Цель не выполняет ввод/вывод самостоятельно, а действует от лица конкретной логической единицы.

В системах хранения данных LUN часто представляет SCSI-диск, на котором компьютер может осуществлять операции чтения и записи. На рисунке 1 показана работа клиент-серверной модели SCSI.

Рисунок 1. Клиент-серверная модель SCSI
The SCSI client/server model

Сначала инициатор посылает команду цели. Цель декодирует команду и запрашивает данные от инициатора, либо посылает данные инициатору, после чего отсылает инициатору статус выполнения операции. Если статус сигнализирует об ошибке, инициатор посылает цели команду с запросом детальной информации об ошибке. Цель возвращает детальную информацию, в которой указывается, что именно пошло не так.

Теперь мы рассмотрим SCSI-команды для работы с устройствами хранения.


SCSI-команды для работы с устройствами хранения данных

SCSI-команды, работающие с устройствами хранения данных, в основном определяются в архитектурной модели SCSI (SAM - SCSI Architecture Model), наборе основных SCSI-команд (SPC - SCSI Primary Commands) и наборе блочных SCSI команд (SBC - SCSI Block Commands):

  • SAM определяет модель SCSI-системы, разделение на функциональные блоки, стандарты реализации и требования, применимые ко всем реализациям SCSI.
  • SPC определяет поведение, являющееся общим для всех архитектурных моделей SCSI-устройств.
  • SBC определяет дополнительные наборы команд для облегчения операций с блочными SCSI-устройствами прямого доступа.

Каждая SCSI-команда описывается блоком дескриптора команды (CDB - Command Descriptor Block), определяющим операции, которые необходимо выполнить SCSI-устройству. Различают SCSI-команды для передачи данных между устройством хранения и клиентом и настроечные команды, позволяющие узнать или задать значения параметров SCSI-устройства. В таблице 1 показаны наиболее широко распространенные команды

Таблица 1. Самые распространенные SCSI-команды
КомандаОписание
InquiryЗапрашивает основную информацию о целевом устройстве
Test/Unit/ReadyПроверяет, готово ли устройство к передаче данных
READЧтение данных с SCSI-устройства
WRITEЗапись данных на SCSI-устройство
Request SenseЗапрашивает информацию об ошибке выполнения последней команды
Read CapacityЗапрашивает ёмкость устройства хранения

В первом байте каждой SCSI-команды должен находиться код операции, которую представляет эта команда. Кроме того, SCSI-команды должны содержать контрольный байт. Обычно это последний байт команды, выделенный для передачи информации, специфичной для производителя устройства, а также иных нужд.

Теперь перейдем к рассмотрению универсального SCSI-драйвера.


Универсальный SCSI-драйвер Linux

В Linux SCSI-устройства часто имеют названия, которые помогают пользователю их идентифицировать. Например, первый SCSI CD-ROM называется /dev/scd0. SCSI-диски называются /dev/sda, /dev/sdb, /dev/sdc и т.д. После окончания инициализации устройства в Linux интерфейсы драйвера SCSI-диска (sd) посылают только SCSI-команды READ и WRITE.

SCSI-устройства также могут иметь универсальные имена или интерфейсы, такие как /dev/sg0, /dev/sg1 или /dev/sga, /dev/sgb и т.д. С помощью интерфейсов универсального драйвера вы можете передавать SCSI-команды напрямую устройству, минуя уровень файловой системы, обычно создаваемой на SCSI-диске и монтируемой в директорию. На рисунке 2 вы можете видеть, как различные приложения взаимодействуют со SCSI-устройствами.

Рисунок 2. Множество способов взаимодействия со SCSI-устройством
The myriad ways of communicating with a SCSI device

С помощью интерфейса универсального драйвера Linux вы можете создавать приложения, умеющие посылать SCSI-устройствам еще более широкий набор команд. Иными словами, у вас есть выбор. Чтобы определить, какому SCSI-устройству соответствует некоторый интерфейс sg, используйте команду sg_map, которая выводит карту SCSI-устройств:

Во-первых, убедимся, что у нас загружен драйвер sg:

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

Если grep не выводит никакой информации о драйвере sg, загрузите его вручную:

[root@taomaoy ~]# modprobe sg

Затем с помощью команды sg_map посмотрите отображение SCSI-устройств в интерфейсы sg:

[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

Если вы работаете с Fedora или RedHat, у вас должен быть установлен пакет sg3_utils. Давайте теперь посмотрим, как выполнить типичный вызов SCSI-команды.


Типичные команды универсального SCSI-драйвера

Универсальный SCSI-драйвер поддерживает множество типичных системных вызовов к своему устройству, в том числе open(), close(), read(), write, poll(), ioctl(). Процедура посылки SCSI-команды на устройство также очень проста:

  1. Открываем файл универсального SCSI-устройства, (например sg1) чтобы получить файловый дескриптор SCSI-устройства.
  2. Подготавливаем SCSI-команду.
  3. Задаем необходимые буферы памяти.
  4. Вызываем функцию ioctl() для выполнения SCSI-команды.
  5. Закрываем файл устройства.

Типичный вызов функции ioctl() может выглядеть так: ioctl(fd,SG_IO,p_io_hdr);.

Здесь ioctl() принимает три обязательных параметра:

  1. fd – дескриптор файла устройства. Этот параметр можно получить, вызвав open() для файла устройства.
  2. SG_IO указывает, что функции ioctl() третьим параметром передается объект типа sg_io_hdr и что именно этот объект будет возвращен после выполнения SCSI-команды.
  3. p_io_hdr - указатель на объект sg_io_hdr, содержащий SCSI-команду и другие параметры операции.

Самой важной структурой данных для универсального драйвера SCSI является структура struct sg_io_hdr, определенная в scsi/sg.h. Она содержит в себе информацию о том, как использовать SCSI-команду. В листинге 1 показано определение этой структуры.

Листинг 1. Определение структуры sg_io_hdr
typedef struct sg_io_hdr
{
    int interface_id;               /* [i] 'S' (обязательно) */
    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 байта длиной (на i386) */

Не все поля этой структуры являются обязательными, поэтому я опишу здесь только наиболее часто используемые:

  • interface_id: всегда должно быть равно S.
  • dxfer_direction: указывает направление передачи данных; может принимать одно из значений:
    • 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: размер указателя cmdp, указывающего на SCSI-команду.
  • mx_sb_len: максимальный размер данных, которые могут быть записаны по адресу указателя sbp, когда вывод осуществляется в sense_buffer.
  • dxfer_len: размер пользовательской памяти, предназначенной для передачи данных.
  • dxferp: указатель на пользовательскую память размером не менее dxfer_len байт, предназначенную для передачи данных.
  • cmdp: указатель на SCSI-команду, которую нужно выполнить.
  • sbp: указатель на буфер информации об ошибке.
  • timeout: используется для задания таймаута выполнения команды command.
  • status: байт состояния, определяемый согласно стандарту SCSI.

Схематично работа с этой структурой выглядит так: указатель cmdp должен указывать на блок SCSI CDB, длина которого хранится в cmd_len; sbp указывает на пользовательскую память размером максимум mx_sb_len. По этому адресу в случае возникновения ошибки будет записана подробная информация об ошибке. Поле dxferp указывает на память SCSI-устройства, откуда (или куда) будут переданы данные (в зависимости от параметра dxfer_direction).

И, наконец, давайте рассмотрим команду inquiry и ее выполнение с помощью универсального драйвера.


Пример: выполнение команды inquiry

Команда inquiry – это наиболее распространенная команда, поддерживаемая всеми SCSI-устройствами. Эта команда используется для получения основной информации о SCSI-устройстве и часто используется в качестве операции ping для проверки доступности SCSI-устройства. В таблице 2 показан формат команды inquiry согласно стандарту SCSI.

Таблица 2. Формат команды inquiry
бит 7бит 6бит 5бит 4бит 3бит 2бит 1бит 0
байт 0код операции = 12h
байт 1LUNзарезервированоEVPD
байт 2код страницы
байт 3зарезервировано
байт 4размер выделенной памяти
байт 5контрольный

Если бит параметра EVPD (enable vital product data - выводить главную информацию о продукте) равен 0 и байт параметра Page Code также равен 0, то будет возвращен стандартный результат команды inquire. Если бит EVPD равен 1, то будет возвращена некая специфичная для производителя устройства информация в соответствии с полем Page Code.

В листинге 2 показаны фрагменты кода, иллюстрирующие использование универсального API для SCSI. Давайте сначала рассмотрим пример задания значений 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'; /* у нас нет другого выбора! */
    /* помещаем LUN во второй байт блока 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. Некоторые его поля указывают на память в пользовательском пространстве. При успешном выполнении выходные данные команды SCSI, копируются в память, на которую указывает dxferp. В противном случае туда, куда указывает sbp будет записана подробная информация о возникшей ошибке. В листинге 3 показан пример посылки команды inquiry целевому SCSI-устройству.

Листинг 3. Посылка команды inquiry целевому SCSI-устройству.
int execute_Inquiry(int fd, int page_code, int evpd, struct sg_io_hdr * p_hdr) {
    unsigned char cdb[6];
    /* задаем формат блока cdb */
    cdb[0] = 0x12; /*код операции для Inquiry*/
    cdb[1] = evpd & 1;
    cdb[2] = page_code & 0xff;
    cdb[3] = 0;
    cdb[4] = 0xff;
    cdb[5] = 0; /*контрольный байт, просто используем 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;
}

Итак, сначала функция подготавливает блок CDB в соответствии со стандартным форматом команды inquiry. Затем она вызывает функцию ioctl(), передавая в нее файловый дескриптор SG_IO и объект sg_io_hdr; статус ответа потом помещается в поле status объекта sg_io_hdr.

Теперь давайте посмотрим, как приложение использует эту функцию для выполнения команды 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-устройства в буфер(ы), находящиеся в пользовательской памяти.

Листинг 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) имеет довольно сложный формат. Согласно стандарту, 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-устройств и интерфейс разработки приложений к нему, позволяющий вам в ваших приложениях посылать SCSI-команды напрямую SCSI-устройствам. Вы можете вручную создавать SCSI-команды, задавать необходимые параметры в объекте sg_io_hdr, затем вызывать ioctl() для выполнения этих команд и получать результат в том же самом объекте sg_io_hdr.


Загрузка

ОписаниеИмяРазмер
Образцы кода для этой статьиscsi_test.zip3КБ

Ресурсы

Научиться

Получить продукты и технологии

  • Разработайте ваш следующий Linux-проект с помощью ознакомительного ПО от IBM, которое можно загрузить непосредственно с сайта developerWorks.

Обсудить

  • Участвуйте в жизни сообщества developerWorks (EN) - посещайте блоги, форумы, подкасты и дискуссионные пространства.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=413586
ArticleTitle=Знакомимся с универсальным SCSI-драйвером в Linux
publish-date=07162009