12.5 中断

  中断例程的确切类型依赖于SCSI控制器所连接到的外围总线的类型(PCI, ISA等等)。

  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()函数族有相当高的额外开销,因此大量 很小的临界区可能也不好。

  中断例程处理的情况和其中细节严重依赖于硬件。我们考虑 “典型(typical)”情况。

  首先,我们检查总线上是否遇到了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;

        /* 丢弃所有入队的CCB */
        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);
        }

        /* 要报告的协商的干净值 */
        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);

        /* 丢弃所有断开连接的CCB和干净协商 */
        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);
                }
        }

        /* 报告事件 */
        xpt_async(AC_BUS_RESET, softc->wpath, NULL);

        /* 重新初始化可能花很多时间,这种情况下应当由另一中断发信号
         * 指示初始化否完成,或在超时时检查 - 但为了简单我们假设
         * 初始化真的很快
         */
        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) {
        /* 或者丢失(stray)的中断,或者某些东西严重错误,
         * 或者这是硬件相关的某些东西
         */
        进行必要的处理;
        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成功 */
                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 没有禁用auto-sense并且命令连同sense数据失败,则运行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) ) {
            /* 启动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:
        /* 恢复到8-bit总线 */
        softc->current_bus_width[targ] = softc->goal_bus_width[targ] = 8;
        /* 报告事件 */
        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]) {
                /* 可接受的回答 */
                softc->current_bus_width[targ] =
                softc->goal_bus_width[targ] = neg.bus_width = wd;

                /* 报告事件 */
                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]) {
                /* 总线宽度改变了 */
                softc->current_bus_width[targ] =
                softc->goal_bus_width[targ] = neg.bus_width = wd;

                /* 报告事件 */
                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;
    }

  然后我们用与前面相同的笨办法处理auto-sense期间可能出现的任何 错误。否则,我们再一次进入细节。

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

    switch(hcb_status) {

  我们考虑的下一事件是未预期的连接断开,这个事件在ABORT或 BUS DEVICE RESET消息之后被看作是正常的,其他情况下是非正常的。

    case UNEXPECTED_DISCONNECT:
        if(requested_abort(hcb)) {
            /* 中止影响目标和LUN上的所有命令,因此将那个目标和LUN上的
             * 所有断开连接的HCB也标记为中止
             */
            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;

            /* 复位影响那个目标上的所有命令,因此将那个目标和LUN上的
             * 所有断开连接的HCB标记为复位
             */

            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);
                }

            /* 发送事件 */
            xpt_async(AC_SENT_BDR, hcb->ccb->ccb_h.path_id, NULL);

            /* 这是CAM_RESET_DEV请求本身,它完成了 */
            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:
        /* 报告事件 */
        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;
        /* 请求后面的代码冻结队列 */
        hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN;
        lun_to_freeze = hcb->lun;
        break;

  然后我们检查一些其他情况,处理(processing)基本上仅限于设置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:
        /*以通用方法处理所有其他错误 */
        ccb_status = CAM_REQ_CMP_ERR;
        /* 请求后面的代码冻结队列 */
        hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN;
        lun_to_freeze = CAM_LUN_WILDCARD;
        break;
    }

  然后我们检查是否错误严重到需要冻结输入队列,直到它得到处理方可 解冻,如果是这样那么就这样来处理:

    if(hcb->ccb->ccb_h.status & CAM_DEV_QFRZN) {
        /* 冻结队列 */
        xpt_freeze_devq(ccb->ccb_h.path, /*count*/1);

*        /* 重新入队这个目标/LUN的所有命令,将它们返回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;

  这包括通用中断处理,尽管特定处理器可能需要某些附加处理。

本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.

如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系<questions@FreeBSD.org>.
关于本文档的问题请发信联系 <doc@FreeBSD.org>.