第 12 章. 通用访问方法 SCSI 控制器

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 代表通用访问方法。它是一种以类似 SCSI 的方式寻址 I/O 总线的通用方法。这允许将通用设备驱动程序与控制 I/O 总线的驱动程序分离:例如,磁盘驱动程序能够控制 SCSI、IDE 和/或任何其他总线上的磁盘,因此无需为每个新的 I/O 总线重写(或复制和修改)磁盘驱动程序部分。因此,两个最重要的活动实体是

  • 外围模块 - 外围设备(磁盘、磁带、CD-ROM 等)的驱动程序

  • SCSI 接口模块 (SIM) - 用于连接到 I/O 总线(如 SCSI 或 IDE)的主机总线适配器驱动程序。

外围驱动程序接收来自操作系统的请求,将其转换为一系列 SCSI 命令,并将这些 SCSI 命令传递给 SCSI 接口模块。SCSI 接口模块负责将这些命令传递给实际的硬件(或者如果实际的硬件不是 SCSI 而是例如 IDE,则还将 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 */
    }

如果每个 SCSI 总线有一个 devq 结构(即,我们将具有多个总线的卡视为具有一个总线的多个卡),则总线号将始终为 0,否则 SCSI 卡上的每个总线都应该获取一个不同的编号。每个总线都需要它自己的独立结构 cam_sim。

之后,我们的控制器就完全挂接到CAM系统上了。现在可以丢弃devq的值了:在CAM后续的所有调用中,sim都将作为参数传递,并且可以从中推导出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;

