Глава 12. Контроллеры SCSI с общим методом доступа (CAM) | FreeBSD Documentation Portal
Description: Контроллеры SCSI с общим методом доступа (CAM)
Keywords: SCSI,Controller,Architecture

# Глава 12. Контроллеры SCSI с общим методом доступа (CAM)

Этот перевод может быть устаревшим. Для того, чтобы помочь с переводом, пожалуйста, обратитесь к Сервер переводов FreeBSD.

## 12.1. Обзор

Этот документ предполагает, что читатель имеет общее представление о драйверах устройств в FreeBSD и о протоколе SCSI. Большая часть информации в этом документе была извлечена из драйверов:

- ncr (/sys/pci/ncr.c) от Wolfgang Stanglmeier и Stefan Esser

- sym (/sys/dev/sym/sym_hipd.c) от Gerard Roudier

- aic7xxx (/sys/dev/aic7xxx/aic7xxx.c) от Justin T. Gibbs

и из самого кода CAM (автор Justin T. Gibbs, см. /sys/cam/*). Когда какое-то решение выглядело наиболее логичным и было практически дословно взято из кода Justin T. Gibbs, я отмечал его как "рекомендуемое".

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

## 12.2. Общая архитектура

CAM означает Common Access Method (Общий Метод Доступа). Это универсальный способ адресации шин ввода-вывода в стиле SCSI. Это позволяет отделить общие драйверы устройств от драйверов, управляющих шиной ввода-вывода: например, драйвер диска получает возможность управлять дисками как на SCSI, IDE, так и на любой другой шине, так что часть драйвера диска не нужно переписывать (или копировать и изменять) для каждой новой шины ввода-вывода. Таким образом, двумя наиболее важными активными сущностями являются:

- Модули периферийных устройств - драйвер для периферийных устройств (диски, ленты, CD-ROM и т.д.)

- Модули интерфейса SCSI (SIM) - драйверы адаптеров шины для подключения к шине ввода-вывода, такой как SCSI или IDE.

Периферийный драйвер получает запросы от ОС, преобразует их в последовательность команд SCSI и передаёт эти команды SCSI модулю интерфейса SCSI. Модуль интерфейса SCSI отвечает за передачу этих команд реальному оборудованию (или, если оборудование не поддерживает SCSI, а использует, например, IDE, также преобразует команды SCSI в собственные команды оборудования).

Так как мы заинтересованы в написании драйвера адаптера SCSI, с этого момента мы будем рассматривать всё с точки зрения модуля SCSI-интерфейса (SIM).

## 12.3. Глобальные переменные и Шаблонный код

Типичный драйвер SIM должен включать следующие заголовочные файлы, связанные с CAM:

    #include <cam/cam.h>
    #include <cam/cam_ccb.h>
    #include <cam/cam_sim.h>
    #include <cam/cam_xpt_sim.h>
    #include <cam/cam_debug.h>
    #include <cam/scsi/scsi_all.h>

## 12.4. Конфигурация устройства: xxx_attach

Первое, что должен сделать каждый драйвер SIM, — это зарегистрироваться в подсистеме CAM. Это выполняется в функции `xxx_attach()` драйвера (здесь и далее xxx_ используется для обозначения уникального префикса имени драйвера). Сама функция `xxx_attach()` вызывается кодом автонастройки системной шины, который мы здесь не описываем.

Это достигается в несколько этапов: сначала необходимо выделить очередь запросов, связанных с этой SIM:

        struct cam_devq *devq;

        if ((devq = cam_simq_alloc(SIZE)) == NULL) {
            error; /* some code to handle the error */
        }

Вот `SIZE` — это размер выделяемой очереди, максимальное количество запросов, которые она может содержать. Это количество запросов, которые драйвер SIM может обрабатывать параллельно на одной SCSI-карте. Обычно его можно вычислить как:

    SIZE = NUMBER_OF_SUPPORTED_TARGETS * MAX_SIMULTANEOUS_COMMANDS_PER_TARGET

Далее мы создаем описание нашего SIM:

        struct cam_sim *sim;

        if ((sim = cam_sim_alloc(action_func, poll_func, driver_name,
                softc, unit, mtx, max_dev_transactions,
                max_tagged_dev_transactions, devq)) == NULL) {
            cam_simq_free(devq);
            error; /* some code to handle the error */
        }

Обратите внимание, что если мы не сможем создать дескриптор SIM, мы также освобождаем `devq`, потому что больше ничего не можем с ним сделать и хотим сэкономить память.

Если SCSI-карта имеет несколько шин SCSI, то каждой шине требуется собственная структура `cam_sim`.

Интересный вопрос: что делать, если SCSI-карта имеет более одной SCSI-шины, нужна ли одна структура `devq` на карту или на SCSI-шину? Ответ, приведённый в комментариях к коду CAM, таков: как угодно, на усмотрение автора драйвера.

Аргументы:

- `action_func` - указатель на функцию `xxx_action` драйвера.

    static void xxx_action(struct cam_sim *, union ccb *);

- `poll_func` - указатель на функцию `xxx_poll()` драйвера

      static void xxx_poll(struct cam_sim *);

- `driver_name` — имя фактического драйвера, например `ncr` или `wds`.

- `softc` — указатель на внутренний дескриптор драйвера для данной SCSI-карты. Этот указатель будет использоваться драйвером в дальнейшем для получения приватных данных.

- unit - номер управляющего устройства, например, для контроллера "mps0" это число будет 0

- mtx - Блокировка, связанная с данной SIM. Для SIM, которые не поддерживают блокировку, передаётся Giant. Для SIM, которые поддерживают, передаётся блокировка, используемая для защиты структур данных этой SIM. Эта блокировка будет удерживаться при вызовах xxx_action и xxx_poll.

- max_dev_transactions - максимальное количество одновременных транзакций на целевом SCSI-устройстве в режиме без тегов. Это значение почти всегда равно 1, за исключением возможных исключений только для не-SCSI карт. Также драйверы, которые надеются получить преимущество, подготавливая одну транзакцию во время выполнения другой, могут установить его в 2, но это не кажется оправданным из-за сложности.

- max_tagged_dev_transactions - то же самое, но в режиме с тегами. Теги — это способ в SCSI инициировать несколько транзакций на устройстве: каждая транзакция получает уникальный тег и отправляется на устройство. Когда устройство завершает транзакцию, оно возвращает результат вместе с тегом, чтобы SCSI-адаптер (и драйвер) могли определить, какая транзакция была завершена. Этот аргумент также известен как максимальная глубина тега. Он зависит от возможностей SCSI-адаптера.

Наконец, мы регистрируем шины SCSI, связанные с нашим SCSI-адаптером:

        if (xpt_bus_register(sim, softc, bus_number) != CAM_SUCCESS) {
            cam_sim_free(sim, /*free_devq*/ TRUE);
            error; /* some code to handle the error */
        }

Если существует одна структура `devq` на каждую шину SCSI (т.е. мы рассматриваем карту с несколькими шинами как несколько карт с одной шиной каждая), то номер шины всегда будет 0, в противном случае каждая шина на SCSI-карте должна получить уникальный номер. Каждой шине требуется своя отдельная структура `cam_sim`.

После этого наш контроллер полностью подключён к системе CAM. Значение `devq` теперь можно отбросить: sim будет передаваться в качестве аргумента во всех последующих вызовах из CAM, а devq можно получить из него.

CAM предоставляет инфраструктуру для подобных асинхронных событий. Некоторые события возникают на нижних уровнях (драйверы SIM), некоторые — в драйверах периферийных устройств, а некоторые — в самой подсистеме CAM. Любой драйвер может зарегистрировать обработчики для определённых типов асинхронных событий, чтобы получать уведомления при их возникновении.

Типичным примером такого события является сброс устройства. Каждая транзакция и событие идентифицируют устройства, к которым они применяются, с помощью "пути". Специфичные для целевого устройства события обычно происходят во время транзакции с этим устройством. Таким образом, путь из этой транзакции может быть повторно использован для сообщения о данном событии (это безопасно, потому что путь события копируется в процедуре сообщения о событии, но не освобождается и не передаётся дальше). Также безопасно динамически выделять пути в любое время, включая процедуры обработки прерываний, хотя это влечёт определённые накладные расходы, и возможная проблема такого подхода заключается в том, что в этот момент может не быть свободной памяти. Для события сброса шины нам необходимо определить путь-шаблон, включающий все устройства на шине. Поэтому мы можем заранее создать путь для будущих событий сброса шины и избежать проблем с возможной нехваткой памяти в будущем:

        struct cam_path *path;

        if (xpt_create_path(&path, /*periph*/NULL,
                    cam_sim_path(sim), CAM_TARGET_WILDCARD,
                    CAM_LUN_WILDCARD) != CAM_REQ_CMP) {
            xpt_bus_deregister(cam_sim_path(sim));
            cam_sim_free(sim, /*free_devq*/TRUE);
            error; /* some code to handle the error */
        }

        softc->wpath = path;
        softc->sim = sim;

