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>.