如您所见,路径包含

  • 外设驱动程序的ID(此处为NULL,因为我们没有)

  • SIM驱动程序的ID(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控制块”。它是许多特定实例的联合体,每个实例都描述了某种类型事务的参数。所有这些实例都共享CCB头,其中存储了参数的公共部分。

CAM支持以发起者(“正常”)模式和目标(模拟SCSI设备)模式工作的SCSI控制器。这里我们只考虑与发起者模式相关的部分。

定义了一些函数和宏(换句话说,方法)来访问struct sim中的公共数据。

  • cam_sim_path(sim) - 路径ID(见上文)

  • cam_sim_name(sim) - sim的名称

  • cam_sim_softc(sim) - 指向softc(驱动程序私有数据)结构的指针

  • cam_sim_unit(sim) - 单元编号

  • cam_sim_bus(sim) - 总线ID

为了识别设备,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,并且通过调用xpt_done(ccb)将已完成的CCB返回给CAM。

xpt_done()不必从xxx_action()中调用:例如,I/O请求可能在SIM驱动程序和/或其SCSI控制器内部入队。然后,当设备发布中断信号表明此请求的处理已完成时,可以从中断处理例程中调用xpt_done()

实际上,CCB状态不仅被分配为返回代码,而且CCB始终具有一定的状态。在CCB传递给xxx_action()例程之前,它会获得状态CCB_REQ_INPROG,表示它正在进行中。在/sys/cam/cam.h中定义了令人惊讶数量的状态值,这些值应该能够非常详细地表示请求的状态。更有趣的是,状态实际上是枚举状态值(低6位)和可能的附加标志位(高位)的“按位或”。枚举值将在后面详细讨论。它们的摘要可以在错误摘要部分找到。可能的状​​态标志有

  • CAM_DEV_QFRZN - 如果SIM驱动程序在处理CCB时遇到严重错误(例如,设备对选择没有响应或违反了SCSI协议),则应通过调用xpt_freeze_simq()冻结请求队列,将为此设备入队但尚未处理的其他CCB返回到CAM队列,然后为有问题的CCB设置此标志并调用xpt_done()。此标志会导致CAM子系统在处理错误后解冻队列。

  • CAM_AUTOSNS_VALID - 如果设备返回错误条件且CCB中未设置标志CAM_DIS_AUTOSENSE,则SIM驱动程序必须自动执行REQUEST SENSE命令以从设备中提取sense(扩展错误信息)数据。如果此尝试成功,则应将sense数据保存在CCB中并设置此标志。

  • CAM_RELEASE_SIMQ - 与CAM_DEV_QFRZN类似,但用于SCSI控制器本身存在某些问题(或资源短缺)的情况。然后,应通过xpt_freeze_simq()停止对控制器的所有未来请求。在SIM驱动程序克服短缺并通过返回带有此标志设置的某些CCB通知CAM后,将重新启动控制器队列。

  • 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 - 请求的路径ID

  • target_id - 请求的目标设备ID

  • target_lun - 目标设备的LUN ID

  • 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是一个两倍大的数组。

使用CCB的SIM私有字段的推荐方法是为它们定义一些有意义的名称,并在驱动程序中使用这些有意义的名称,例如

#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 - 执行I/O事务

联合体ccb的实例“struct ccb_scsiio csio”用于传递参数。它们是

  • cdb_io - 指向SCSI命令缓冲区的指针或缓冲区本身

  • cdb_len - SCSI命令长度

  • data_ptr - 指向数据缓冲区的指针(如果使用分散/收集,则会变得有点复杂)

  • dxfer_len - 要传输的数据长度

  • sglist_cnt - 分散/收集段的计数器

  • scsi_status - 返回SCSI状态的位置

  • sense_data - 如果命令返回错误,则用于SCSI sense信息的缓冲区(在这种情况下,如果未设置CCB标志CAM_DIS_AUTOSENSE,则SIM驱动程序应该自动运行REQUEST SENSE命令)

  • sense_len - 该缓冲区的长度(如果碰巧大于sense_data的大小,则SIM驱动程序必须静默地假定较小的值)

  • resid, sense_resid - 如果数据或SCSI sense的传输返回错误,则这些是返回的残余(未传输)数据的计数器。它们似乎没有特别的意义,因此在难以计算它们的情况下(例如,计算SCSI控制器FIFO缓冲区中的字节数),近似值也可以。对于成功完成的传输,必须将其设置为零。

  • 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重新入队。稍后,当资源可用时,必须通过返回一个在状态中设置了CAM_SIMQ_RELEASE位的ccb来解冻SIM队列。否则,如果一切顺利,则将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中或指向它,在后一种情况下,指针可以是物理的或虚拟的。由于硬件通常需要物理地址,因此我们始终将地址转换为物理地址,通常使用busdma API。

如果请求物理地址,则返回状态为CAM_REQ_INVALID的CCB是可以的,当前驱动程序就是这样做的。如有必要,也可以将物理地址转换或映射回虚拟地址,但代价很大,因此我们不这样做。

    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;

然后我们检查数据是存储在一个块中还是散列-收集列表中,以及地址是物理地址还是虚拟地址。SCSI 控制器可能只能处理数量有限、长度有限的块。如果请求达到此限制,我们将返回错误。我们使用一个特殊函数来返回 CCB,以便在一个地方处理 HCB 资源短缺。添加块的函数依赖于驱动程序,这里我们不提供详细的实现。有关地址转换问题的详细信息,请参阅 SCSI 命令 (CDB) 处理的说明。如果某些变体对于特定卡来说难以或无法实现,则返回状态CAM_REQ_INVALID是可以的。实际上,现在 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标志的值传递给它,以防止在 CAM 子系统不希望时自动执行 REQUEST SENSE。

剩下的唯一事情就是设置超时、将我们的 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 “总线设备重置”消息发送到设备

除了标头之外,CCB 中没有传输任何数据,它最有趣的参数是 target_id。根据控制器硬件,可能会构建一个类似于 XPT_SCSI_IO 请求的硬件控制块(参见 XPT_SCSI_IO 请求说明)并将其发送到控制器,或者 SCSI 控制器可能会立即编程以将此 RESET 消息发送到设备,或者此请求可能根本不受支持(并返回状态CAM_REQ_INVALID)。此外,在请求完成后,必须中止此目标的所有断开连接的事务(可能在中断例程中)。

此外,目标的所有当前协商在重置时都会丢失,因此也可能需要清理它们。或者可以延迟清理,因为无论如何目标将在下一个事务中请求重新协商。

12.5.3. XPT_RESET_BUS - 将 RESET 信号发送到 SCSI 总线

CCB 中没有传递任何参数,唯一有趣的参数是 struct sim 指针指示的 SCSI 总线。

一个最小化的实现将忘记总线上的所有设备的 SCSI 协商并返回状态 CAM_REQ_CMP。

正确的实现将此外实际重置 SCSI 总线(也可能重置 SCSI 控制器),并将所有正在处理的 CCB(包括硬件队列中的 CCB 和断开连接的 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

参数在联合 ccb 的实例“struct ccb_abort cab”中传输。其中的唯一参数字段是

  • 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 中止实现为一个函数可能是一个非常好的主意,如果 I/O 事务超时,此函数可以重复使用。唯一的区别是,超时的事务将为超时请求返回状态 CAM_CMD_TIMEOUT。然后 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 传输设置的值

参数在联合 ccb 的实例“struct ccb_trans_setting cts”中传输

  • 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。

建议的实现是保留三组协商的(总线宽度和同步传输)参数

  • user - 使用者设置,如上所述

  • current - 实际生效的参数

  • goal - 通过设置“current”参数请求的参数

代码如下所示

    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;

然后,当处理下一个 I/O 请求时,它将检查是否需要重新协商,例如通过调用函数 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 */
    }

重新协商值后,必须将结果值分配给 current 和 goal 参数,因此对于未来的 I/O 事务,current 和 goal 参数将相同,并且target_negotiated()将返回 TRUE。当卡初始化(在xxx_attach()中)时,必须将当前协商值初始化为窄异步模式,必须将 goal 和 current 值初始化为控制器支持的最大值。

12.5.6. XPT_GET_TRAN_SETTINGS - 获取 SCSI 传输设置的值

此操作是 XPT_SET_TRAN_SETTINGS 的反向操作。使用标志 CCB_TRANS_CURRENT_SETTINGS 或 CCB_TRANS_USER_SETTINGS(如果两者都设置,则现有驱动程序返回当前设置)填充 CCB 实例“struct ccb_trans_setting cts”中的数据。在 valid 字段中设置所有位。

12.5.7. XPT_CALC_GEOMETRY - 计算磁盘的逻辑(BIOS)几何形状

参数在联合 ccb 的实例“struct ccb_calc_geometry ccg”中传输

  • block_size - 输入,块(又名扇区)大小,以字节为单位

  • volume_size - 输入,卷大小,以字节为单位

  • cylinders - 输出,逻辑柱面数

  • heads - 输出,逻辑磁头数

  • secs_per_track - 输出,每磁道的逻辑扇区数

如果返回的几何形状与 SCSI 控制器 BIOS 认为的几何形状有很大差异,并且此 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 - 主机总线适配器)属性