Как вы можете видеть, путь включает:

- Идентификатор драйвера периферийного устройства (NULL здесь, так как у нас его нет)

- Идентификатор драйвера SIM (`cam_sim_path(sim)`)

- Номер целевого устройства SCSI (CAM_TARGET_WILDCARD означает "все устройства")

- Номер SCSI LUN подустройства (CAM_LUN_WILDCARD означает "все LUN")

Если драйвер не может выделить этот путь, он не сможет нормально работать, поэтому в таком случае мы демонтируем эту шину SCSI.

И мы сохраняем указатель пути в структуре `softc` для дальнейшего использования. После этого сохраняем значение sim (или можем также отбросить его при выходе из `xxx_probe()`, если захотим).

Вот и всё для минималистичной инициализации. Чтобы сделать всё правильно, остался ещё один вопрос.

Для драйвера SIM есть одно особенно важное событие: когда целевое устройство считается потерянным. В этом случае может быть хорошей идеей сбросить SCSI-переговоры с этим устройством. Поэтому мы регистрируем обратный вызов для этого события в CAM. Запрос передаётся в CAM путём запроса действия CAM в блоке управления CAM для этого типа запроса:

        struct ccb_setasync csa;

        xpt_setup_ccb(&csa.ccb_h, path, /*priority*/5);
        csa.ccb_h.func_code = XPT_SASYNC_CB;
        csa.event_enable = AC_LOST_DEVICE;
        csa.callback = xxx_async;
        csa.callback_arg = sim;
        xpt_action((union ccb *)&csa);

## 12.5. Обработка сообщений CAM: xxx_action

    static void xxx_action(struct cam_sim *sim, union ccb *ccb);

Выполнить некоторое действие по запросу подсистемы CAM. Sim описывает SIM для запроса, CCB — это сам запрос. CCB расшифровывается как "CAM Control Block" (блок управления CAM). Это объединение множества конкретных экземпляров, каждый из которых описывает аргументы для определённого типа транзакций. Все эти экземпляры имеют общий заголовок CCB, в котором хранится общая часть аргументов.

CAM поддерживает SCSI-контроллеры, работающие как в режиме инициатора («обычном»), так и в режиме цели (эмулирующем SCSI-устройство). Здесь мы рассматриваем только часть, относящуюся к режиму инициатора.

Существует несколько функций и макросов (другими словами, методов), определённых для доступа к публичным данным в структуре sim:

- `cam_sim_path(sim)` - идентификатор пути (см. выше)

- `cam_sim_name(sim)` — имя sim

- `cam_sim_softc(sim)` - указатель на структуру softc (приватные данные драйвера)

- `cam_sim_unit(sim)` - номер устройства

- `cam_sim_bus(sim)` - идентификатор шины

Для идентификации устройства `xxx_action()` может получить номер устройства и указатель на его структуру softc, используя следующие функции.

Тип запроса хранится в `ccb→ccb_h.func_code`. Поэтому, как правило, `xxx_action()` состоит из большого оператора switch:

        struct xxx_softc *softc = (struct xxx_softc *) cam_sim_softc(sim);
        struct ccb_hdr *ccb_h = &ccb->ccb_h;
        int unit = cam_sim_unit(sim);
        int bus = cam_sim_bus(sim);

        switch (ccb_h->func_code) {
        case ...:
            ...
        default:
            ccb_h->status = CAM_REQ_INVALID;
            xpt_done(ccb);
            break;
        }

Как видно из случая по умолчанию (если получена неизвестная команда) код возврата команды устанавливается в `ccb→ccb_h.status`, а завершённый CCB возвращается обратно в CAM вызовом `xpt_done(ccb)`.

`xpt_done()` не обязательно вызывать из `xxx_action()`: Например, запрос ввода-вывода может быть поставлен в очередь внутри драйвера SIM и/или его SCSI-контроллера. Затем, когда устройство пошлет прерывание, сигнализирующее о завершении обработки этого запроса, `xpt_done()` может быть вызван из процедуры обработки прерывания.

На самом деле, статус CCB не только присваивается в качестве кода возврата, но и CCB всегда имеет какой-то статус. Перед тем как CCB передаётся в процедуру `xxx_action()`, он получает статус CCB_REQ_INPROG, означающий, что запрос находится в процессе выполнения. В /sys/cam/cam.h определено удивительно большое количество значений статуса, которые должны детально отражать состояние запроса. Что ещё интереснее, статус фактически представляет собой "побитовое ИЛИ" перечисленного значения статуса (младшие 6 бит) и возможных дополнительных флагов (старшие биты). Перечисленные значения будут подробно рассмотрены далее. Их краткое описание можно найти в разделе "Сводка ошибок". Возможные флаги статуса:

- CAM_DEV_QFRZN - если драйвер SIM получает серьёзную ошибку (например, устройство не отвечает на выборку или нарушает протокол SCSI) при обработке CCB, он должен заморозить очередь запросов, вызвав `xpt_freeze_simq()`, вернуть другие поставленные в очередь, но ещё не обработанные CCB для этого устройства обратно в очередь CAM, затем установить этот флаг для проблемного CCB и вызвать `xpt_done()`. Этот флаг заставляет подсистему CAM разморозить очередь после обработки ошибки.

- CAM_AUTOSNS_VALID - если устройство вернуло состояние ошибки и флаг CAM_DIS_AUTOSENSE не установлен в CCB, драйвер SIM должен автоматически выполнить команду REQUEST SENSE, чтобы извлечь данные sense (расширенную информацию об ошибке) из устройства. Если попытка была успешной, данные sense должны быть сохранены в CCB, а этот флаг установлен.

- CAM_RELEASE_SIMQ - аналогично CAM_DEV_QFRZN, но используется в случае возникновения проблем (или нехватки ресурсов) с самим SCSI-контроллером. В этом случае все последующие запросы к контроллеру должны быть остановлены с помощью `xpt_freeze_simq()`. Очередь контроллера будет возобновлена после того, как драйвер SIM устранит нехватку и уведомит CAM, вернув некоторый CCB с установленным этим флагом.

- CAM_SIM_QUEUED - этот флаг должен быть установлен, когда SIM помещает CCB в свою очередь запросов (и снят, когда этот CCB извлекается из очереди перед возвратом в CAM). В настоящее время этот флаг нигде не используется в коде CAM, поэтому его назначение чисто диагностическое.

- CAM_QOS_VALID - Данные QOS теперь действительны.

Функция `xxx_action()` не может находиться в состоянии ожидания, поэтому вся синхронизация доступа к ресурсам должна выполняться с использованием SIM или заморозки очереди устройств. Помимо упомянутых флагов, подсистема CAM предоставляет функции `xpt_release_simq()` и `xpt_release_devq()` для разморозки очередей напрямую, без передачи CCB в CAM.

Заголовок CCB содержит следующие поля:

- path - идентификатор пути для запроса

- target_id - идентификатор целевого устройства для запроса

- target_lun - идентификатор LUN целевого устройства

- timeout - интервал таймаута для этой команды, в миллисекундах

- timeout_ch - удобное место для драйвера SIM, чтобы хранить обработчик таймаута (сама подсистема CAM не делает никаких предположений о нём)

- flags - различные биты информации о запросе spriv_ptr0, spriv_ptr1 — поля, зарезервированные для приватного использования драйвером SIM (например, для связи с очередями SIM или приватными блоками управления SIM); фактически они существуют как объединения: spriv_ptr0 и spriv_ptr1 имеют тип (void *), spriv_field0 и spriv_field1 имеют тип unsigned long, sim_priv.entries[0].bytes и sim_priv.entries[1].bytes - это байтовые массивы размера, согласованного с другими вариантами объединения, а sim_priv.bytes - это один массив, вдвое большего размера.

Рекомендуемый способ использования приватных полей SIM в CCB — это определить для них осмысленные имена и использовать эти осмысленные имена в драйвере, например:

    #define ccb_some_meaningful_name    sim_priv.entries[0].bytes
    #define ccb_hcb spriv_ptr1 /* for hardware control block */

Наиболее распространённые запросы в режиме инициатора:

### 12.5.1. XPT_SCSI_IO — выполнить транзакцию ввода-вывода

Экземпляр "struct ccb_scsiio csio" объединения ccb используется для передачи аргументов. Они включают:

- cdb_io - указатель на буфер команды SCSI или сам буфер

- cdb_len - длина команды SCSI

- data_ptr - указатель на буфер данных (усложняется, если используется scatter/gather)

- dxfer_len - длина передаваемых данных

- sglist_cnt - счетчик сегментов scatter/gather

- scsi_status - место для возврата статуса SCSI

- sense_data - буфер для информации SCSI sense, если команда возвращает ошибку (драйвер SIM должен автоматически выполнить команду REQUEST SENSE в этом случае, если флаг CCB CAM_DIS_AUTOSENSE не установлен)

- sense_len - длина этого буфера (если она окажется больше размера sense_data, драйвер SIM должен без уведомления принять меньшее значение)

- resid, sense_resid — если передача данных или SCSI sense вернула ошибку, это счётчики остаточных (не переданных) данных. Они не кажутся особенно значимыми, поэтому в случаях, когда их сложно вычислить (например, подсчёт байтов в FIFO-буфере SCSI-контроллера), подойдёт и приблизительное значение. Для успешно завершённой передачи они должны быть установлены в ноль.

- tag_action - тип используемого тега:

  - `CAM_TAG_ACTION_NONE` - не использовать теги для данной транзакции

  - MSG_SIMPLE_Q_TAG, MSG_HEAD_OF_Q_TAG, MSG_ORDERED_Q_TAG — значение, соответствующее указанному теговому сообщению (см. /sys/cam/scsi/scsi_message.h); указывает только тип тега, значение тега должно быть назначено самим драйвером SIM

Общая логика обработки этого запроса следующая:

Первое, что нужно сделать, это проверить возможные состояния гонки, чтобы убедиться, что команда не была прервана, пока находилась в очереди:

        struct ccb_scsiio *csio = &ccb->csio;

        if ((ccb_h->status & CAM_STATUS_MASK) != CAM_REQ_INPROG) {
            xpt_done(ccb);
            return;
        }

Также мы проверяем, что устройство вообще поддерживается нашим контроллером:

        if (ccb_h->target_id > OUR_MAX_SUPPORTED_TARGET_ID
        || cch_h->target_id == OUR_SCSI_CONTROLLERS_OWN_ID) {
            ccb_h->status = CAM_TID_INVALID;
            xpt_done(ccb);
            return;
        }
        if (ccb_h->target_lun > OUR_MAX_SUPPORTED_LUN) {
            ccb_h->status = CAM_LUN_INVALID;
            xpt_done(ccb);
            return;
        }

Затем выделяем все необходимые структуры данных (такие как зависящий от карты блок управления оборудованием), которые нам нужны для обработки этого запроса. Если мы не можем этого сделать, то замораживаем очередь SIM и запоминаем, что у нас есть отложенная операция, возвращаем CCB обратно и просим CAM поставить его в очередь снова. Позже, когда ресурсы станут доступны, очередь SIM должна быть разморожена путём возврата CCB с установленным битом `CAM_SIMQ_RELEASE` в его статусе. В противном случае, если всё прошло успешно, связываем CCB с блоком управления оборудованием (HCB) и помечаем его как поставленный в очередь.

        struct xxx_hcb *hcb = allocate_hcb(softc, unit, bus);

        if (hcb == NULL) {
            softc->flags |= RESOURCE_SHORTAGE;
            xpt_freeze_simq(sim, /*count*/1);
            ccb_h->status = CAM_REQUEUE_REQ;
            xpt_done(ccb);
            return;
        }

        hcb->ccb = ccb; ccb_h->ccb_hcb = (void *)hcb;
        ccb_h->status |= CAM_SIM_QUEUED;

Извлечь целевые данные из CCB в аппаратный блок управления. Проверить, запрошено ли назначение тега, и если да, то сгенерировать уникальный тег и построить сообщения тега SCSI. Драйвер SIM также отвечает за согласование с устройствами для установки максимальной взаимно поддерживаемой ширины шины, синхронной скорости и смещения.

        hcb->target = ccb_h->target_id; hcb->lun = ccb_h->target_lun;
        generate_identify_message(hcb);
        if (ccb_h->tag_action != CAM_TAG_ACTION_NONE)
            generate_unique_tag_message(hcb, ccb_h->tag_action);
        if (!target_negotiated(hcb))
            generate_negotiation_messages(hcb);

Затем настройте команду SCSI. Хранилище команды может быть указано в CCB различными способами, определяемыми флагами CCB. Буфер команды может содержаться в CCB или указываться на него; в последнем случае указатель может быть физическим или виртуальным. Поскольку оборудованию обычно требуется физический адрес, мы всегда преобразуем адрес в физический, как правило, используя API busdma.

В случае, если запрашивается физический адрес, допустимо вернуть CCB со статусом `CAM_REQ_INVALID`, текущие драйверы так и делают. При необходимости физический адрес также может быть преобразован или отображён обратно в виртуальный, но с большими трудностями, поэтому мы этого не делаем.

        if (ccb_h->flags & CAM_CDB_POINTER) {
            /* CDB is a pointer */
            if (!(ccb_h->flags & CAM_CDB_PHYS)) {
                /* CDB pointer is virtual */
                hcb->cmd = vtobus(csio->cdb_io.cdb_ptr);
            } else {
                /* CDB pointer is physical */
                hcb->cmd = csio->cdb_io.cdb_ptr ;
            }
        } else {
            /* CDB is in the ccb (buffer) */
            hcb->cmd = vtobus(csio->cdb_io.cdb_bytes);
        }
        hcb->cmdlen = csio->cdb_len;

Теперь настало время настроить данные. Опять же, хранилище данных может быть указано в CCB различными интересными способами, определяемыми флагами CCB. Сначала мы получаем направление передачи данных. Самый простой случай — если нет данных для передачи:

        int dir = (ccb_h->flags & CAM_DIR_MASK);

        if (dir == CAM_DIR_NONE)
            goto end_data;

Затем мы проверяем, находятся ли данные в одном фрагменте или в списке scatter-gather, а также являются ли адреса физическими или виртуальными. SCSI-контроллер может обрабатывать только ограниченное количество фрагментов ограниченной длины. Если запрос превышает это ограничение, мы возвращаем ошибку. Мы используем специальную функцию для возврата CCB, чтобы в одном месте обрабатывать нехватку ресурсов HCB. Функции для добавления фрагментов зависят от драйвера, и здесь мы оставляем их без детальной реализации. Подробности о проблемах трансляции адресов см. в описании обработки SCSI-команд (CDB). Если какая-то вариация слишком сложна или невозможна для реализации с конкретной картой, допустимо вернуть статус `CAM_REQ_INVALID`. На самом деле, похоже, что возможность scatter-gather нигде в коде CAM сейчас не используется. Но как минимум случай с единичным неразделённым виртуальным буфером должен быть реализован, так как он активно используется CAM.

        int rv;

        initialize_hcb_for_data(hcb);

        if ((!(ccb_h->flags & CAM_SCATTER_VALID)) {
            /* single buffer */
            if (!(ccb_h->flags & CAM_DATA_PHYS)) {
                rv = add_virtual_chunk(hcb, csio->data_ptr, csio->dxfer_len, dir);
                }
            } else {
                rv = add_physical_chunk(hcb, csio->data_ptr, csio->dxfer_len, dir);
            }
        } else {
            int i;
            struct bus_dma_segment *segs;
            segs = (struct bus_dma_segment *)csio->data_ptr;

            if ((ccb_h->flags & CAM_SG_LIST_PHYS) != 0) {
                /* The SG list pointer is physical */
                rv = setup_hcb_for_physical_sg_list(hcb, segs, csio->sglist_cnt);
            } else if (!(ccb_h->flags & CAM_DATA_PHYS)) {
                /* SG buffer pointers are virtual */
                for (i = 0; i < csio->sglist_cnt; i++) {
                    rv = add_virtual_chunk(hcb, segs[i].ds_addr,
                        segs[i].ds_len, dir);
                    if (rv != CAM_REQ_CMP)
                        break;
                }
            } else {
                /* SG buffer pointers are physical */
                for (i = 0; i < csio->sglist_cnt; i++) {
                    rv = add_physical_chunk(hcb, segs[i].ds_addr,
                        segs[i].ds_len, dir);
                    if (rv != CAM_REQ_CMP)
                        break;
                }
            }
        }
        if (rv != CAM_REQ_CMP) {
            /* we expect that add_*_chunk() functions return CAM_REQ_CMP
             * if they added a chunk successfully, CAM_REQ_TOO_BIG if
             * the request is too big (too many bytes or too many chunks),
             * CAM_REQ_INVALID in case of other troubles
             */
            free_hcb_and_ccb_done(hcb, ccb, rv);
            return;
        }
        end_data:

Если отключение запрещено для этого CCB, мы передаем эту информацию в hcb:

        if (ccb_h->flags & CAM_DIS_DISCONNECT)
            hcb_disable_disconnect(hcb);

Если контроллер способен самостоятельно выполнять команду REQUEST SENSE, то ему также следует передать значение флага CAM_DIS_AUTOSENSE, чтобы предотвратить автоматическое выполнение REQUEST SENSE, если подсистема CAM этого не требует.

Осталось только установить таймаут, передать наш hcb оборудованию и вернуться, остальное будет сделано обработчиком прерывания (или обработчиком таймаута).

        ccb_h->timeout_ch = timeout(xxx_timeout, (caddr_t) hcb,
            (ccb_h->timeout * hz) / 1000); /* convert milliseconds to ticks */
        put_hcb_into_hardware_queue(hcb);
        return;

И вот возможная реализация функции, возвращающей CCB:

        static void
        free_hcb_and_ccb_done(struct xxx_hcb *hcb, union ccb *ccb, u_int32_t status)
        {
            struct xxx_softc *softc = hcb->softc;

            ccb->ccb_h.ccb_hcb = 0;
            if (hcb != NULL) {
                untimeout(xxx_timeout, (caddr_t) hcb, ccb->ccb_h.timeout_ch);
                /* we're about to free a hcb, so the shortage has ended */
                if (softc->flags & RESOURCE_SHORTAGE)  {
                    softc->flags &= ~RESOURCE_SHORTAGE;
                    status |= CAM_RELEASE_SIMQ;
                }
                free_hcb(hcb); /* also removes hcb from any internal lists */
            }
            ccb->ccb_h.status = status |
                (ccb->ccb_h.status & ~(CAM_STATUS_MASK|CAM_SIM_QUEUED));
            xpt_done(ccb);
        }

### 12.5.2. XPT_RESET_DEV — отправить устройству сообщение SCSI "BUS DEVICE RESET"

В CCB не передаются данные, кроме заголовка, и наиболее интересным аргументом в нём является target_id. В зависимости от аппаратного обеспечения контроллера может быть создан аппаратный блок управления (как для запроса XPT_SCSI_IO, см. описание запроса XPT_SCSI_IO) и отправлен контроллеру, или SCSI-контроллер может быть немедленно запрограммирован на отправку этого сообщения RESET устройству, или этот запрос может просто не поддерживаться (и возвращать статус `CAM_REQ_INVALID`). Также при завершении запроса все отключенные транзакции для этого целевого устройства должны быть прерваны (вероятно, в процедуре прерывания).

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

### 12.5.3. XPT_RESET_BUS — отправить сигнал RESET на шину SCSI

В CCB не передаются аргументы, единственный интересный аргумент — это шина SCSI, указанная структурой sim.

Минималистичная реализация могла бы пропустить SCSI-переговоры для всех устройств на шине и вернуть статус CAM_REQ_CMP.

Правильная реализация дополнительно должна фактически сбросить шину SCSI (возможно, также сбросить контроллер SCSI) и пометить все обрабатываемые CCB, как находящиеся в аппаратной очереди, так и отключенные, как завершенные со статусом CAM_SCSI_BUS_RESET. Например:

        int targ, lun;
        struct xxx_hcb *h, *hh;
        struct ccb_trans_settings neg;
        struct cam_path *path;

        /* The SCSI bus reset may take a long time, in this case its completion
         * should be checked by interrupt or timeout. But for simplicity
         * we assume here that it is really fast.
         */
        reset_scsi_bus(softc);

        /* drop all enqueued CCBs */
        for (h = softc->first_queued_hcb; h != NULL; h = hh) {
            hh = h->next;
            free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET);
        }

        /* the clean values of negotiations to report */
        neg.bus_width = 8;
        neg.sync_period = neg.sync_offset = 0;
        neg.valid = (CCB_TRANS_BUS_WIDTH_VALID
            | CCB_TRANS_SYNC_RATE_VALID | CCB_TRANS_SYNC_OFFSET_VALID);

        /* drop all disconnected CCBs and clean negotiations  */
        for (targ=0; targ <= OUR_MAX_SUPPORTED_TARGET; targ++) {
            clean_negotiations(softc, targ);

            /* report the event if possible */
            if (xpt_create_path(&path, /*periph*/NULL,
                    cam_sim_path(sim), targ,
                    CAM_LUN_WILDCARD) == CAM_REQ_CMP) {
                xpt_async(AC_TRANSFER_NEG, path, &neg);
                xpt_free_path(path);
            }

            for (lun=0; lun <= OUR_MAX_SUPPORTED_LUN; lun++)
                for (h = softc->first_discon_hcb[targ][lun]; h != NULL; h = hh) {
                    hh=h->next;
                    free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET);
                }
        }

        ccb->ccb_h.status = CAM_REQ_CMP;
        xpt_done(ccb);

        /* report the event */
        xpt_async(AC_BUS_RESET, softc->wpath, NULL);
        return;

Реализация сброса шины SCSI в виде функции может быть хорошей идеей, так как она может быть повторно использована функцией таймаута в качестве последнего средства, если что-то пойдёт не так.

### 12.5.4. XPT_ABORT — прервать указанный CCB

Аргументы передаются в экземпляре "struct ccb_abort cab" объединения ccb. Единственное поле аргумента в нём:

- abort_ccb — указатель на CCB, который необходимо прервать

Если прерывание не поддерживается, просто верните статус CAM_UA_ABORT. Это также простой способ минимальной реализации этого вызова — в любом случае возвращать CAM_UA_ABORT.

Трудный путь — честно реализовать этот запрос. Сначала проверьте, что прерывание применяется к SCSI-транзакции:

        struct ccb *abort_ccb;
        abort_ccb = ccb->cab.abort_ccb;

        if (abort_ccb->ccb_h.func_code != XPT_SCSI_IO) {
            ccb->ccb_h.status = CAM_UA_ABORT;
            xpt_done(ccb);
            return;
        }

Затем необходимо найти этот CCB в нашей очереди. Это можно сделать, пройдясь по списку всех наших блоков управления оборудованием в поисках связанного с этим CCB:

        struct xxx_hcb *hcb, *h;

        hcb = NULL;

        /* We assume that softc->first_hcb is the head of the list of all
         * HCBs associated with this bus, including those enqueued for
         * processing, being processed by hardware and disconnected ones.
         */
        for (h = softc->first_hcb; h != NULL; h = h->next) {
            if (h->ccb == abort_ccb) {
                hcb = h;
                break;
            }
        }

        if (hcb == NULL) {
            /* no such CCB in our queue */
            ccb->ccb_h.status = CAM_PATH_INVALID;
            xpt_done(ccb);
            return;
        }

        hcb=found_hcb;

Теперь мы рассмотрим текущее состояние обработки HCB. Он может находиться в очереди, ожидая отправки на шину SCSI, передаваться в данный момент, быть отключенным и ожидать результата команды, или фактически завершённым с точки зрения аппаратуры, но ещё не отмеченным программным обеспечением, как выполненный. Чтобы избежать состояний гонки с аппаратурой, мы помечаем HCB как прерванный, так что если этот HCB вот-вот будет отправлен на шину SCSI, контроллер SCSI увидит этот флаг и пропустит его.

        int hstatus;

        /* shown as a function, in case special action is needed to make
         * this flag visible to hardware
         */
        set_hcb_flags(hcb, HCB_BEING_ABORTED);

        abort_again:

        hstatus = get_hcb_status(hcb);
        switch (hstatus) {
        case HCB_SITTING_IN_QUEUE:
            remove_hcb_from_hardware_queue(hcb);
            /* FALLTHROUGH */
        case HCB_COMPLETED:
            /* this is an easy case */
            free_hcb_and_ccb_done(hcb, abort_ccb, CAM_REQ_ABORTED);
            break;

Если CCB передаётся в данный момент, мы хотели бы сигнализировать контроллеру SCSI аппаратно-зависимым способом, что хотим прервать текущую передачу. Контроллер SCSI установит сигнал SCSI ATTENTION, и когда целевое устройство ответит на него, отправит сообщение ABORT. Мы также сбрасываем таймаут, чтобы убедиться, что целевое устройство не засыпает навсегда. Если команда не будет прервана в разумное время, например, за 10 секунд, процедура таймаута продолжит работу и сбросит всю шину SCSI. Поскольку команда будет прервана в разумные сроки, мы можем просто вернуть запрос на прерывание как успешно выполненный и пометить прерванный CCB как прерванный (но пока не помечать его как завершённый).

        case HCB_BEING_TRANSFERRED:
            untimeout(xxx_timeout, (caddr_t) hcb, abort_ccb->ccb_h.timeout_ch);
            abort_ccb->ccb_h.timeout_ch =
                timeout(xxx_timeout, (caddr_t) hcb, 10 * hz);
            abort_ccb->ccb_h.status = CAM_REQ_ABORTED;
            /* ask the controller to abort that HCB, then generate
             * an interrupt and stop
             */
            if (signal_hardware_to_abort_hcb_and_stop(hcb) < 0) {
                /* oops, we missed the race with hardware, this transaction
                 * got off the bus before we aborted it, try again */
                goto abort_again;
            }

            break;

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

        case HCB_DISCONNECTED:
            untimeout(xxx_timeout, (caddr_t) hcb, abort_ccb->ccb_h.timeout_ch);
            abort_ccb->ccb_h.timeout_ch =
                timeout(xxx_timeout, (caddr_t) hcb, 10 * hz);
            put_abort_message_into_hcb(hcb);
            put_hcb_at_the_front_of_hardware_queue(hcb);
            break;
        }
        ccb->ccb_h.status = CAM_REQ_CMP;
        xpt_done(ccb);
        return;

