12.2 通用基础结构

  CAM代表通用访问方法(Common Access Method)。它以类SCSI方式寻址 I/O总线。这就允许将通用设备驱动程序和控制I/O总线的驱动程序分离开来: 例如磁盘驱动程序能同时控制SCSI、IDE、且/或任何其他总线上的磁盘, 这样磁盘驱动程序部分不必为每种新的I/O总线而重写(或拷贝修改)。 这样,两个最重要的活动实体是:

  外围设备驱动程序从OS接收请求,将它们转换为SCSI命令序列并将 这些SCSI命令传递到SCSI接口模块。SCSI接口模块负责将这些命令传递给 实际硬件(或者如果实际硬件不是SCSI,而是例如IDE,则也要将这些SCSI 命令转换为硬件的native命令)。

  由于这儿我们感兴趣的是编写SCSI适配器驱动程序,从此处开始我们 将从SIM的角度考虑所有的事情。

  典型的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>

  每个SIM驱动程序必须做的第一件事情是向CAM子系统注册它自己。 这在驱动程序的xxx_attach()函数(此处和以后的 xxx_用于指带唯一的驱动程序名字前缀)期间完成。 xxx_attach()函数自身由系统总线自动配置代码 调用,我们在此不描述这部分代码。

  这需要好几步来完成:首先需要分配与SIM关联的请求队列:

    struct cam_devq *devq;

    if(( devq = cam_simq_alloc(SIZE) )==NULL) {
        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, max_dev_transactions, 
            max_tagged_dev_transactions, devq) )==NULL) {
        cam_simq_free(devq);
        error; /* 一些错误处理代码 */
    }

  注意如果我们不能创建SIM描述符,我们也释放 devq,因为我们对其无法做任何其他事情, 而且我们想节约内存。

  如果SCSI卡上有多条SCSI总线,则每条总线需要它自己的 cam_sim 结构。

  一个有趣的问题是,如果SCSI卡有不只一条SCSI总线我们该怎么做, 每个卡需要一个devq结构还是每条SCSI总线? 在CAM代码的注释中给出的答案是:任一方式均可,由驱动程序的作者 选择。

  参量为:



  最后我们注册与我们的SCSI适配器关联的SCSI总线。

    if(xpt_bus_register(sim, bus_number) != CAM_SUCCESS) {
        cam_sim_free(sim, /*free_devq*/ TRUE);
        error; /* 一些错误处理代码 */
    }

  如果每条SCSI总线有一个devq结构(即, 我们将带有多条总线的卡看作多个卡,每个卡带有一条总线),则总线号 总是为0,否则SCSI卡上的每条总线应当有不同的号。每条总线需要 它自己单独的cam_sim结构。

  这之后我们的控制器完全挂接到CAM系统。现在 devq的值可以被丢弃:在所有以后从CAM发出的 调用中将以sim为参量,devq可以由它导出。

  CAM为这些异步事件提供了框架。有些事件来自底层(SIM驱动程序), 有些来自外围设备驱动程序,还有一些来自CAM子系统本身。任何驱动 程序都可以为某些类型的异步事件注册回调,这样那些事件发生时它就 会被通知。

  这种事件的一个典型例子就是设备复位。每个事务和事件以 “path”的方式区分它们所作用的设备。目标特定的事件 通常在与设备进行事务处理期间发生。因此那个事务的路径可以被重用 来报告此事件(这是安全的,因为事件路径的拷贝是在事件报告例程中进行的, 而且既不会被deallocate也不作进一步传递)。在任何时刻,包括中断例程中, 动态分配路径也是安全的,尽管那样会导致某些额外开销,并且这种方法 可能存在的一个问题是碰巧那时可能没有空闲内存。对于总线复位事件, 我们需要定义包括总线上所有设备在内的通配符路径。这样我们就能提前为 以后的总线复位事件创建路径,避免以后内存不足的问题:

    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; /* 一些错误处理代码 */
    }

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

  正如你所看到的,路径包括:

  如果驱动程序不能分配这个路径,它将不能正常工作,因此那样情况下 我们卸除(dismantle)那个SCSI总线。

  我们在softc结构中保存路径指针以便以后 使用。这之后我们保存sim的值(或者如果我们愿意,也可以在从 xxx_probe()退出时丢弃它)。

  这就是最低要求的初始化所需要做的一切。为了把事情做正确无误, 还剩下一个问题。

  对于SIM驱动程序,有一个特殊感兴趣的事件:何时目标设备被认为 找不到了。这种情况下复位与这个设备的SCSI协商可能是个好主意。因此我们 为这个事件向CAM注册一个回调。通过为这种类型的请求来请求CAM控制块上 的CAM动作,请求就被传递到CAM:(译注:参看下面示例代码和原文)

    struct ccb_setasync csa;

    xpt_setup_ccb(&csa.ccb_h, path, /*优先级*/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);

  现在我们看一下xxx_action()xxx_poll()的驱动程序入口点。

  

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



  响应CAM子系统的请求采取某些动作。Sim描述了请求的SIM,CCB为 请求本身。CCB代表“CAM Control Block”。它是很多特定 实例的联合,每个实例为某些类型的事务描述参量。所有这些实例共享 存储着参量公共部分的CCB头部。(译注:这一段不很准确,请自行参考原文)

  CAM既支持SCSI控制器工作于发起者(initiator)(“normal”) 模式,也支持SCSI控制器工作于目标(target)(模拟SCSI设备)模式。这儿 我们只考虑与发起者模式有关的部分。

  定义了几个函数和宏(换句话说,方法)来访问结构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;
    }

  从default case语句部分可以看出(如果收到未知命令),命令的返回码 被设置到 ccb->ccb_h.status 中,并且通过 调用xpt_done(ccb)将整个CCB返回到CAM中。

  xpt_done()不必从 xxx_action()中调用:例如I/O请求可以在SIM驱动程序 和/或它的SCSI控制器中排队。(译注:它指I/O请求?) 然后,当设备传递(post)一个中断信号,指示对此请求的处理已结束时, xpt_done()可以从中断处理例程中被调用。

  实际上,CCB状态不是仅仅被赋值为一个返回码,而是始终有某种状态。 CCB被传递给xxx_action()例程前,其取得状态 CCB_REQ_INPROG,表示其正在进行中。/sys/cam/cam.h 中定义了数量惊人的状态值,它们应该能非常详尽地表示请求的状态。 更有趣的是,状态实际上是一个枚举状态值(低6位)和一些可能出现的附加 类(似)旗标位(高位)的“位或(bitwise or)”。枚举值会在以后 更详细地讨论。对它们的汇总可以在错误概览节(Errors Summary section) 找到。可能的状态旗标为:

  函数xxx_action()不允许睡眠,因此对资源 访问的所有同步必须通过冻结SIM或设备队列来完成。除了前述的旗标外, CAM子系统提供了函数xpt_release_simq()xpt_release_devq()来直接解冻队列,而不必将 CCB传递到CAM。

  CCB头部包含如下字段:

  使用CCB的SIM私有字段的建议方法是为它们定义一些有意义的名字, 并且在驱动程序中使用这些有意义的名字,就像下面这样:

#define ccb_some_meaningful_name    sim_priv.entries[0].bytes
#define ccb_hcb spriv_ptr1 /* 用于硬件控制块 */

  最常见的发起者模式的请求是:

_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语句就会很小,像下面这样:

    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;
  • XPT_SET_TRAN_SETTINGS - 显式设置SCSI传输设置的值

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

    支持两组协商参数,用户设置和当前设置。用户设置在SIM驱动程序中 实际上用得不多,这通常只是一片内存,供上层存储(并在以后恢复)其关于 参数的一些主张。设置用户参数并不会导致重新协商传输速率。但当SCSI 控制器协商时,它必须永远不能设置高于用户参数的值,因此它实质上是 上限。

    当前设置,正如其名字所示,指当前的。改变它们意味着下一次传输时 必须重新协商参数。又一次,这些“new current settings” 并没有被假定为强制用于设备上,它们只是用作协商的起始步骤。此外, 它们必须受SCSI控制器的实际能力限制:例如,如果SCSI控制器有8位总线, 而请求要求设置16位传输,则在发送给设备前参数必须被悄悄地截取为8位。

    一个需要注意的问题就是总线宽度和同步两个参数是针对每目标的而言的, 而断开连接和启用标签两个参数是针对每lun而言的。

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

    代码看起来像:

        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 */
        }
    

    重新协商这些值后,结果值必须同时赋给当前和目的(goal)参数, 这样对于以后的I/O事务当前和目的参数将相同,且 target_negotiated()会返回TRUE。当初始化卡 (在xxx_attach()中)当前协商值必须被初始化为 最窄同步模式,目的和当前值必须被初始化为控制器所支持的最大值。 (译注:原文可能有误,此处未改)

  • XPT_GET_TRAN_SETTINGS - 获得SCSI传输设置的值

    此操作为XPT_SET_TRAN_SETTINGS的逆操作。用通过旗标 CCB_TRANS_CURRENT_SETTINGS或CCB_TRANS_USER_SETTINGS(如果同时设置则 现有驱动程序返回当前设置)所请求而得的数据填充CCB实例 “struct ccb_trans_setting cts”.

  • XPT_CALC_GEOMETRY - 计算磁盘的逻辑(BIOS)结构(geometry)

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

    如果返回的结构与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的癖好(quirk)。如果 BIOS没有提供方法设置EEPROM中的“extended translation” 旗标,则此旗标通常应当假定等于1。其他流行结构有:

        128 heads, 63 sectors - Symbios控制器
        16 heads, 63 sectors - 老式控制器
    

    一些系统BIOS和SCSI BIOS会相互竞争,胜负不定,例如Symbios 875/895 SCSI和Phoenix BIOS的结合在系统加电时会给出结构128/63, 而当冷启动或软启动后会是255/63。

  • XPT_PATH_INQ - 路径问询, 换句话说,获得SIM驱动程序和SCSI控制器(也称为HBA - 主机总线适配器) 的特性。

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

    设置字符串字段的建议方法是使用strncpy,如:

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

    设置这些值后将状态设置为CAM_REQ_CMP,并将CCB标记为完成。

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

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