属性在联合 ccb 的实例“struct ccb_pathinq cpi”中返回

  • 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 - 用户已禁用初始总线复位

  • hba_eng_cnt - 神秘的 HBA 引擎计数,与压缩相关,现在始终设置为 0

  • vuhba_flags - 供应商唯一标志,现在未使用

  • max_target - 最大支持的目标 ID(8 位总线为 7,16 位总线为 15,光纤通道为 127)

  • max_lun - 最大支持的 LUN ID(旧 SCSI 控制器的 7,新控制器的 63)

  • async_flags - 已安装的异步处理程序的位掩码,现在未使用

  • hpath_id - 子系统中的最高路径 ID,现在未使用

  • unit_number - 控制器单元编号,cam_sim_unit(sim)

  • bus_id - 总线编号,cam_sim_bus(sim)

  • initiator_id - 控制器本身的 SCSI ID

  • base_transfer_speed - 异步窄传输的名义传输速度(KB/s),对于 SCSI 等于 3300

  • sim_vid - SIM 驱动程序的供应商 ID,一个以零结尾的最大长度为 SIM_IDLEN 的字符串,包括终止零

  • hba_vid - SCSI 控制器的供应商 ID,一个以零结尾的最大长度为 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 *);

当中断子系统无法正常工作时(例如,系统崩溃并正在创建系统转储),轮询功能用于模拟中断。CAM 子系统在调用轮询例程之前设置适当的中断级别。因此,它需要做的就是调用中断例程(或者反过来,轮询例程可能正在执行实际操作,而中断例程只会调用轮询例程)。那么为什么要费心使用一个单独的功能呢?这与不同的调用约定有关。xxx_poll 例程以 struct cam_sim 指针作为其参数,而 PCI 中断例程根据惯例以指向 struct xxx_softc 的指针作为参数,而 ISA 中断例程只接收设备单元号。因此,轮询例程通常如下所示

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. 异步事件

如果已设置异步事件回调,则应定义回调函数。

static void
ahc_async(void *callback_arg, u_int32_t code, struct cam_path *path, void *arg)
  • callback_arg - 注册回调时提供的值

  • 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. 中断

中断例程的确切类型取决于 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() 系列函数的开销相当高,因此大量的小型临界区可能也不好。

中断例程处理的条件和细节在很大程度上取决于硬件。我们考虑一组“典型”条件。

首先,我们检查总线上是否遇到 SCSI 复位(可能是同一 SCSI 总线上另一个 SCSI 控制器引起的)。如果是,我们丢弃所有排队和断开的请求,报告事件并重新初始化我们的 SCSI 控制器。重要的是,在此初始化期间,控制器不会发出另一个复位,否则同一 SCSI 总线上的两个控制器可能会永远来回复位。致命控制器错误/挂起的情况可以在同一位置处理,但可能还需要向 SCSI 总线发送 RESET 信号以重置与 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. 错误汇总

执行 I/O 请求时,许多事情可能会出错。错误的原因可以在 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 COMMAND 失败

  • 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 ID 不受 SCSI 控制器支持

  • CAM_TID_INVALID - 请求中的目标 ID 不受 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。这似乎合乎逻辑,因为如果一个请求超时,那么设备可能真的发生了不好的事情,因此,如果它们不被干扰,它们会自己超时。


上次修改时间:2024 年 3 月 9 日,作者:Danilo G. Baio