Вот и все, что касается запроса ABORT, хотя есть ещё один момент. Поскольку сообщение ABORT очищает все текущие транзакции на LUN, нам необходимо пометить все остальные активные транзакции на этом LUN как прерванные. Это должно быть выполнено в процедуре аппаратного прерывания после того, как транзакция будет прервана.

Реализация прерывания CCB в виде функции может быть довольно хорошей идеей, эта функция может быть повторно использована, если транзакция ввода-вывода превысит время ожидания. Единственное различие будет в том, что для транзакции с истекшим временем ожидания будет возвращён статус CAM_CMD_TIMEOUT. Тогда код в case XPT_ABORT будет небольшим, например:

        case XPT_ABORT:
            struct ccb *abort_ccb;
            abort_ccb = ccb->cab.abort_ccb;

            if (abort_ccb->ccb_h.func_code != XPT_SCSI_IO) {
                ccb->ccb_h.status = CAM_UA_ABORT;
                xpt_done(ccb);
                return;
            }
            if (xxx_abort_ccb(abort_ccb, CAM_REQ_ABORTED) < 0)
                /* no such CCB in our queue */
                ccb->ccb_h.status = CAM_PATH_INVALID;
            else
                ccb->ccb_h.status = CAM_REQ_CMP;
            xpt_done(ccb);
            return;

### 12.5.5. XPT_SET_TRAN_SETTINGS — явно установить значения настроек передачи SCSI

Аргументы передаются в экземпляре "struct ccb_trans_setting cts" объединения ccb:

- valid - битовая маска, показывающая, какие настройки должны быть обновлены:

  - CCB_TRANS_SYNC_RATE_VALID - скорость синхронной передачи

  - CCB_TRANS_SYNC_OFFSET_VALID - синхронное смещение

  - CCB_TRANS_BUS_WIDTH_VALID - ширина шины

  - CCB_TRANS_DISC_VALID - установить разрешение/запрет отключения

  - CCB_TRANS_TQ_VALID - установить разрешение/запрет очередей с тегами

- flags - состоит из двух частей: бинарных аргументов и идентификации подопераций. Бинарные аргументы:

  - CCB_TRANS_DISC_ENB - разрешить отключение

  - CCB_TRANS_TAG_ENB - разрешить тегированную очередь

- подоперации:

  - CCB_TRANS_CURRENT_SETTINGS - изменить текущие параметры согласования

  - CCB_TRANS_USER_SETTINGS - сохранять желаемые пользовательские значения sync_period, sync_offset - самоочевидные параметры; если sync_offset==0, то запрашивается асинхронный режим bus_width - ширина шины в битах (не в байтах)

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

Текущие настройки, как следует из названия, являются текущими. Их изменение означает, что параметры должны быть повторно согласованы при следующей передаче. Опять же, эти «новые текущие настройки» не предназначены для принудительного применения к устройству, они лишь используются в качестве начального шага переговоров. Кроме того, они должны быть ограничены реальными возможностями SCSI-контроллера: например, если SCSI-контроллер имеет 8-битную шину, а запрос требует установки 16-битных передач, этот параметр должен быть тихо усечён до 8-битных передач перед отправкой на устройство.

Один нюанс заключается в том, что ширина шины и синхронные параметры относятся к цели, тогда как параметры отключения и включения тегов относятся к логическому устройству LUN.

Рекомендуемая реализация заключается в хранении 3 наборов согласованных параметров (ширина шины и синхронная передача):

- user - пользовательский набор, как указано выше

- current - тот, который фактически действуют

- goal - тот набор, который запрошен для установки параметров в качестве "текущих"

Код выглядит следующим образом:

        struct ccb_trans_settings *cts;
        int targ, lun;
        int flags;

        cts = &ccb->cts;
        targ = ccb_h->target_id;
        lun = ccb_h->target_lun;
        flags = cts->flags;
        if (flags & CCB_TRANS_USER_SETTINGS) {
            if (flags & CCB_TRANS_SYNC_RATE_VALID)
                softc->user_sync_period[targ] = cts->sync_period;
            if (flags & CCB_TRANS_SYNC_OFFSET_VALID)
                softc->user_sync_offset[targ] = cts->sync_offset;
            if (flags & CCB_TRANS_BUS_WIDTH_VALID)
                softc->user_bus_width[targ] = cts->bus_width;

            if (flags & CCB_TRANS_DISC_VALID) {
                softc->user_tflags[targ][lun] &= ~CCB_TRANS_DISC_ENB;
                softc->user_tflags[targ][lun] |= flags & CCB_TRANS_DISC_ENB;
            }
            if (flags & CCB_TRANS_TQ_VALID) {
                softc->user_tflags[targ][lun] &= ~CCB_TRANS_TQ_ENB;
                softc->user_tflags[targ][lun] |= flags & CCB_TRANS_TQ_ENB;
            }
        }
        if (flags & CCB_TRANS_CURRENT_SETTINGS) {
            if (flags & CCB_TRANS_SYNC_RATE_VALID)
                softc->goal_sync_period[targ] =
                    max(cts->sync_period, OUR_MIN_SUPPORTED_PERIOD);
            if (flags & CCB_TRANS_SYNC_OFFSET_VALID)
                softc->goal_sync_offset[targ] =
                    min(cts->sync_offset, OUR_MAX_SUPPORTED_OFFSET);
            if (flags & CCB_TRANS_BUS_WIDTH_VALID)
                softc->goal_bus_width[targ] = min(cts->bus_width, OUR_BUS_WIDTH);

            if (flags & CCB_TRANS_DISC_VALID) {
                softc->current_tflags[targ][lun] &= ~CCB_TRANS_DISC_ENB;
                softc->current_tflags[targ][lun] |= flags & CCB_TRANS_DISC_ENB;
            }
            if (flags & CCB_TRANS_TQ_VALID) {
                softc->current_tflags[targ][lun] &= ~CCB_TRANS_TQ_ENB;
                softc->current_tflags[targ][lun] |= flags & CCB_TRANS_TQ_ENB;
            }
        }
        ccb->ccb_h.status = CAM_REQ_CMP;
        xpt_done(ccb);
        return;

Затем, когда следующий запрос ввода-вывода будет обработан, он проверит, нужно ли повторное согласование, например, вызовом функции target_negotiated(hcb). Это может быть реализовано следующим образом:

        int
        target_negotiated(struct xxx_hcb *hcb)
        {
            struct softc *softc = hcb->softc;
            int targ = hcb->targ;

            if (softc->current_sync_period[targ] != softc->goal_sync_period[targ]
            || softc->current_sync_offset[targ] != softc->goal_sync_offset[targ]
            || softc->current_bus_width[targ] != softc->goal_bus_width[targ])
                return 0; /* FALSE */
            else
                return 1; /* TRUE */
        }

После пересогласования значений полученные значения должны быть присвоены как текущим, так и целевым параметрам, чтобы для будущих операций ввода-вывода текущие и целевые параметры совпадали, и функция `target_negotiated()` возвращала TRUE. При инициализации карты (в `xxx_attach()`) текущие параметры согласования должны быть инициализированы узким асинхронным режимом, а целевые и текущие значения должны быть инициализированы максимальными значениями, поддерживаемыми контроллером.

### 12.5.6. XPT_GET_TRAN_SETTINGS — получить значения настроек передачи SCSI

Эта операция является обратной XPT_SET_TRAN_SETTINGS. Заполните экземпляр CCB "struct ccb_trans_setting cts" данными, запрошенными флагами CCB_TRANS_CURRENT_SETTINGS или CCB_TRANS_USER_SETTINGS (если установлены оба, существующие драйверы возвращают текущие настройки). Установите все биты в поле valid.

### 12.5.7. XPT_CALC_GEOMETRY — вычислить логическую (BIOS) геометрию диска

Аргументы передаются в экземпляре "struct ccb_calc_geometry ccg" объединения ccb:

- block_size - вход, размер блока (также известный как сектор) в байтах

- volume_size - вход, размер тома в байтах

- cylinders - выход, логические цилиндры

- heads - выход, логические головки

- secs_per_track - выход, логических секторов на дорожку

Если возвращённая геометрия значительно отличается от той, которую предполагает BIOS SCSI-контроллера, и диск на этом SCSI-контроллере используется как загрузочный, система может не загрузиться. Типичный пример расчёта, взятый из драйвера `aic7xxx`, выглядит следующим образом:

        struct    ccb_calc_geometry *ccg;
        u_int32_t size_mb;
        u_int32_t secs_per_cylinder;
        int   extended;

        ccg = &ccb->ccg;
        size_mb = ccg->volume_size
            / ((1024L * 1024L) / ccg->block_size);
        extended = check_cards_EEPROM_for_extended_geometry(softc);

        if (size_mb > 1024 && extended) {
            ccg->heads = 255;
            ccg->secs_per_track = 63;
        } else {
            ccg->heads = 64;
            ccg->secs_per_track = 32;
        }
        secs_per_cylinder = ccg->heads * ccg->secs_per_track;
        ccg->cylinders = ccg->volume_size / secs_per_cylinder;
        ccb->ccb_h.status = CAM_REQ_CMP;
        xpt_done(ccb);
        return;

Это даёт общее представление, точный расчет зависит от особенностей конкретной BIOS. Если BIOS не предоставляет возможности установить флаг "расширенной трансляции" в EEPROM, этот флаг обычно следует считать равным 1. Другие популярные геометрии:

        128 heads, 63 sectors - Symbios controllers
        16 heads, 63 sectors - old controllers

Некоторые системные BIOS и SCSI BIOS конфликтуют друг с другом с переменным успехом. Например, комбинация Symbios 875/895 SCSI и Phoenix BIOS может выдавать геометрию 128/63 после включения питания и 255/63 после жёсткого сброса или мягкой перезагрузки.

### 12.5.8. XPT_PATH_INQ — запрос пути, другими словами, получение свойств драйвера SIM и контроллера SCSI (также известного как HBA - Host Bus Adapter)

Свойства возвращаются в экземпляре "struct ccb_pathinq cpi" объединения ccb:

- `version_num` - номер версии драйвера SIM, в настоящее время все драйверы используют 1

- hba_inquiry - битовая маска функций, поддерживаемых контроллером:

  - PI_MDP_ABLE - поддерживает сообщение MDP (что-то из SCSI3?)

  - PI_WIDE_32 — поддерживает 32-битную широкую SCSI

  - PI_WIDE_16 — поддерживает 16-битную широкую SCSI

  - PI_SDTR_ABLE - может согласовать синхронную скорость передачи

  - PI_LINKED_CDB - поддерживает связанные команды

  - PI_TAG_ABLE - поддерживает помеченные команды

  - PI_SOFT_RST — поддерживает альтернативу мягкого сброса (жёсткий сброс и мягкий сброс являются взаимоисключающими в пределах шины SCSI)

- target_sprt - флаги поддержки целевого режима, 0 если не поддерживается

- hba_misc - различные функции контроллера:

  - PIM_SCANHILO - сканирование шины от высокого ID к низкому ID

  - PIM_NOREMOVE - съемные устройства не включены в сканирование

  - PIM_NOINITIATOR - роль инициатора не поддерживается

  - PIM_NOBUSRESET - пользователь отключил начальный BUS RESET

- hba_eng_cnt - загадочное количество движков HBA, что-то связанное со сжатием, в настоящее время всегда устанавливается в 0

- vuhba_flags - уникальные флаги производителя, в настоящее время не используются

- max_target - максимальный поддерживаемый идентификатор целевого устройства (7 для 8-битной шины, 15 для 16-битной шины, 127 для Fibre Channel)

- max_lun - максимально поддерживаемый идентификатор LUN (7 для старых SCSI-контроллеров, 63 для новых)

- async_flags - битовая маска установленных обработчиков Async, в настоящее время не используется

- hpath_id - наивысший Path ID в подсистеме, в настоящее время не используется

- unit_number - номер контроллера, cam_sim_unit(sim)

- bus_id - номер шины, cam_sim_bus(sim)

- initiator_id - SCSI ID самого контроллера

- base_transfer_speed - номинальная скорость передачи в КБ/с для асинхронных узкополосных передач, равна 3300 для SCSI

- sim_vid - идентификатор производителя драйвера SIM, строка с нулевым окончанием максимальной длины SIM_IDLEN, включая завершающий ноль

- hba_vid - идентификатор производителя SCSI-контроллера, строка с нулевым окончанием максимальной длины HBA_IDLEN, включая завершающий ноль

- dev_name - имя драйвера устройства, строка с нулевым окончанием максимальной длины DEV_IDLEN, включая завершающий ноль, эквивалентно cam_sim_name(sim)

Рекомендуемый способ установки строковых полей — использование strncpy, например:

        strncpy(cpi->dev_name, cam_sim_name(sim), DEV_IDLEN);

После установки значений установите статус в CAM_REQ_CMP и пометьте CCB как завершённый.

## 12.6. Опрос xxx_poll

    static void xxx_poll(struct cam_sim *);

Функция poll используется для имитации прерываний, когда подсистема прерываний не функционирует (например, когда система аварийно завершила работу и создаёт дамп памяти). Подсистема CAM устанавливает соответствующий уровень прерывания перед вызовом процедуры poll. Таким образом, все, что ей нужно сделать, — это вызвать процедуру прерывания (или наоборот, процедура poll может выполнять реальные действия, а процедура прерывания просто вызывает процедуру poll). Зачем тогда нужна отдельная функция? Это связано с различными соглашениями о вызовах. Процедура `xxx_poll` получает указатель на структуру cam_sim в качестве аргумента, в то время как процедура прерывания PCI по общему соглашению получает указатель на структуру `xxx_softc`, а процедура прерывания ISA получает только номер устройства. Таким образом, процедура poll обычно выглядит следующим образом:

    static void
    xxx_poll(struct cam_sim *sim)
    {
        xxx_intr((struct xxx_softc *)cam_sim_softc(sim)); /* for PCI device */
    }

или

    static void
    xxx_poll(struct cam_sim *sim)
    {
        xxx_intr(cam_sim_unit(sim)); /* for ISA device */
    }

## 12.7. Асинхронные события

Если была настроена асинхронная callback-функция для события, то callback-функция должна быть определена.

    static void
    ahc_async(void *callback_arg, u_int32_t code, struct cam_path *path, void *arg)

- callback_arg - значение, переданное при регистрации callback

- code - определяет тип события

- path - определяет устройства, к которым применяется событие

- arg - аргумент, специфичный для события

Реализация для одного типа события, AC_LOST_DEVICE, выглядит следующим образом:

        struct xxx_softc *softc;
        struct cam_sim *sim;
        int targ;
        struct ccb_trans_settings neg;

        sim = (struct cam_sim *)callback_arg;
        softc = (struct xxx_softc *)cam_sim_softc(sim);
        switch (code) {
        case AC_LOST_DEVICE:
            targ = xpt_path_target_id(path);
            if (targ <= OUR_MAX_SUPPORTED_TARGET) {
                clean_negotiations(softc, targ);
                /* send indication to CAM */
                neg.bus_width = 8;
                neg.sync_period = neg.sync_offset = 0;
                neg.valid = (CCB_TRANS_BUS_WIDTH_VALID
                    | CCB_TRANS_SYNC_RATE_VALID | CCB_TRANS_SYNC_OFFSET_VALID);
                xpt_async(AC_TRANSFER_NEG, path, &neg);
            }
            break;
        default:
            break;
        }

## 12.8. Прерывания

Точный тип процедуры прерывания зависит от типа периферийной шины (PCI, ISA и так далее), к которой подключен SCSI-контроллер.

Прерывания в драйверах SIM выполняются на уровне прерывания splcam. Поэтому в драйвере следует использовать `splcam()` для синхронизации между обработчиком прерывания и остальной частью драйвера (для драйверов, учитывающих многопроцессорность, ситуация становится ещё сложнее, но здесь мы этот случай не рассматриваем). Псевдокод в этом документе беззаботно игнорирует проблемы синхронизации. Реальный код так делать не должен. Простейший подход — установить `splcam()` при входе в другие функции и сбросить при выходе, защищая их одной большой критической секцией. Чтобы гарантировать восстановление уровня прерывания, можно определить обёрточную функцию, например:

        static void
        xxx_action(struct cam_sim *sim, union ccb *ccb)
        {
            int s;
            s = splcam();
            xxx_action1(sim, ccb);
            splx(s);
        }

        static void
        xxx_action1(struct cam_sim *sim, union ccb *ccb)
        {
            ... process the request ...
        }

Этот подход прост и надежен, но проблема в том, что прерывания могут блокироваться на относительно долгое время, что негативно скажется на производительности системы. С другой стороны, функции семейства `spl()` имеют довольно высокие накладные расходы, поэтому большое количество мелких критических секций также может быть нежелательным.

Условия, обрабатываемые процедурой прерывания, и детали сильно зависят от оборудования. Мы рассматриваем набор "типичных" условий.

Сначала проверяем, было ли на шине событие SCSI сброса (вероятно, вызванное другим SCSI-контроллером на той же SCSI-шине). Если это так, мы отменяем все поставленные в очередь и отключенные запросы, сообщаем о событиях и повторно инициализируем наш SCSI-контроллер. Важно, чтобы во время этой инициализации контроллер не инициировал ещё один сброс, иначе два контроллера на одной SCSI-шине могут бесконечно обмениваться сбросами. Случай фатальной ошибки/зависания контроллера может быть обработан в том же месте, но, вероятно, также потребуется отправка сигнала RESET на SCSI-шину для сброса состояния соединений с SCSI-устройствами.

        int fatal=0;
        struct ccb_trans_settings neg;
        struct cam_path *path;

        if (detected_scsi_reset(softc)
        || (fatal = detected_fatal_controller_error(softc))) {
            int targ, lun;
            struct xxx_hcb *h, *hh;

            /* drop all enqueued CCBs */
            for(h = softc->first_queued_hcb; h != NULL; h = hh) {
                hh = h->next;
                free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET);
            }

            /* the clean values of negotiations to report */
            neg.bus_width = 8;
            neg.sync_period = neg.sync_offset = 0;
            neg.valid = (CCB_TRANS_BUS_WIDTH_VALID
                | CCB_TRANS_SYNC_RATE_VALID | CCB_TRANS_SYNC_OFFSET_VALID);

            /* drop all disconnected CCBs and clean negotiations  */
            for (targ=0; targ <= OUR_MAX_SUPPORTED_TARGET; targ++) {
                clean_negotiations(softc, targ);

                /* report the event if possible */
                if (xpt_create_path(&path, /*periph*/NULL,
                        cam_sim_path(sim), targ,
                        CAM_LUN_WILDCARD) == CAM_REQ_CMP) {
                    xpt_async(AC_TRANSFER_NEG, path, &neg);
                    xpt_free_path(path);
                }

                for (lun=0; lun <= OUR_MAX_SUPPORTED_LUN; lun++)
                    for (h = softc->first_discon_hcb[targ][lun]; h != NULL; h = hh) {
                        hh=h->next;
                        if (fatal)
                            free_hcb_and_ccb_done(h, h->ccb, CAM_UNREC_HBA_ERROR);
                        else
                            free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET);
                    }
            }

            /* report the event */
            xpt_async(AC_BUS_RESET, softc->wpath, NULL);

            /* re-initialization may take a lot of time, in such case
             * its completion should be signaled by another interrupt or
             * checked on timeout - but for simplicity we assume here that
             * it is really fast
             */
            if (!fatal) {
                reinitialize_controller_without_scsi_reset(softc);
            } else {
                reinitialize_controller_with_scsi_reset(softc);
            }
            schedule_next_hcb(softc);
            return;
        }

Если прерывание не вызвано условием, общим для всего контроллера, то, вероятно, что-то произошло с текущим блоком управления аппаратным обеспечением. В зависимости от оборудования могут быть и другие события, не связанные с HCB, но мы их здесь не рассматриваем. Затем мы анализируем, что произошло с этим HCB:

        struct xxx_hcb *hcb, *h, *hh;
        int hcb_status, scsi_status;
        int ccb_status;
        int targ;
        int lun_to_freeze;

        hcb = get_current_hcb(softc);
        if (hcb == NULL) {
            /* either stray interrupt or something went very wrong
             * or this is something hardware-dependent
             */
            handle as necessary;
            return;
        }

        targ = hcb->target;
        hcb_status = get_status_of_current_hcb(softc);

Сначала мы проверяем, завершился ли HCB, и если да, то проверяем возвращённый статус SCSI.

        if (hcb_status == COMPLETED) {
            scsi_status = get_completion_status(hcb);

Затем проверьте, связан ли этот статус с командой REQUEST SENSE, и если да, обработайте его простым способом.

            if (hcb->flags & DOING_AUTOSENSE) {
                if (scsi_status == GOOD) { /* autosense was successful */
                    hcb->ccb->ccb_h.status |= CAM_AUTOSNS_VALID;
                    free_hcb_and_ccb_done(hcb, hcb->ccb, CAM_SCSI_STATUS_ERROR);
                } else {
            autosense_failed:
                    free_hcb_and_ccb_done(hcb, hcb->ccb, CAM_AUTOSENSE_FAIL);
                }
                schedule_next_hcb(softc);
                return;
            }

Иначе сама команда завершена, уделяйте больше внимания деталям. Если автоопределение не отключено для этого CCB и команда завершилась неудачно с данными состояния, выполните команду REQUEST SENSE для получения этих данных.

            hcb->ccb->csio.scsi_status = scsi_status;
            calculate_residue(hcb);

            if ((hcb->ccb->ccb_h.flags & CAM_DIS_AUTOSENSE)==0
            && (scsi_status == CHECK_CONDITION
                    || scsi_status == COMMAND_TERMINATED)) {
                /* start auto-SENSE */
                hcb->flags |= DOING_AUTOSENSE;
                setup_autosense_command_in_hcb(hcb);
                restart_current_hcb(softc);
                return;
            }
            if (scsi_status == GOOD)
                free_hcb_and_ccb_done(hcb, hcb->ccb, CAM_REQ_CMP);
            else
                free_hcb_and_ccb_done(hcb, hcb->ccb, CAM_SCSI_STATUS_ERROR);
            schedule_next_hcb(softc);
            return;
        }

Типичным примером могут быть события согласования: сообщения согласования, полученные от цели SCSI (в ответ на нашу попытку согласования или по инициативе цели), или если цель не может согласовать (отклоняет наши сообщения согласования или не отвечает на них).

        switch (hcb_status) {
        case TARGET_REJECTED_WIDE_NEG:
            /* revert to 8-bit bus */
            softc->current_bus_width[targ] = softc->goal_bus_width[targ] = 8;
            /* report the event */
            neg.bus_width = 8;
            neg.valid = CCB_TRANS_BUS_WIDTH_VALID;
            xpt_async(AC_TRANSFER_NEG, hcb->ccb.ccb_h.path_id, &neg);
            continue_current_hcb(softc);
            return;
        case TARGET_ANSWERED_WIDE_NEG:
            {
                int wd;

                wd = get_target_bus_width_request(softc);
                if (wd <= softc->goal_bus_width[targ]) {
                    /* answer is acceptable */
                    softc->current_bus_width[targ] =
                    softc->goal_bus_width[targ] = neg.bus_width = wd;

                    /* report the event */
                    neg.valid = CCB_TRANS_BUS_WIDTH_VALID;
                    xpt_async(AC_TRANSFER_NEG, hcb->ccb.ccb_h.path_id, &neg);
                } else {
                    prepare_reject_message(hcb);
                }
            }
            continue_current_hcb(softc);
            return;
        case TARGET_REQUESTED_WIDE_NEG:
            {
                int wd;

                wd = get_target_bus_width_request(softc);
                wd = min (wd, OUR_BUS_WIDTH);
                wd = min (wd, softc->user_bus_width[targ]);

                if (wd != softc->current_bus_width[targ]) {
                    /* the bus width has changed */
                    softc->current_bus_width[targ] =
                    softc->goal_bus_width[targ] = neg.bus_width = wd;

                    /* report the event */
                    neg.valid = CCB_TRANS_BUS_WIDTH_VALID;
                    xpt_async(AC_TRANSFER_NEG, hcb->ccb.ccb_h.path_id, &neg);
                }
                prepare_width_nego_rsponse(hcb, wd);
            }
            continue_current_hcb(softc);
            return;
        }

Затем мы обрабатываем любые ошибки, которые могли произойти во время автоопределения, тем же простым способом, что и раньше. В противном случае мы снова внимательно изучаем детали.

        if (hcb->flags & DOING_AUTOSENSE)
            goto autosense_failed;

        switch (hcb_status) {

Следующее событие, которое мы рассматриваем, — это неожиданное отключение. Оно считается нормальным после сообщения ABORT или BUS DEVICE RESET и аномальным в остальных случаях.

        case UNEXPECTED_DISCONNECT:
            if (requested_abort(hcb)) {
                /* abort affects all commands on that target+LUN, so
                 * mark all disconnected HCBs on that target+LUN as aborted too
                 */
                for (h = softc->first_discon_hcb[hcb->target][hcb->lun];
                        h != NULL; h = hh) {
                    hh=h->next;
                    free_hcb_and_ccb_done(h, h->ccb, CAM_REQ_ABORTED);
                }
                ccb_status = CAM_REQ_ABORTED;
            } else if (requested_bus_device_reset(hcb)) {
                int lun;

                /* reset affects all commands on that target, so
                 * mark all disconnected HCBs on that target+LUN as reset
                 */

                for (lun=0; lun <= OUR_MAX_SUPPORTED_LUN; lun++)
                    for (h = softc->first_discon_hcb[hcb->target][lun];
                            h != NULL; h = hh) {
                        hh=h->next;
                        free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET);
                    }

                /* send event */
                xpt_async(AC_SENT_BDR, hcb->ccb->ccb_h.path_id, NULL);

                /* this was the CAM_RESET_DEV request itself, it is completed */
                ccb_status = CAM_REQ_CMP;
            } else {
                calculate_residue(hcb);
                ccb_status = CAM_UNEXP_BUSFREE;
                /* request the further code to freeze the queue */
                hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN;
                lun_to_freeze = hcb->lun;
            }
            break;

Если цель отказывается принимать теги, мы уведомляем CAM об этом и возвращаем все команды для этого LUN:

        case TAGS_REJECTED:
            /* report the event */
            neg.flags = 0 & ~CCB_TRANS_TAG_ENB;
            neg.valid = CCB_TRANS_TQ_VALID;
            xpt_async(AC_TRANSFER_NEG, hcb->ccb.ccb_h.path_id, &neg);

            ccb_status = CAM_MSG_REJECT_REC;
            /* request the further code to freeze the queue */
            hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN;
            lun_to_freeze = hcb->lun;
            break;

Затем мы проверяем ряд других условий, при этом обработка в основном ограничивается установкой статуса CCB:

        case SELECTION_TIMEOUT:
            ccb_status = CAM_SEL_TIMEOUT;
            /* request the further code to freeze the queue */
            hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN;
            lun_to_freeze = CAM_LUN_WILDCARD;
            break;
        case PARITY_ERROR:
            ccb_status = CAM_UNCOR_PARITY;
            break;
        case DATA_OVERRUN:
        case ODD_WIDE_TRANSFER:
            ccb_status = CAM_DATA_RUN_ERR;
            break;
        default:
            /* all other errors are handled in a generic way */
            ccb_status = CAM_REQ_CMP_ERR;
            /* request the further code to freeze the queue */
            hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN;
            lun_to_freeze = CAM_LUN_WILDCARD;
            break;
        }

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

        if (hcb->ccb->ccb_h.status & CAM_DEV_QFRZN) {
            /* freeze the queue */
            xpt_freeze_devq(ccb->ccb_h.path, /*count*/1);

            /* re-queue all commands for this target/LUN back to CAM */

            for (h = softc->first_queued_hcb; h != NULL; h = hh) {
                hh = h->next;

                if (targ == h->targ
                && (lun_to_freeze == CAM_LUN_WILDCARD || lun_to_freeze == h->lun))
                    free_hcb_and_ccb_done(h, h->ccb, CAM_REQUEUE_REQ);
            }
        }
        free_hcb_and_ccb_done(hcb, hcb->ccb, ccb_status);
        schedule_next_hcb(softc);
        return;

На этом общее описание обработки прерываний завершается, хотя для некоторых контроллеров могут потребоваться дополнительные действия.

## 12.9. Сводка ошибок

При выполнении запроса ввода-вывода может произойти множество ошибок. Причина ошибки может быть указана в статусе CCB с большим количеством деталей. Примеры использования разбросаны по всему документу. Для полноты изложения приведём сводку рекомендуемых действий при типичных ошибках:

- CAM_RESRC_UNAVAIL — некоторый ресурс временно недоступен, и драйвер SIM не может сгенерировать событие, когда он станет доступен. Примером такого ресурса может быть некоторый внутренний аппаратный ресурс контроллера, для которого контроллер не генерирует прерывание при его доступности.

- CAM_UNCOR_PARITY - произошла неисправимая ошибка чётности

- CAM_DATA_RUN_ERR - переполнение данных или неожиданная фаза данных (направление передачи не соответствует указанному в CAM_DIR_MASK) или нечётная длина передачи для широкой передачи

- CAM_SEL_TIMEOUT - произошел таймаут выбора (цель не отвечает)

- CAM_CMD_TIMEOUT - произошло превышение времени ожидания команды (сработала функция таймаута)

- CAM_SCSI_STATUS_ERROR - устройство вернуло ошибку

- CAM_AUTOSENSE_FAIL - устройство вернуло ошибку и команда REQUEST SENSE завершилась неудачно

- CAM_MSG_REJECT_REC - получено сообщение MESSAGE REJECT

- CAM_SCSI_BUS_RESET - получен сброс шины SCSI

- CAM_REQ_CMP_ERR - произошла «невозможная» фаза SCSI или что-то столь же странное, либо это просто общая ошибка, если дополнительная информация недоступна

- CAM_UNEXP_BUSFREE - произошло неожиданное отключение

- CAM_BDR_SENT - Сообщение BUS DEVICE RESET было отправлено целевому устройству

- CAM_UNREC_HBA_ERROR - невосстановимая ошибка адаптера шины хоста

- CAM_REQ_TOO_BIG - запрос слишком велик для данного контроллера

- CAM_REQUEUE_REQ - этот запрос должен быть повторно поставлен в очередь для сохранения порядка транзакций. Обычно это происходит, когда SIM обнаруживает ошибку, которая должна заморозить очередь, и необходимо поместить другие запросы в очереди для цели на уровне SIM обратно в очередь XPT. Типичными случаями таких ошибок являются тайм-ауты выбора, тайм-ауты команд и другие подобные условия. В таких случаях проблемная команда возвращает статус, указывающий на ошибку, а другие команды, которые ещё не были отправлены на шину, повторно ставятся в очередь.

- CAM_LUN_INVALID - идентификатор LUN в запросе не поддерживается контроллером SCSI

- CAM_TID_INVALID - идентификатор целевого устройства в запросе не поддерживается контроллером SCSI

## 12.10. Обработка таймаутов

Когда время ожидания для HCB истекает, этот запрос должен быть прерван, как и в случае с запросом XPT_ABORT. Единственное отличие заключается в том, что возвращаемый статус прерванного запроса должен быть CAM_CMD_TIMEOUT вместо CAM_REQ_ABORTED (вот почему реализацию прерывания лучше сделать в виде функции). Но есть ещё одна возможная проблема: что если сам запрос на прерывание зависнет? В этом случае шина SCSI должна быть сброшена, как и при запросе XPT_RESET_BUS (и идея о реализации этого в виде функции, вызываемой из обоих мест, применима и здесь). Также мы должны сбросить всю шину SCSI, если запрос на сброс устройства завис. В итоге функция обработки таймаута будет выглядеть следующим образом:

    static void
    xxx_timeout(void *arg)
    {
        struct xxx_hcb *hcb = (struct xxx_hcb *)arg;
        struct xxx_softc *softc;
        struct ccb_hdr *ccb_h;

        softc = hcb->softc;
        ccb_h = &hcb->ccb->ccb_h;

        if (hcb->flags & HCB_BEING_ABORTED || ccb_h->func_code == XPT_RESET_DEV) {
            xxx_reset_bus(softc);
        } else {
            xxx_abort_ccb(hcb->ccb, CAM_CMD_TIMEOUT);
        }
    }

Когда мы прерываем запрос, все остальные отключенные запросы к тому же целевому устройству/LUN также прерываются. Возникает вопрос: следует ли возвращать их со статусом CAM_REQ_ABORTED или CAM_CMD_TIMEOUT? Текущие драйверы используют CAM_CMD_TIMEOUT. Это кажется логичным, потому что если один запрос превысил время ожидания, то, вероятно, с устройством происходит что-то действительно плохое, и если их не трогать, они бы сами превысили время ожидания.

Изменено: 17 мая 2026 г. by Vladlen Popolitov

Назад
Главная
Далее