编写设备驱动程序

第 2 部分 设计特定种类的设备驱动程序

本书的第二部分提供特定于驱动程序类型的设计信息:

第 15 章 字符设备驱动程序

字符设备没有可物理寻址的存储介质(如磁带机或串行端口),在这些存储介质中 I/O 通常是以字节流的形式执行的。本章介绍字符设备驱动程序的结构,其中重点介绍字符驱动程序的入口点。此外,本章还介绍在同步和异步 I/O 传输上下文中 physio(9F)aphysio(9F) 的用法。

本章介绍有关以下主题的信息:

字符驱动程序结构概述

图 15–1 显示了用来定义字符设备驱动程序结构的数据结构和例程。设备驱动程序通常包括以下元素:

下图中涂有阴影的设备访问部分列出了字符驱动程序入口点。

图 15–1 字符驱动程序结构示意图

图显示了字符设备驱动程序的结构和入口点。

对于每个设备驱动程序,都有一个 dev_ops(9S) 结构与之关联,该结构进而又指向 cb_ops(9S) 结构。这些结构包含指向驱动程序入口点的指针:


注 –

可以根据需要将其中一些入口点替换为 nodev(9F)nulldev(9F)


字符设备自动配置

attach(9E) 例程应执行所有设备需要的常见初始化任务,例如:

有关这些任务的代码示例,请参见attach() 入口点

字符设备驱动程序将创建类型为 S_IFCHR 的次要节点。类型为 S_IFCHR 的次要节点会使代表节点的字符特殊文件最终出现在 /devices 分层结构中。

以下示例显示了字符驱动程序的典型 attach(9E) 例程。与设备有关的属性通常在 attach() 例程中声明。该示例使用了预定义的 Size 属性。在获取块设备的分区大小时,SizeNblocks 属性是等效的。举例来说,如果要在磁盘设备上执行字符 I/O,就可以使用 Size 来获取分区大小。因为 Size 是 64 位属性,所以必须使用 64 位属性接口。在本例中,使用 ddi_prop_update_int64(9F)。有关属性的更多信息,请参见设备属性


示例 15–1 字符驱动程序 attach() 例程

static int
xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
  int instance = ddi_get_instance(dip);
  switch (cmd) {
  case DDI_ATTACH:
      /* 
       * Allocate a state structure and initialize it.
       * Map the device's registers.
       * Add the device driver's interrupt handler(s).
       * Initialize any mutexes and condition variables.
       * Create power manageable components.
       *
       * Create the device's minor node. Note that the node_type
       * argument is set to DDI_NT_TAPE.
       */
       if (ddi_create_minor_node(dip, minor_name, S_IFCHR,
           instance, DDI_NT_TAPE, 0) == DDI_FAILURE) {
           /* Free resources allocated so far. */
           /* Remove any previously allocated minor nodes. */
           ddi_remove_minor_node(dip, NULL);
           return (DDI_FAILURE);
       }
      /*
       * Create driver properties like "Size." Use "Size" 
       * instead of "size" to ensure the property works 
       * for large bytecounts.
       */
       xsp->Size = size_of_device_in_bytes;
       maj_number = ddi_driver_major(dip);
       if (ddi_prop_update_int64(makedevice(maj_number, instance), 
           dip, "Size", xsp->Size) != DDI_PROP_SUCCESS) {
           cmn_err(CE_CONT, "%s: cannot create Size property\n",
               ddi_get_name(dip));
               /* Free resources allocated so far. */
           return (DDI_FAILURE);
       }
      /* ... */
      return (DDI_SUCCESS);    
case DDI_RESUME:
      /* See the "Power Management" chapter in this book. */
default:
      return (DDI_FAILURE);
  }
}

设备访问(字符驱动程序)

可通过 open(9E)close(9E) 入口点来控制一个或多个应用程序对设备的访问。对代表字符设备的特殊文件的 open(2) 系统调用总会导致对驱动程序 open(9E) 例程的调用。对于特定的从设备,open(9E) 可以多次被调用,而 close(9E) 例程只有在删除了对设备的最终引用时才会调用。如果通过文件描述符访问设备,则 close(2)exit(2) 系统调用都可能导致对 close(9E) 的最终调用。如果通过内存映射访问设备,则 munmap(2) 系统调用可能导致对 close(9E) 的最终调用。

open() 入口点(字符驱动程序)

open() 的主要功能是检验是否允许打开请求。open(9E) 的语法如下所示:

int xxopen(dev_t *devp, int flag, int otyp, cred_t *credp);

其中:

devp

指向设备编号的指针。会向 open() 例程传递指针,以便驱动程序可以更改次要设备号。使用此指针,驱动程序能够动态创建设备的次要实例。伪终端驱动程序就是这样,只要打开该驱动程序,就会创建新的伪终端。通常,动态选择次要设备号的驱动程序使用 ddi_create_minor_node(9F)attach(9E) 中仅创建一个从设备节点,然后使用 makedevice(9F)getmajor(9F) 更改 *devp 的次要设备号部分:

    *devp = makedevice(getmajor(*devp), new_minor);

您不必调用 ddi_create_minor_node(9F) 来创建新的次要节点。驱动程序不得更改 *devp 的主设备号。驱动程序必须在内部跟踪可用的次要设备号。

flag

使用位指示打开设备是供读取 (FREAD)、写入 (FWRITE) 还是供可同时读写的标志。发出 open(2) 系统调用的用户线程也可以请求对设备进行独占访问 (FEXCL),或指定不得以任何原因阻止打开操作 (FNDELAY),但驱动程序必须强制执行两者。只写设备(例如打印机)的驱动程序可能会将 open(9E) 视为对读操作无效。

otyp

表示如何调用 open() 的整数。驱动程序必须检查 otyp 的值是否适用于相应设备。对于字符驱动程序,otyp 应为 OTYP_CHR(请参见 open(9E) 手册页)。

credp

指向包含有关调用方信息(例如用户 ID 和组 ID)的凭证结构的指针。驱动程序不会直接检查此结构,但会使用 drv_priv(9F) 来检查超级用户权限的一般情况。在本示例中,只允许 root 或具有 PRIV_SYS_DEVICES 权限的用户打开设备进行写入。

以下示例显示了字符驱动程序的 open(9E) 例程。


示例 15–2 字符驱动程序 open(9E) 例程

static int
xxopen(dev_t *devp, int flag, int otyp, cred_t *credp)
{
    minor_t        instance;

    if (getminor(*devp)         /* if device pointer is invalid */
        return (EINVAL);
    instance = getminor(*devp); /* one-to-one example mapping */
    /* Is the instance attached? */
    if (ddi_get_soft_state(statep, instance) == NULL)
        return (ENXIO);
    /* verify that otyp is appropriate */
    if (otyp != OTYP_CHR)
        return (EINVAL);
    if ((flag & FWRITE) && drv_priv(credp) == EPERM)
        return (EPERM);
    return (0);
}

close() 入口点(字符驱动程序)

close(9E) 的语法如下所示:

int xxclose(dev_t dev, int flag, int otyp, cred_t *credp);

close() 应执行任何必要的清除操作,以完成次要设备的使用,并准备好设备(以及驱动程序)以便再次被打开。例如,可能已使用独占访问 (FEXCL) 标志调用了打开例程。对 close(9E) 的调用允许其他打开例程继续运行。close(9E) 可以执行的其他功能包括:

如果因为外部条件(例如流量控制)而造成 I/O 执行延迟,则等待 I/O 执行完毕的驱动程序会一直等待下去。有关如何避免此问题的信息,请参见线程无法接收信号

I/O 请求处理

本节将详细讨论 I/O 请求处理。

用户地址

当用户线程发出 write(2) 系统调用时,该线程会传递用户空间中某个缓冲区的地址:

    char buffer[] = "python";
    count = write(fd, buffer, strlen(buffer) + 1);

系统通过分配 iovec(9S) 结构,并将 iov_base 字段设置为传递给 write(2) 的地址(在本例中为 buffer),来生成 uio(9S) 结构以描述此传输。uio(9S) 结构将被传递到驱动程序 write(9E) 例程。有关 uio(9S) 结构的详细信息,请参见向量化的 I/O

iovec(9S) 中的地址位于用户空间而非内核空间。因此,既不能保证该地址当前处于内存中,也不能保证该地址是有效地址。无论哪一种情况,从设备驱动程序或从内核访问用户地址都会导致系统崩溃。因此,设备驱动程序永远不应该直接访问用户地址,而是应使用 Solaris DDI/DKI 中的数据传输例程,向内核中传送数据或从内核中读取数据。这些例程能够处理页面错误。DDI/DKI 例程可以读取适当的用户页面,以便继续进行透明复制。这些例程也可以在发生无效访问时返回错误。

使用 copyout(9F) 可将数据从内核空间复制到用户空间。copyin(9F) 可将数据从用户空间复制到内核空间。ddi_copyout(9F)ddi_copyin(9F) 的运行方式与它们类似,但要在 ioctl(9E) 例程中使用。可以对每个 iovec(9S) 结构描述的缓冲区使用 copyin(9F)copyout(9F),或者 uiomove(9F) 可以对驱动程序或设备内存的连续区域执行完整的数据传入或传出操作。

向量化的 I/O

在字符驱动程序中,传输由 uio(9S) 结构进行描述。uio(9S) 结构包含有关传输方向和传输大小以及传输的其中一端缓冲区数组的信息。另一端就是设备。

uio(9S) 结构包含以下成员:

iovec_t       *uio_iov;       /* base address of the iovec */
                              /* buffer description array */
int           uio_iovcnt;     /* the number of iovec structures */
off_t         uio_offset;     /* 32-bit offset into file where */
                              /* data is transferred from or to */
offset_t      uio_loffset;    /* 64-bit offset into file where */
                              /* data is transferred from or to */
uio_seg_t     uio_segflg;     /* identifies the type of I/O transfer */
                              /* UIO_SYSSPACE:  kernel <-> kernel */
                              /* UIO_USERSPACE: kernel <-> user */
short         uio_fmode;      /* file mode flags (not driver setTable) */
daddr_t       uio_limit;      /* 32-bit ulimit for file (maximum */
                              /* block offset). not driver settable. */
diskaddr_t    uio_llimit;     /* 64-bit ulimit for file (maximum block */
                              /* block offset). not driver settable. */
int           uio_resid;      /* amount (in bytes) not */
                              /* transferred on completion */

uio(9S) 结构将被传递到驱动程序 read(9E)write(9E) 入口点。之所以广泛应用此结构,是为了支持称作集中写入分散读取的操作。向设备写入数据时,待写入的数据缓冲区在应用程序内存中不必是连续的。同样,从设备传输到内存的数据虽然不属于连续流,但也可以写入应用程序内存的非连续区域。有关分散/集中式 I/O 的更多信息,请参见 readv(2)writev(2)pread(2)pwrite(2) 手册页。

每个缓冲区都由一个 iovec(9S) 结构描述。该结构包含指向数据区域的指针以及待传输的字节数。

caddr_t    iov_base;    /* address of buffer */
int        iov_len;     /* amount to transfer */

uio 结构包含指向 iovec(9S) 结构数组的指针。此数组的基本地址保存在 uio_iov 中,元素数目保存在 uio_iovcnt 中。

uio_offset 字段包含设备的 32 位偏移位址,应用程序需要在此处开始传输。uio_loffset 用于 64 位文件偏移。如果设备不支持偏移的概念,则可以安全地忽略这些字段。驱动程序会解释 uio_offsetuio_loffset,但不会同时解释两者。如果驱动程序在 cb_ops(9S) 结构中设置了 D_64BIT 标志,则该驱动程序应使用 uio_loffset

uio_resid 字段起初是待传输的字节数,即 uio_iov 中所有 iov_len 字段的总和。此字段在返回之前必须由驱动程序设置为传输的字节数。read(2)write(2) 系统调用使用来自 read(9E)write(9E) 入口点的返回值,来确定失败的传输。如果出现故障,这些例程将返回 -1。如果返回值指示成功,系统调用将返回所请求的字节数减去 uio_resid。如果驱动程序没有更改 uio_resid,则 read(2)write(2) 调用将返回 0。返回值 0 表明文件结束,即使已经传输了所有数据也是如此。

支持例程 uiomove(9F)physio(9F)aphysio(9F) 直接更新 uio(9S) 结构。这些支持例程更新设备偏移以用于数据传输。如果驱动程序用于使用位置概念的可查找设备,则 uio_offsetuio_loffset 字段都不需要调整。以此方式对设备执行的 I/O 操作受 uio_offsetuio_loffset 的最大可能值约束。对磁盘的原始 I/O 操作即是此用法的一个示例。

如果设备没有位置概念,则驱动程序会采取下列步骤:

  1. 保存 uio_offsetuio_loffset

  2. 执行 I/O 操作。

  3. uio_offsetuio_loffset 恢复为字段的初始值。

以此方式对设备执行的 I/O 操作不受 uio_offsetuio_loffset 的最大可能值约束。此种用法的一个示例是串行线路上的 I/O 操作。

以下示例说明了在 read(9E) 函数中保留 uio_loffset 的一种方法。

static int
xxread(dev_t dev, struct uio *uio_p, cred_t *cred_p)
{
    offset_t off;
    /* ... */
    off = uio_p->uio_loffset;  /* save the offset */
    /* do the transfer */
    uio_p->uio_loffset = off;  /* restore it */
}

同步 I/O 与异步 I/O 之间的差别

数据传输可以是同步的,也可以是异步的。决定因素取决于调度传输的入口点是立即返回还是等到 I/O 操作完成之后。

read(9E)write(9E) 入口点都是同步入口点。传输在 I/O 操作完成之前不得返回。待例程返回值时,进程就会知道传输是否成功。

aread(9E)awrite(9E) 入口点都是异步入口点。异步入口点调度 I/O 并立即返回。返回时,发出请求的进程即知道 I/O 被调度,并且随后必须确定 I/O 的状态。同时,该进程还可以执行其他操作。

对于发送到内核的异步 I/O 请求,不要求进程在 I/O 处理过程中等待。一个进程可以执行多个 I/O 请求,并允许内核处理数据传输细节。通过异步 I/O 请求,事务处理等应用程序可以使用并发编程方法来提高性能或缩短响应时间。但是,因使用异步 I/O 的应用程序而改善的任何性能,必须以增加编程复杂性为代价。

数据传输方法

可以使用程控 I/O 或 DMA 传输数据。同步或异步入口点都可以使用这些数据传输方法,具体视设备的功能而定。

程控 I/O 传输

程控 I/O 设备依赖 CPU 来执行数据传输。程控 I/O 数据传输与设备寄存器的其他读写操作相同。可使用各种数据访问例程,从设备内存读取值或向设备内存中存储值。

可以使用 uiomove(9F) 将数据传输到一些程控 I/O 设备。uiomove(9F)uio(9S) 结构所定义的用户空间与内核之间传输数据。uiomove() 可以处理缺页,因此不必锁定要向其传输数据的内存。uiomove() 还会更新 uio(9S) 结构中的 uio_resid 字段。以下示例说明了编写 ramdisk read(9E) 例程的一种方法。它使用同步 I/O,并依赖 ramdisk 状态结构中下列字段的存在:

caddr_t    ram;        /* base address of ramdisk */
int        ramsize;    /* size of the ramdisk */

示例 15–3 使用 uiomove(9F) 的 ramdisk read(9E) 例程

static int
rd_read(dev_t dev, struct uio *uiop, cred_t *credp)
{
     rd_devstate_t     *rsp;

     rsp = ddi_get_soft_state(rd_statep, getminor(dev));
     if (rsp == NULL)
       return (ENXIO);
     if (uiop->uio_offset >= rsp->ramsize)
       return (EINVAL);
     /*
      * uiomove takes the offset into the kernel buffer,
      * the data transfer count (minimum of the requested and
      * the remaining data), the UIO_READ flag, and a pointer
      * to the uio structure.
      */
     return (uiomove(rsp->ram + uiop->uio_offset,
         min(uiop->uio_resid, rsp->ramsize - uiop->uio_offset),
         UIO_READ, uiop));
}

另一个程控 I/O 示例是每次直接向设备内存中写入一字节数据的驱动程序。每一字节都是使用 uwritec(9F)uio(9S) 结构中检索到的。随后该字节被发送到设备中。read(9E) 可以使用 ureadc(9F) 将字节从设备传输到由 uio(9S) 结构描述的区域中。


示例 15–4 使用 uwritec(9F) 的程控 I/O write(9E) 例程

static int
xxwrite(dev_t dev, struct uio *uiop, cred_t *credp)
{
    int    value;
    struct xxstate     *xsp;

    xsp = ddi_get_soft_state(statep, getminor(dev));
    if (xsp == NULL)
        return (ENXIO);
    /* if the device implements a power manageable component, do this: */
    pm_busy_component(xsp->dip, 0);
    if (xsp->pm_suspended)
        pm_raise_power(xsp->dip, normal power);

    while (uiop->uio_resid > 0) {
        /*
         * do the programmed I/O access
         */
        value = uwritec(uiop);
        if (value == -1)
               return (EFAULT);
        ddi_put8(xsp->data_access_handle, &xsp->regp->data,
            (uint8_t)value);
        ddi_put8(xsp->data_access_handle, &xsp->regp->csr,
            START_TRANSFER);
        /*
         * this device requires a ten microsecond delay
         * between writes
         */
        drv_usecwait(10);
    }
    pm_idle_component(xsp->dip, 0);
    return (0);
}

DMA 传输(同步)

字符驱动程序通常在 read(9E)write(9E) 中使用 physio(9F) 来设置 DMA 传输,如示例 15–5 中所示。

int physio(int (*strat)(struct buf *), struct buf *bp,
     dev_t dev, int rw, void (*mincnt)(struct buf *),
     struct uio *uio);

physio(9F) 要求驱动程序提供 strategy(9E) 例程的地址。physio(9F) 可确保内存空间处于锁定状态,即在数据传输期间内存不能页出。由于 DMA 传输不能处理缺页,因此这种锁定对 DMA 传输来说是十分必要的。physio(9F) 还提供了一种将较大的传输分解为一系列更小的、更易于管理的传输的自动方法。有关更多信息,请参见minphys() 入口点


示例 15–5 使用 physio(9F) 的 read(9E) 和 write(9E) 例程

static int
xxread(dev_t dev, struct uio *uiop, cred_t *credp)
{
     struct xxstate *xsp;
     int ret;

     xsp = ddi_get_soft_state(statep, getminor(dev));
     if (xsp == NULL)
        return (ENXIO);
     ret = physio(xxstrategy, NULL, dev, B_READ, xxminphys, uiop);
     return (ret);
}    

static int
xxwrite(dev_t dev, struct uio *uiop, cred_t *credp)
{     
     struct xxstate *xsp;
     int ret;

     xsp = ddi_get_soft_state(statep, getminor(dev));
     if (xsp == NULL)
        return (ENXIO);
     ret = physio(xxstrategy, NULL, dev, B_WRITE, xxminphys, uiop);
     return (ret);
}

在对 physio(9F) 的调用中,xxstrategy 是指向驱动程序 strategy() 例程的指针。如果将 NULL 作为 buf(9S) 结构指针传递,则指示 physio(9F) 分配 buf(9S) 结构。如果驱动程序必须向 physio(9F) 提供 buf(9S) 结构,应使用 getrbuf(9F) 来分配该结构。如果传输成功完成,physio(9F) 返回零;如果传输失败,则返回错误号。调用 strategy(9E) 后,physio(9F) 会调用 biowait(9F) 以进入阻塞状态,直到传输完成或失败。physio(9F) 的返回值由 buf(9S) 结构中 bioerror(9F) 设置的错误字段确定。

DMA 传输(异步)

支持 aread(9E)awrite(9E) 的字符驱动程序使用 aphysio(9F),而非 physio(9F)

int aphysio(int (*strat)(struct buf *), int (*cancel)(struct buf *),
     dev_t dev, int rw, void (*mincnt)(struct buf *),
     struct aio_req *aio_reqp);

注 –

anocancel(9F) 的地址是唯一一个当前可作为第二个参数传递到 aphysio(9F) 的值。


aphysio(9F) 要求驱动程序传递 strategy(9E) 例程的地址。aphysio(9F) 可确保内存空间处于锁定状态,即在数据传输期间内存不能页出。由于 DMA 传输不能处理缺页,因此这种锁定对 DMA 传输来说是十分必要的。aphysio(9F) 还提供了一种将较大的传输分解为一系列更小的、更易于管理的传输的自动方法。有关更多信息,请参见minphys() 入口点

示例 15–5示例 15–6 说明了 aread(9E)awrite(9E) 入口点与 read(9E)write(9E) 入口点之间的轻微差异。这种差异主要在于,前两者使用 aphysio(9F),而非 physio(9F)


示例 15–6 使用 aphysio(9F) 的 aread(9E) 和 awrite(9E) 例程

static int
xxaread(dev_t dev, struct aio_req *aiop, cred_t *cred_p)
{
     struct xxstate *xsp;

     xsp = ddi_get_soft_state(statep, getminor(dev));
     if (xsp == NULL)
         return (ENXIO);
     return (aphysio(xxstrategy, anocancel, dev, B_READ,
     xxminphys, aiop));
}

static int
xxawrite(dev_t dev, struct aio_req *aiop, cred_t *cred_p)
{
     struct xxstate *xsp;

     xsp = ddi_get_soft_state(statep, getminor(dev));
     if (xsp == NULL)
        return (ENXIO);
     return (aphysio(xxstrategy, anocancel, dev, B_WRITE,
     xxminphys,aiop));  
}

在对 aphysio(9F) 的调用中,xxstrategy() 是指向驱动程序策略例程的指针。aiop 是指向 aio_req(9S) 结构的指针。aiop 将被传递到 aread(9E)awrite(9E)aio_req(9S) 描述了数据在用户空间中的存储位置。如果成功调度了 I/O 请求,aphysio(9F) 返回零;如果调度失败,则返回错误号。调用 strategy(9E) 后,aphysio(9F) 会返回而不等待 I/O 完成或失败。

minphys() 入口点

minphys() 入口点是指向要由 physio(9F)aphysio(9F) 调用的函数的指针。xxminphys 的用途在于确保所请求的传输大小不会超过驱动程序强加的限制。如果用户请求较大的传输,则会重复调用 strategy(9E),这就要求每次都不能超过强加的限制。因为 DMA 资源有限,所以该方法非常重要。对于慢速设备(例如打印机)的驱动程序,应避免其长时间占用资源。

通常,驱动程序会传递内核函数 minphys(9F) 的地址,但驱动程序也可以定义自己的 xxminphys() 例程。xxminphys() 的作用是使 buf(9S) 结构的 b_bcount 字段保持在驱动程序限制内。驱动程序还应遵循其他系统限制。例如,驱动程序的 xxminphys() 例程应该在设置 b_bcount 字段之后而在返回之前调用系统 minphys(9F) 例程。


示例 15–7 minphys(9F) 例程

#define XXMINVAL (512 << 10)    /* 512 KB */
static void
xxminphys(struct buf *bp)
{
       if (bp->b_bcount > XXMINVAL)
        bp->b_bcount = XXMINVAL
      minphys(bp);
}

strategy() 入口点

strategy(9E) 例程源于块驱动程序。策略函数因实现用于对块设备的 I/O 请求的有效排队的策略而得名。面向字符设备的驱动程序也可以使用 strategy(9E) 例程。在这里提供的字符 I/O 模型中,strategy(9E) 并不维护请求队列,只是一次为一个请求提供服务。

在以下示例中,用于面向字符的 DMA 设备的 strategy(9E) 例程为同步数据传输分配 DMA 资源。strategy() 通过对设备寄存器进行编程来启动此命令。有关详细说明,请参见第 9 章


注 –

strategy(9E) 不会以参数形式接收设备编号 (dev_t)。设备编号是从传递给 strategy(9E)buf(9S) 结构中的 b_edev 字段检索到的。



示例 15–8 strategy(9E) 例程

static int
xxstrategy(struct buf *bp)
{
     minor_t            instance;
     struct xxstate     *xsp;
     ddi_dma_cookie_t   cookie;

     instance = getminor(bp->b_edev);
     xsp = ddi_get_soft_state(statep, instance);
     /* ... */
      * If the device has power manageable components,
      * mark the device busy with pm_busy_components(9F),
      * and then ensure that the device is
      * powered up by calling pm_raise_power(9F).
      */
     /* Set up DMA resources with ddi_dma_alloc_handle(9F) and
      * ddi_dma_buf_bind_handle(9F).
      */
     xsp->bp = bp; /* remember bp */
     /* Program DMA engine and start command */
     return (0);
}


注 –

虽然声明了 strategy() 返回 int,但 strategy() 必须总是返回零。


在完成 DMA 传输时,设备会产生中断,从而导致对中断例程的调用。在以下示例中,xxintr() 接收指向可能产生中断的设备的状态结构的指针。


示例 15–9 中断例程

static u_int
xxintr(caddr_t arg)
{
     struct xxstate *xsp = (struct xxstate *)arg;
     if ( /* device did not interrupt */ ) {
        return (DDI_INTR_UNCLAIMED);
     }
     if ( /* error */ ) {
        /* error handling */
     }
     /* Release any resources used in the transfer, such as DMA resources.
      * ddi_dma_unbind_handle(9F) and ddi_dma_free_handle(9F)
      * Notify threads that the transfer is complete.
      */
     biodone(xsp->bp);
     return (DDI_INTR_CLAIMED);
}

驱动程序通过调用 bioerror(9F) 来指示错误。当传输完成或者使用 biodone(9F) 指示错误后,驱动程序必须调用 bioerror(9F)

映射设备内存

通过内存映射,用户线程可以直接访问某些设备(如帧缓存器)的内存。这些设备的驱动程序通常不支持 read(9E)write(9E) 接口。相反,这些驱动程序支持使用 devmap(9E) 入口点的内存映射。例如,帧缓存器驱动程序可以实现 devmap(9E) 入口点,以允许将帧缓存器映射到用户线程。

调用 devmap(9E) 入口点可以将设备内存或内核内存导出到用户应用程序。可以从 segmap(9E) 内部的 devmap_setup(9F) 调用 devmap() 函数,也可以代表 ddi_devmap_segmap(9F) 调用它。

segmap(9E) 入口点负责设置 mmap(2) 系统调用所请求的内存映射。许多内存映射设备的驱动程序使用 ddi_devmap_segmap(9F) 作为入口点,而不是定义自己的 segmap(9E) 例程。

有关详细信息,请参见第 10 章第 11 章

对文件描述符执行多路复用 I/O 操作

一个线程有时需要处理多个文件描述符上的 I/O。需要从温度感应设备读取温度并将此温度报告给交互显示的应用程序就是一个示例。在与用户再次交互之前等待温度时,发出读取请求但没有可用数据的程序不会进入阻塞状态。

poll(2) 系统调用为用户提供了对一组引用打开的文件的文件描述符执行多路复用 I/O 操作的机制。poll(2) 识别那些在它们上面程序可以发送或接收数据而不会阻塞的文件描述符,或在它们上面特定事件已发生的文件描述符。

要允许程序轮询字符驱动程序,该驱动程序必须实现 chpoll(9E) 入口点。当用户进程对与设备相关的文件描述符发出 poll(2) 系统调用时,系统就会调用 chpoll(9E)。需要支持轮询的非 STREAMS 字符设备驱动程序使用 chpoll(9E) 入口点例程。

chpoll(9E) 函数使用以下语法:

int xxchpoll(dev_t dev, short events, int anyyet, short *reventsp,
     struct pollhead **phpp);

在 chpoll(9E) 入口点中,驱动程序必须遵循下列规则:

示例 15–10示例 15–11 说明了如何实现轮询规程以及如何使用 pollwakeup(9F)。

以下示例说明如何处理 POLLINPOLLERR 事件。首先,驱动程序读取状态寄存器以确定设备的当前状态。参数 events 指定驱动程序应该检查哪些条件。如果出现适当的条件,驱动程序会在 *reventsp 中设置相应的位。如果未出现任何条件并且没有设置 anyyet,则会在 *phpp 中返回 pollhead 结构的地址。


示例 15–10 chpoll(9E) 例程

static int
xxchpoll(dev_t dev, short events, int anyyet,
    short *reventsp, struct pollhead **phpp)
{
     uint8_t status;
     short revent;
     struct xxstate *xsp;

     xsp = ddi_get_soft_state(statep, getminor(dev));
     if (xsp == NULL)
         return (ENXIO);
     revent = 0;
     /*
    * Valid events are:
    * POLLIN | POLLOUT | POLLPRI | POLLHUP | POLLERR
    * This example checks only for POLLIN and POLLERR.
    */
     status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
     if ((events & POLLIN) && data available to read) {
        revent |= POLLIN;
     }
     if (status & DEVICE_ERROR) {
        revent |= POLLERR;
     }
     /* if nothing has occurred */
     if (revent == 0) {
        if (!anyyet) {
        *phpp = &xsp->pollhead;
        }
     }
       *reventsp = revent;
     return (0);
}

以下示例说明如何使用 pollwakeup(9F) 函数。当出现支持的条件时,通常会在中断例程中调用 pollwakeup(9F) 函数。中断例程从状态寄存器中读取状态并检查条件。然后,该例程为每个事件调用 pollwakeup(9F),以便有可能通知轮询线程再次进行检查。请注意,不能在持有任何锁定的情况下调用 pollwakeup(9F),这是因为如果另一个例程尝试进入 chpoll(9E) 并获取相同的锁,则会导致死锁。


示例 15–11 支持 chpoll(9E) 的中断例程

static u_int
xxintr(caddr_t arg)
{
     struct xxstate *xsp = (struct xxstate *)arg;
     uint8_t    status;
     /* normal interrupt processing */
     /* ... */
     status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
     if (status & DEVICE_ERROR) {
        pollwakeup(&xsp->pollhead, POLLERR);
     }
     if ( /* just completed a read */ ) {
        pollwakeup(&xsp->pollhead, POLLIN);
     }
     /* ... */
     return (DDI_INTR_CLAIMED);
}

其他 I/O 控制

当用户线程对与设备相关的文件描述符发出 ioctl(2) 系统调用时,就会调用 ioctl(9E) 例程。I/O 控制机制是获取和设置设备特定参数的统称。该机制经常用于设置设备特定模式(通过设置内部驱动程序软件标志或将命令写入设备)。也可以使用该控制机制向用户返回有关当前设备状态的信息。简而言之,控制机制可以做应用程序和驱动程序需要完成的任何事情。

ioctl() 入口点(字符驱动程序)

int xxioctl(dev_t dev, int cmd, intptr_t arg, int mode,
     cred_t *credp, int *rvalp);

cmd 参数表明应该执行哪个 ioctl(9E) 命令。根据约定,命令的 8-15 位指示与 I/O 控制命令有关的驱动程序。通常,字符的 ASCII 代码代表该驱动程序。驱动程序特定命令位于 0-7 位。以下示例说明了一些 I/O 命令的创建:

#define XXIOC    (`x' << 8)     /* `x' is a character representing */
                                      /* device xx */
#define XX_GET_STATUS    (XXIOC | 1)  /* get status register */
#define XX_SET_CMD       (XXIOC | 2)  /* send command */

arg 的解释视命令而定。在驱动程序文档或手册页中应该介绍了这些 I/O 控制命令。在公共头文件中还会定义命令,以便应用程序能够确定命令的名称、命令执行的操作以及命令以 arg 的形式接受或返回的内容。进出驱动程序的任何 arg 数据传输都必须由驱动程序执行。

特定种类的设备(如帧缓存器或磁盘)必须支持 I/O 控制请求的标准集。这些标准 I/O 控制接口在 Solaris 8 Reference Manual Collection 中进行了介绍。例如,fbio(7I) 介绍了帧缓存器必须支持的 I/O 控制,而 dkio(7I) 则介绍了标准的磁盘 I/O 控制。有关 I/O 控制的更多信息,请参见其他 I/O 控制

驱动程序必须使用 ddi_copyin(9F) 从用户级别的应用程序向内核级别的应用程序传输 arg 数据。驱动程序必须使用 ddi_copyout(9F) 从内核级别向用户级别传输数据。在这两种情况下,如果无法使用 ddi_copyin(9F)ddi_copyout(9F),则会导致系统出现紧急情况。如果体系结构将内核地址空间和用户地址空间分开,或者用户地址空间被换出,系统都会出现紧急情况。

对于每个支持的 ioctl(9E) 请求,ioctl(9E) 通常是 switch 语句。


示例 15–12 ioctl(9E) 例程

static int
xxioctl(dev_t dev, int cmd, intptr_t arg, int mode,
    cred_t *credp, int *rvalp)
{
     uint8_t        csr;
     struct xxstate     *xsp;

     xsp = ddi_get_soft_state(statep, getminor(dev));
     if (xsp == NULL) {
        return (ENXIO);
     }
     switch (cmd) {
     case XX_GET_STATUS:
       csr = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
       if (ddi_copyout(&csr, (void *)arg,
           sizeof (uint8_t), mode) != 0) {
           return (EFAULT);
       }
       break;
     case XX_SET_CMD:
       if (ddi_copyin((void *)arg, &csr,
         sizeof (uint8_t), mode) != 0) {
         return (EFAULT);
       }
       ddi_put8(xsp->data_access_handle, &xsp->regp->csr, csr);
       break;
     default:
       /* generic "ioctl unknown" error */
       return (ENOTTY);
     }
     return (0);
}

cmd 变量识别特定的设备控制操作。如果 arg 包含用户虚拟地址,则会出现问题。ioctl(9E) 必须调用 ddi_copyin(9F)ddi_copyout(9F),以便在 arg 指向的应用程序中的数据结构与驱动程序之间传输数据。在示例 15–12 中,对于 XX_GET_STATUS 请求,xsp->regp->csr 的内容会被复制到 arg 中的地址。ioctl(9E) 可以在 *rvalp 中存储任何作为成功发出请求的 ioctl(2) 系统调用的返回值的整数值。应当避免返回负值,例如 -1。许多应用程序假定负值表示失败。

以下示例说明了使用上一段落中所讨论的 I/O 控制的应用程序。


示例 15–13 使用 ioctl(9E)

#include <sys/types.h>
#include "xxio.h"     /* contains device's ioctl cmds and args */
int
main(void)
{
     uint8_t    status;
     /* ... */
     /*
      * read the device status
      */
     if (ioctl(fd, XX_GET_STATUS, &status) == -1) {
         /* error handling */
     }
     printf("device status %x\n", status);
     exit(0);
}

对有 64 位处理能力的设备驱动程序的 I/O 控制支持

Solaris 内核在适当的硬件上以 64 位模式运行,既支持 32 位应用程序,也支持 64 位应用程序。要求 64 位设备驱动程序同时支持来自这两种处理能力的程序的 I/O 控制命令。32 位程序与 64 位程序的差异在于 C 语言类型模型。32 位程序是 ILP32,而 64 位程序是 LP64。有关 C 数据类型模型的信息,请参见附录 C

如果程序和内核之间传输的数据具有不同的格式,则驱动程序必须能够处理这种模型不匹配。处理模型不匹配需要对数据进行适当的调整。

要确定是否存在模型不匹配,ioctl(9E) 模式参数会将数据模型位传递到驱动程序。如示例 15–14 中所示,该模式参数随后会被传递到 ddi_model_convert_from(9F),以确定是否有必要进行模型转换。

模式参数的标志子字段用于将数据模型传递到 ioctl(9E) 例程中。可以将此标志设置为以下值之一:

可以有条件地定义 FNATIVE,以匹配内核实现的数据模型。应使用 FMODELS 掩码来提取 mode 参数的标志。驱动程序随后会对数据模型进行明确检查,以确定如何复制应用程序的数据结构。

DDI 函数 ddi_model_convert_from(9F) 是一个公用例程,可帮助一些驱动程序完成它们的 ioctl() 调用。该函数将用户应用程序的数据类型模型用作参数,并返回下列值之一:

如果不必进行数据转换,则会返回 DDI_MODEL_NONE。当应用程序和驱动程序具有相同的数据模型时,便会发生这种情况。DDI_MODEL_ILP32 将返回到被编译为 LP64 模式而且能够与 32 位应用程序进行通信的驱动程序。

在以下示例中,驱动程序复制了包含用户地址的数据结构。数据结构的处理能力从 ILP32 更改为 LP64。相应地,此 64 位驱动程序在与 32 位应用程序进行通信时使用 32 位版本的结构。


示例 15–14 用于支持 32 位应用程序和 64 位应用程序的 ioctl(9E) 例程

struct args32 {
    uint32_t    addr;    /* 32-bit address in LP64 */
    int     len;
}
struct args {
    caddr_t     addr;    /* 64-bit address in LP64 */
    int     len;
}

static int
xxioctl(dev_t dev, int cmd, intptr_t arg, int mode,
    cred_t *credp, int *rvalp)
{
    struct  xxstate  *xsp;
    struct  args     a;
    xsp = ddi_get_soft_state(statep, getminor(dev));
    if (xsp == NULL) {
        return (ENXIO);
    }
    switch (cmd) {
    case XX_COPYIN_DATA:
        switch(ddi_model_convert_from(mode)) {
        case DDI_MODEL_ILP32:
        {
            struct args32 a32;

            /* copy 32-bit args data shape */
            if (ddi_copyin((void *)arg, &a32,
                sizeof (struct args32), mode) != 0) {
                return (EFAULT);
            }
            /* convert 32-bit to 64-bit args data shape */
            a.addr = a32.addr;
            a.len = a32.len;
            break;
        }
        case DDI_MODEL_NONE:
            /* application and driver have same data model. */
            if (ddi_copyin((void *)arg, &a, sizeof (struct args),
                mode) != 0) {
                return (EFAULT);
            }
        }
        /* continue using data shape in native driver data model. */
        break;

    case XX_COPYOUT_DATA:
        /* copyout handling */
        break;
    default:
        /* generic "ioctl unknown" error */
        return (ENOTTY);
    }
    return (0);
}

处理 copyout() 溢出

驱动程序有时需要将不再适于 32 位大小结构的本机数值复制出来。在这种情况下,驱动程序应向调用方返回 EOVERFLOWEOVERFLOW 用于表明接口中的数据类型太小,无法保存要返回的值,如以下示例中所示。


示例 15–15 处理 copyout(9F) 溢出

int
    xxioctl(dev_t dev, int cmd, intptr_t arg, int mode,
     cred_t *cr, int *rval_p)
    {
        struct resdata res;
        /* body of driver */
        switch (ddi_model_convert_from(mode & FMODELS)) {
        case DDI_MODEL_ILP32: {
            struct resdata32 res32;

            if (res.size > UINT_MAX)
                    return (EOVERFLOW);    
            res32.size = (size32_t)res.size;
            res32.flag = res.flag;
            if (ddi_copyout(&res32,
                (void *)arg, sizeof (res32), mode))
                    return (EFAULT);
        }
        break;

        case DDI_MODEL_NONE:
            if (ddi_copyout(&res, (void *)arg, sizeof (res), mode))
                    return (EFAULT);
            break;
        }
        return (0);
    }

32 位和 64 位数据结构宏

示例 15–15 中的方法适用于许多驱动程序。另一种方案是使用 <sys/model.h> 中提供的数据结构宏在应用程序和内核之间移动数据。从功能角度看,这些宏减少了代码混乱问题,并使代码的表现形式完全相同。


示例 15–16 使用数据结构宏移动数据

int
    xxioctl(dev_t dev, int cmd, intptr_t arg, int mode,
        cred_t *cr, int *rval_p)
    {    
        STRUCT_DECL(opdata, op);

        if (cmd != OPONE)
            return (ENOTTY);

        STRUCT_INIT(op, mode);

        if (copyin((void *)arg,
            STRUCT_BUF(op), STRUCT_SIZE(op)))
                return (EFAULT);

        if (STRUCT_FGET(op, flag) != XXACTIVE ||     
            STRUCT_FGET(op, size) > XXSIZE)
                return (EINVAL);
        xxdowork(device_state, STRUCT_FGET(op, size));
        return (0);
}

结构宏如何工作?

在 64 位设备驱动程序中,结构宏使得两种处理能力的数据结构可以使用相同的内核内存片段。内存缓冲区保存数据结构的本机形式的内容,即 LP64 和 ILP32 形式。每种结构的访问是通过条件表达式实现的。如果驱动程序以 32 位方式编译,则仅支持一种数据模型(本机形式)。不使用条件表达式。

64 位版本的宏依赖于数据结构阴影版本的定义。阴影版本描述了使用固定宽度类型的 32 位接口。将 "32" 附加到本机数据结构名称,就形成了阴影数据结构的名称。为方便起见,将阴影结构的定义放置到与本机结构相同的文件中,以降低将来的维护成本。

这些宏可以采用下列参数:

structname

数据结构的本机形式的结构名称,即在 struct 关键字后输入的内容。

umodel

包含用户数据模型(例如 FILP32FLP64)的标志字,从 ioctl(9E) 的模式参数中提取。

handle

此名称用于引用这些宏所处理的结构的特定实例。

fieldname

结构内部的字段的名称。

何时使用结构宏

宏使您能够仅对数据项的字段进行适当地引用。宏不提供采用基于数据模型的单独代码路径的方法。如果数据结构中的字段数量很大,则应避免使用宏。如果对这些字段的引用非常频繁,也应避免使用宏。

在实现宏的过程中,宏隐藏了数据模型之间的很多差异。因此,使用此接口编写的代码通常比较容易理解。如果驱动程序以 32 位方式编译,则生成的代码较为简洁,并且无需冗长的 #ifdefs,但仍保留了类型检查。

声明并初始化结构句柄

可以使用 STRUCT_DECL(9F)STRUCT_INIT(9F) 声明和初始化句柄及空间,以便在栈中对 ioctl 进行解码。STRUCT_HANDLE(9F)STRUCT_SET_HANDLE(9F) 可以声明和初始化句柄,但不在栈中分配空间。如果结构非常大,或者包含在其他某个数据结构中,则后面的宏比较有用。


注 –

因为 STRUCT_DECL(9F)STRUCT_HANDLE(9F) 宏扩展为数据结构声明,所以这些宏在 C 代码中应该使用这些声明进行分组。


用于声明和初始化结构的宏如下所示:

STRUCT_DECL(structname, handle)

声明为 structname 数据结构调用了 handle结构句柄STRUCT_DECL 按其本机形式在栈中分配空间。假定本机形式大于或等于结构的 ILP32 形式。

STRUCT_INIT(handle, umodel)

handle 的数据模型初始化为 umodel。在对使用 STRUCT_DECL(9F) 声明的结构句柄进行任何访问之前,必须调用此宏。

STRUCT_HANDLE(structname, handle)

声明调用了 handle结构句柄。它与 STRUCT_DECL(9F) 相对。

STRUCT_SET_HANDLE(handle , umodel, addr )

handle 的数据模型初始化为 umodel,然后将 addr 设置为用于后续处理的缓冲区。在访问使用 STRUCT_DECL(9F) 声明的结构句柄之前,请调用此宏。

结构句柄的操作

用于在结构上执行操作的宏如下所示:

size_t STRUCT_SIZE(handle )

返回 handle 所引用的结构的大小(取决于该结构的嵌入式数据模型)。

typeof fieldname STRUCT_FGET(handle, fieldname)

返回 handle 所引用的数据结构中的指定字段。此字段为非指针类型。

typeof fieldname STRUCT_FGETP(handle, fieldname)

返回 handle 所引用的数据结构中的指定字段。此字段为指针类型。

STRUCT_FSET(handle , fieldname, val)

handle 所引用的数据结构中的指定字段设置为值 valval 的类型应与 fieldname 的类型相匹配。此字段为非指针类型。

STRUCT_FSETP(handle , fieldname, val)

handle 所引用的数据结构中的指定字段设置为值 val。此字段为指针类型。

typeof fieldname *STRUCT_FADDR(handle, fieldname)

返回 handle 所引用的数据结构中的指定字段的地址。

struct structname *STRUCT_BUF(handle)

返回指向 handle 所描述的本机结构的指针。

其他操作

其他一些结构宏如下所示:

size_t SIZEOF_STRUCT(struct_name, datamodel)

返回 struct_name 的大小(取决于给定的数据模型)。

size_t SIZEOF_PTR(datamodel )

根据给定的数据模型,返回指针的大小。

第 16 章 块设备驱动程序

本章介绍块设备驱动程序的结构。内核将块设备视为一组可随机访问的逻辑块。文件系统使用一系列 buf(9S) 结构来缓存块设备和用户空间之间的数据块。只有块设备可以支持文件系统。

本章介绍有关以下主题的信息:

块驱动程序结构概述

图 16–1 显示了用来定义块设备驱动程序结构的数据结构和例程。设备驱动程序通常包括以下元素:

下图中涂有阴影的设备访问部分列出了块驱动程序入口点。

图 16–1 块驱动程序结构图

图显示了块设备驱动程序的结构和入口点。

对于每个设备驱动程序,都有一个 dev_ops(9S) 结构与之关联,该结构进而又引用 cb_ops(9S) 结构。有关驱动程序数据结构的详细信息,请参见第 6 章

块设备驱动程序提供下列入口点:


注 –

可根据需要将其中一些入口点替换为 nodev(9F)nulldev(9F)


文件 I/O

文件系统是由目录和文件组成的树状分层结构。诸如 UNIX 文件系统 (UNIX File System, UFS) 之类的一些文件系统驻留在面向块的设备上。文件系统由 format(1M)newfs(1M) 创建。

当应用程序向 UFS 文件系统上的普通文件发出 read(2)write(2) 系统调用时,该文件系统可为所驻留在的块设备调用设备驱动程序 strategy(9E) 入口点。对于一个 read(2)write(2) 系统调用,文件系统代码可多次调用 strategy(9E)

文件系统代码为每个普通文件块确定逻辑设备地址,即逻辑块编号。然后采用指向块设备的 buf(9S) 结构的形式建立块 I/O 请求。驱动程序 strategy(9E) 入口点随后解释 buf(9S) 结构并完成请求。

块设备自动配置

attach(9E) 会为每个设备实例执行常见初始化任务:

块设备驱动程序创建类型为 S_IFBLK 的次要节点。因此,代表节点的块特殊文件会出现在 /devices 分层结构中。

块设备的逻辑设备名称位于 /dev/dsk 目录中,该名称由控制器编号、总线地址编号、磁盘编号和片编号组成。如果节点类型设置为 DDI_NT_BLOCKDDI_NT_BLOCK_CHAN,则这些名称由 devfsadm(1M) 程序创建。如果设备通过通道(即有附加寻址能力级别的总线)进行通信,则应该指定 DDI_NT_BLOCK_CHAN。SCSI 磁盘就是一个典型示例。DDI_NT_BLOCK_CHAN 可使总线地址字段 (tN) 出现在逻辑名称中。其他大多数设备则应该使用 DDI_NT_BLOCK

次要设备指磁盘上的分区。对于每个次要设备,驱动程序必须创建 nblocksNblocks 属性。此整数属性给出了次要设备所支持的块数,以 DEV_BSIZE(即 512 字节)为单位。文件系统使用 nblocksNblocks 属性来确定设备限制。Nblocks 是 64 位版本的 nblocks。应该将 Nblocks 用于每个磁盘的存储容量超过 1 TB 的存储设备。有关更多信息,请参见设备属性

示例 16–1 说明了一个典型的 attach(9E) 入口点,重点说明如何创建设备的次要节点和 Nblocks 属性。请注意,由于此示例使用 Nblocks 而非 nblocks,因此将调用 ddi_prop_update_int64(9F) 而非 ddi_prop_update_int(9F)

作为附带说明,本示例还说明了如何使用 makedevice(9F)ddi_prop_update_int64() 创建设备编号。makedevice 函数利用 ddi_driver_major(9F),后者基于指向 dev_info_t 结构的指针生成主设备号。使用 ddi_driver_major() 与使用 getmajor(9F) 类似,后者用于获取 dev_t 结构指针。


示例 16–1 块驱动程序 attach() 例程

static int
xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
     int instance = ddi_get_instance(dip);
     switch (cmd) {
       case DDI_ATTACH:
       /*
        * allocate a state structure and initialize it
        * map the devices registers
        * add the device driver's interrupt handler(s)
        * initialize any mutexes and condition variables
        * read label information if the device is a disk
        * create power manageable components
        *
        * Create the device minor node. Note that the node_type
        * argument is set to DDI_NT_BLOCK.
        */
       if (ddi_create_minor_node(dip, "minor_name", S_IFBLK,
          instance, DDI_NT_BLOCK, 0) == DDI_FAILURE) {
          /* free resources allocated so far */
          /* Remove any previously allocated minor nodes */
          ddi_remove_minor_node(dip, NULL);
          return (DDI_FAILURE);
        }
       /*
        * Create driver properties like "Nblocks". If the device
        * is a disk, the Nblocks property is usually calculated from
        * information in the disk label.  Use "Nblocks" instead of
        * "nblocks" to ensure the property works for large disks.
        */
       xsp->Nblocks = size;
       /* size is the size of the device in 512 byte blocks */
       maj_number = ddi_driver_major(dip);
       if (ddi_prop_update_int64(makedevice(maj_number, instance), dip, 
          "Nblocks", xsp->Nblocks) != DDI_PROP_SUCCESS) {
          cmn_err(CE_CONT, "%s: cannot create Nblocks property\n",
               ddi_get_name(dip));
         /* free resources allocated so far */
         return (DDI_FAILURE);
       }
       xsp->open = 0;
       xsp->nlayered = 0;
       /* ... */
       return (DDI_SUCCESS);

    case DDI_RESUME:
       /* For information, see Chapter 12, "Power Management," in this book. */
       default:
          return (DDI_FAILURE);
     }
}

控制设备访问

本节介绍块设备驱动程序中 open()close() 函数的入口点。有关 open(9E)close(9E) 的更多信息,请参见第 15 章

open() 入口点(块驱动程序)

open(9E) 入口点用于访问给定的设备。当用户线程对与次要设备相关的块特殊文件发出 open(2)mount(2) 系统调用时,或者当分层驱动程序调用 open(9E) 时,均会调用块驱动程序的 open(9E) 例程。有关更多信息,请参见文件 I/O

open() 入口点会检查下列条件:

以下示例说明了块驱动程序 open(9E) 入口点。


示例 16–2 块驱动程序 open(9E) 例程

static int
xxopen(dev_t *devp, int flags, int otyp, cred_t *credp)
{
       minor_t         instance;
       struct xxstate        *xsp;

     instance = getminor(*devp);
     xsp = ddi_get_soft_state(statep, instance);
     if (xsp == NULL)
           return (ENXIO);
     mutex_enter(&xsp->mu);
     /*
    * only honor FEXCL. If a regular open or a layered open
    * is still outstanding on the device, the exclusive open
    * must fail.
    */
     if ((flags & FEXCL) && (xsp->open || xsp->nlayered)) {
       mutex_exit(&xsp->mu);
       return (EAGAIN);
     }
     switch (otyp) {
       case OTYP_LYR:
         xsp->nlayered++;
         break;
      case OTYP_BLK:
         xsp->open = 1;
         break;
     default:
         mutex_exit(&xsp->mu);
         return (EINVAL);
     }
   mutex_exit(&xsp->mu);
      return (0);
}

otyp 参数用于指定设备的打开类型。OTYP_BLK 是块设备的典型打开类型。将 otyp 设置为 OTYP_BLK 后,可以多次打开设备。最后关闭设备的 OTYP_BLK 类型时,仅调用一次 close(9E)。如果将设备用作分层设备,则 otyp 设置为 OTYP_LYR。每次打开 OTYP_LYR 类型,分层驱动程序都会发出类型为 OTYP_LYR 的对应关闭。此示例跟踪每种类型的打开,因此驱动程序可以确定何时设备不用于 close(9E)

close() 入口点(块驱动程序)

close(9E) 入口点使用与 open(9E) 相同的参数,但有一个例外。dev 是设备编号,而不是指向设备编号的指针。

close() 例程应采用与前面所述的 open(9E) 入口点所采用的相同的方式来检验 otyp。在以下示例中,close() 必须确定何时可以真正关闭设备。关闭受块打开数和分层打开数的影响。


示例 16–3 块设备 close(9E) 例程

static int
xxclose(dev_t dev, int flag, int otyp, cred_t *credp)
{
     minor_t instance;
     struct xxstate *xsp;

     instance = getminor(dev);
     xsp = ddi_get_soft_state(statep, instance);
       if (xsp == NULL)
          return (ENXIO);
     mutex_enter(&xsp->mu);
     switch (otyp) {
       case OTYP_LYR:
       xsp->nlayered--;
       break;
      case OTYP_BLK:
       xsp->open = 0;
       break;
     default:
       mutex_exit(&xsp->mu);
       return (EINVAL);
       }

     if (xsp->open || xsp->nlayered) {
       /* not done yet */
       mutex_exit(&xsp->mu);
       return (0);
     }
       /* cleanup (rewind tape, free memory, etc.) */
   /* wait for I/O to drain */
     mutex_exit(&xsp->mu);

     return (0);
}

strategy() 入口点

strategy(9E) 入口点用于从块设备读取数据缓冲区以及向块设备写入数据缓冲区。名称 strategy 指的是该入口点可以实现一些优化策略以对设备请求进行排序。

可以将 strategy(9E) 编写为一次处理一个请求,即同步传输。也可以将 strategy() 编写为对发送给设备的多个请求进行排队,即异步传输。选择方法时,应当考虑设备的能力和限制。

将向 strategy(9E) 例程传递一个指向 buf(9S) 结构的指针。此结构描述传输请求,并包含有关返回的状态信息。buf(9S)strategy(9E) 是块设备操作的焦点。

buf 结构

以下 buf 结构成员对块驱动程序很重要:

     int          b_flags;     /* Buffer Status */
     struct buf       *av_forw;    /* Driver work list link */
     struct buf       *av_back;    /* Driver work list link */
     size_t       b_bcount;    /* # of bytes to transfer */
     union {
     caddr_t      b_addr;      /* Buffer's virtual address */
     } b_un;
     daddr_t      b_blkno;     /* Block number on device */
     diskaddr_t       b_lblkno;    /* Expanded block number on device */
     size_t       b_resid;     /* # of bytes not transferred */
                       /* after error */
     int          b_error;     /* Expanded error field */
     void         *b_private;      /* “opaque” driver private area */
     dev_t        b_edev;      /* expanded dev field */

其中:

av_forwav_back

驱动程序可用以管理其使用的一组缓冲区的指针。有关 av_forwav_back 指针的讨论,请参见异步数据传输(块驱动程序)

b_bcount

指定要由设备传输的字节数。

b_un.b_addr

数据缓冲区的内核虚拟地址。仅在进行 bp_mapin(9F) 调用后有效。

b_blkno

设备上用于数据传输的起始 32 位逻辑块编号,以 DEV_BSIZE(512 字节)为单位。驱动程序应使用 b_blknob_lblkno,但不能同时使用两者。

b_lblkno

设备上用于数据传输的起始 64 位逻辑块编号,以 DEV_BSIZE(512 字节)为单位。驱动程序应使用 b_blknob_lblkno,但不能同时使用两者。

b_resid

由驱动程序设置的用于表明由于发生错误而未传输的字节数。有关设置 b_resid 的示例,请参见示例 16–7b_resid 成员会过载。此外,disksort(9F) 也会使用 b_resid

b_error

当发生传输错误时,由驱动程序设置为错误编号。b_error 应与 b_flags B_ERROR 位一起设置。有关错误值的详细信息,请参见 Intro(9E) 手册页。驱动程序应使用 bioerror(9F),而不是直接设置 b_error

b_flags

表示 buf 结构的状态属性和传输属性的标志。如果设置了 B_READ,则 buf 结构指明从设备到内存的传输。否则,此结构指明从内存到设备的传输。如果在数据传输期间驱动程序遇到错误,则该驱动程序应设置 b_flags 成员中的 B_ERROR 字段。此外,该驱动程序还应在 b_error 中提供一个更明确的错误值。驱动程序应使用 bioerror(9F),而不是设置 B_ERROR


注意 – 注意 –

驱动程序绝不能清除 b_flags


b_private

专供驱动程序存储驱动程序专用数据。

b_edev

包含用于传输的设备的设备编号。

bp_mapin 结构

可以将 buf 结构指针传递到设备驱动程序的 strategy(9E) 例程。但是,b_un.b_addr 引用的数据缓冲区不一定映射到内核地址空间中。因此,驱动程序无法直接访问数据。大多数面向块的设备具有 DMA 功能,因此不需要直接访问数据缓冲区。这些设备改为使用 DMA 映射例程以使设备的 DMA 引擎进行数据传输。有关使用 DMA 的详细信息,请参见第 9 章

如果驱动程序需要直接访问数据缓冲区,则该驱动程序必须首先使用 bp_mapin(9F) 将缓冲区映射到内核地址空间。当驱动程序不再需要直接访问数据时,应使用 bp_mapout(9F)


注意 – 注意 –

只应对已分配且由设备驱动程序拥有的缓冲区调用 bp_mapout(9F)。不得对通过 strategy(9E) 入口点传递到驱动程序的缓冲区(如文件系统)调用 bp_mapout()bp_mapin(9F) 不保留引用计数。bp_mapout(9F) 将删除设备驱动程序之上的层所依赖的任何内核映射。


同步数据传输(块驱动程序)

本节介绍一种执行同步 I/O 传输的简单方法。此方法假设硬件是使用 DMA 一次只能传输一个数据缓冲区的简单磁盘设备。另一个假设是磁盘可以通过软件命令启动和停止。设备驱动程序的 strategy(9E) 例程等待当前请求完成,然后再接受新的请求。当传输完成时,设备中断。如果发生错误,设备也中断。

执行块驱动程序的同步数据传输的步骤如下:

  1. 检查是否有无效的 buf(9S) 请求。

    检查传递到 strategy(9E)buf(9S) 结构是否有效。所有驱动程序应检查以下条件:

    • 请求起始于有效的块。驱动程序将 b_blkno 字段转换为正确的设备偏移,然后确定该偏移对设备而言是否有效。

    • 请求不能超出设备上的最后一个块。

    • 满足特定于设备的要求。

    如果遇到错误,驱动程序应使用 bioerror(9F) 指示相应的错误。然后,驱动程序通过调用 biodone(9F) 来完成请求。biodone() 会通知 strategy(9E) 的调用方传输已完成。在本例中,传输因错误而停止。

  2. 检查此设备是否忙。

    同步数据传输允许对设备进行单线程访问。设备驱动程序通过两种方式执行这种访问:

    • 驱动程序保持由互斥锁保护的忙标志。

    • 当设备忙时,驱动程序等待 cv_wait(9F) 的条件变量。

    如果设备忙,线程会一直等待,直到中断处理程序指示设备不再忙。cv_broadcast(9F)cv_signal(9F) 函数可以指示可用的状态。有关条件变量的详细信息,请参见第 3 章

    当设备不再忙时,strategy(9E) 例程将设备标记为可用。然后,strategy() 为传输准备缓冲区和设备。

  3. 为 DMA 设置缓冲区。

    通过使用 ddi_dma_alloc_handle(9F) 为 DMA 传送准备数据缓冲区,以便分配 DMA 句柄。使用 ddi_dma_buf_bind_handle(9F) 将数据缓冲区绑定到该句柄。有关设置 DMA 资源及相关数据结构的信息,请参见第 9 章

  4. 开始传输。

    此时,指向 buf(9S) 结构的指针将保存在设备的状态结构中。然后中断例程通过调用 biodone(9F) 来完成传输。

    设备驱动程序随后访问设备寄存器以启动数据传输。在大多数情况下,驱动程序应通过使用互斥锁来保护设备寄存器免受其他线程干扰。在本例中,由于 strategy(9E) 是单线程的,因此没有必要保护设备寄存器。有关数据锁定的详细信息,请参见第 3 章

    当执行线程已经启动设备的 DMA 引擎时,驱动程序可以将执行控制权返回到正在调用的例程,如下所示:

    static int
    xxstrategy(struct buf *bp)
    {
        struct xxstate *xsp;
        struct device_reg *regp;
        minor_t instance;
        ddi_dma_cookie_t cookie;
        instance = getminor(bp->b_edev);
        xsp = ddi_get_soft_state(statep, instance);
        if (xsp == NULL) {
           bioerror(bp, ENXIO);
           biodone(bp);
           return (0);
        }
        /* validate the transfer request */
        if ((bp->b_blkno >= xsp->Nblocks) || (bp->b_blkno < 0)) {
           bioerror(bp, EINVAL);    
           biodone(bp);
           return (0);
        }
        /*
         * Hold off all threads until the device is not busy.
         */
        mutex_enter(&xsp->mu);
        while (xsp->busy) {
           cv_wait(&xsp->cv, &xsp->mu);
        }
        xsp->busy = 1;
        mutex_exit(&xsp->mu);
        /* 
         * If the device has power manageable components, 
         * mark the device busy with pm_busy_components(9F),
         * and then ensure that the device 
         * is powered up by calling pm_raise_power(9F).
         *
         * Set up DMA resources with ddi_dma_alloc_handle(9F) and
         * ddi_dma_buf_bind_handle(9F).
         */
        xsp->bp = bp;
        regp = xsp->regp;
        ddi_put32(xsp->data_access_handle, &regp->dma_addr,
            cookie.dmac_address);
        ddi_put32(xsp->data_access_handle, &regp->dma_size,
             (uint32_t)cookie.dmac_size);
        ddi_put8(xsp->data_access_handle, &regp->csr,
             ENABLE_INTERRUPTS | START_TRANSFER);
        return (0);
    }
  5. 处理中断的设备。

    当设备完成数据传输时,设备会生成中断,最终导致驱动程序的中断例程被调用。注册中断时,大多数驱动程序将设备的状态结构指定为中断例程的参数。请参见 ddi_add_intr(9F) 手册页和注册中断。随后,中断例程可以访问正在被传输的 buf(9S) 结构,以及状态结构提供的任何其他信息。

    中断处理程序会检查设备的状态寄存器,来确定是否在未发生任何错误的情况下完成传输。如果发生错误,处理程序应该使用 bioerror(9F) 指示相应的错误。处理程序还应该清除设备的挂起中断,然后通过调用 biodone(9F) 来完成传输。

    最后一项任务是处理程序清除忙标志。处理程序随后对条件变量调用 cv_signal(9F)cv_broadcast(9F),发出设备不再忙的信号。此通知会使在 strategy(9E) 中等待设备的其他线程继续进行下一个数据传输。

    以下示例说明了一个同步中断例程。


示例 16–4 块驱动程序的同步中断例程

static u_int
xxintr(caddr_t arg)
{
    struct xxstate *xsp = (struct xxstate *)arg;
    struct buf *bp;
    uint8_t status;
    mutex_enter(&xsp->mu);
    status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
    if (!(status & INTERRUPTING)) {
       mutex_exit(&xsp->mu);
       return (DDI_INTR_UNCLAIMED);
    }
    /* Get the buf responsible for this interrupt */
    bp = xsp->bp;
    xsp->bp = NULL;
    /*
     * This example is for a simple device which either
     * succeeds or fails the data transfer, indicated in the
     * command/status register.
     */
    if (status & DEVICE_ERROR) {
       /* failure */
       bp->b_resid = bp->b_bcount;
       bioerror(bp, EIO);
    } else {
       /* success */
       bp->b_resid = 0;
    }
    ddi_put8(xsp->data_access_handle, &xsp->regp->csr,
       CLEAR_INTERRUPT);
    /* The transfer has finished, successfully or not */
    biodone(bp);
    /*
     * If the device has power manageable components that were
     * marked busy in strategy(9F), mark them idle now with
     * pm_idle_component(9F)
     * Release any resources used in the transfer, such as DMA
     * resources ddi_dma_unbind_handle(9F) and
     * ddi_dma_free_handle(9F).
     *
     * Let the next I/O thread have access to the device.
     */
    xsp->busy = 0;
    cv_signal(&xsp->cv);
    mutex_exit(&xsp->mu);
    return (DDI_INTR_CLAIMED);
}

异步数据传输(块驱动程序)

本节介绍一种执行异步 I/O 传输的方法。驱动程序将对 I/O 请求进行排队,然后将控制权返回到调用方。还是假设硬件是一次可以传输一个缓冲区的简单磁盘设备。当数据传输完成时,设备中断。如果发生错误,也会产生中断。执行异步数据传输的基本步骤如下:

  1. 检查是否有无效的 buf(9S) 请求。

  2. 对请求进行排队。

  3. 开始第一个传输。

  4. 处理中断的设备。

检查是否有无效的 buf 请求

正如同步传输示例中所示,设备驱动程序会检查传递到 strategy(9E)buf(9S) 结构是否有效。有关更多详细信息,请参见同步数据传输(块驱动程序)

对请求进行排队

与同步数据传输不同,驱动程序不等待异步请求完成,而是向队列中添加请求。队列的开头可以是当前传输,也可以是保留活动请求的状态结构中的独立字段,如示例 16–5 中所示。

如果队列开始是空的,那么硬件不忙,并且 strategy(9E) 在返回之前开始传输。否则,如果在队列非空的情况下完成一个传输,则中断例程会开始一个新的传输。为方便起见,示例 16–5 仅在一个单独的例程中决定是否开始新的传输。

驱动程序可以使用 buf(9S) 结构中的 av_forwav_back 成员来管理传输请求列表。可以使用单个指针管理单链接表,也可以同时使用两个指针建立双链接表。设备硬件规格指定哪种类型的列表管理(如插入策略)用于优化设备的性能。传输列表是按设备提供的列表,因此列表的头和尾都存储在状态结构中。

以下示例提供了对驱动程序共享数据(如传输列表)有访问权限的多个线程。您必须标识共享数据,并且必须用互斥锁保护这些数据。有关互斥锁的更多详细信息,请参见第 3 章


示例 16–5 对块驱动程序的数据传输请求进行排队

static int
xxstrategy(struct buf *bp)
{
    struct xxstate *xsp;
    minor_t instance;
    instance = getminor(bp->b_edev);
    xsp = ddi_get_soft_state(statep, instance);
    /* ... */
    /* validate transfer request */
    /* ... */
    /*
     * Add the request to the end of the queue. Depending on the device, a sorting
     * algorithm, such as disksort(9F) can be used if it improves the
     * performance of the device.
     */
    mutex_enter(&xsp->mu);
    bp->av_forw = NULL;
    if (xsp->list_head) {
       /* Non-empty transfer list */
       xsp->list_tail->av_forw = bp;
       xsp->list_tail = bp;
    } else {
       /* Empty Transfer list */
       xsp->list_head = bp;
       xsp->list_tail = bp;
    }
    mutex_exit(&xsp->mu);
    /* Start the transfer if possible */
    (void) xxstart((caddr_t)xsp);
    return (0);
}

开始第一个传输

可实现排队的设备驱动程序通常具有 start() 例程。start() 可将下一个请求从队列中删除,并开始出入设备的数据传输。在以下示例中,不管设备状态是忙还是空闲,start() 将处理所有请求。


注 –

必须写入 start() 以便从任何上下文都可以对其进行调用。内核上下文中的策略例程与中断上下文中的中断例程都可以调用 start()


每次 strategy() 对请求排队时,将由 strategy(9E) 调用 start(),以便可以启动空闲设备。如果设备忙,则 start() 立即返回。

在处理程序从声明的中断返回之前,也可以由中断处理程序调用 start(),以便可以为非空队列提供服务。如果队列是空的,则 start() 立即返回。

由于 start() 是一个专用驱动程序例程,因此 start() 可以采用任何参数并返回任何类型。将写入以下代码样例用作 DMA 回调,尽管没有显示那一部分。相应地,此示例必须采用 caddr_t 作为参数并返回 int。有关 DMA 回调例程的更多信息,请参见处理资源分配故障


示例 16–6 开始块驱动程序的第一个数据请求

static int
xxstart(caddr_t arg)
{
    struct xxstate *xsp = (struct xxstate *)arg;
    struct buf *bp;

    mutex_enter(&xsp->mu);
    /*
     * If there is nothing more to do, or the device is
     * busy, return.
     */
    if (xsp->list_head == NULL || xsp->busy) {
       mutex_exit(&xsp->mu);
       return (0);
    }
    xsp->busy = 1;
    /* Get the first buffer off the transfer list */
    bp = xsp->list_head;
    /* Update the head and tail pointer */
    xsp->list_head = xsp->list_head->av_forw;
    if (xsp->list_head == NULL)
       xsp->list_tail = NULL;
    bp->av_forw = NULL;
    mutex_exit(&xsp->mu);
    /*
     * If the device has power manageable components,
     * mark the device busy with pm_busy_components(9F),
     * and then ensure that the device
     * is powered up by calling pm_raise_power(9F).
     *
     * Set up DMA resources with ddi_dma_alloc_handle(9F) and
     * ddi_dma_buf_bind_handle(9F).
     */
    xsp->bp = bp;
    ddi_put32(xsp->data_access_handle, &xsp->regp->dma_addr,
        cookie.dmac_address);
    ddi_put32(xsp->data_access_handle, &xsp->regp->dma_size,
         (uint32_t)cookie.dmac_size);
    ddi_put8(xsp->data_access_handle, &xsp->regp->csr,
         ENABLE_INTERRUPTS | START_TRANSFER);
    return (0);
}

处理中断的设备

中断例程与异步版本类似,只是增加了对 start() 的调用并删除了对 cv_signal(9F) 的调用。


示例 16–7 异步中断的块驱动程序例程

static u_int
xxintr(caddr_t arg)
{
    struct xxstate *xsp = (struct xxstate *)arg;
    struct buf *bp;
    uint8_t status;
    mutex_enter(&xsp->mu);
    status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr);
    if (!(status & INTERRUPTING)) {
        mutex_exit(&xsp->mu);
        return (DDI_INTR_UNCLAIMED);
    }
    /* Get the buf responsible for this interrupt */
    bp = xsp->bp;
    xsp->bp = NULL;
    /*
     * This example is for a simple device which either
     * succeeds or fails the data transfer, indicated in the
     * command/status register.
     */
    if (status & DEVICE_ERROR) {
        /* failure */
        bp->b_resid = bp->b_bcount;
        bioerror(bp, EIO);
    } else {
        /* success */
        bp->b_resid = 0;
    }
    ddi_put8(xsp->data_access_handle, &xsp->regp->csr,
        CLEAR_INTERRUPT);
    /* The transfer has finished, successfully or not */
    biodone(bp);
    /*
     * If the device has power manageable components that were
     * marked busy in strategy(9F), mark them idle now with
     * pm_idle_component(9F)
     * Release any resources used in the transfer, such as DMA
     * resources (ddi_dma_unbind_handle(9F) and
     * ddi_dma_free_handle(9F)).
     *
     * Let the next I/O thread have access to the device.
     */
    xsp->busy = 0;
    mutex_exit(&xsp->mu);
    (void) xxstart((caddr_t)xsp);
    return (DDI_INTR_CLAIMED);
}

dump()print() 入口点

本节讨论 dump(9E)print(9E) 入口点。

dump() 入口点(块驱动程序)

dump(9E) 入口点用于在系统发生故障时将虚拟地址空间的一部分直接复制到指定的设备。在检查点操作期间,还可以使用 dump() 将内核状态复制到磁盘。有关更多信息,请参见 cpr(7)dump(9E) 手册页。由于在检查点操作期间中断被禁用,因此该入口点必须能够在不使用中断的情况下执行此操作。

int dump(dev_t dev, caddr_t addr, daddr_t blkno, int nblk)

其中:

dev

接收转储的设备的设备编号。

addr

开始转储的基本内核虚拟地址。

blkno

开始转储的块。

nblk

转储的块的编号。

转储依赖于现有的驱动程序是否工作正常。

print() 入口点(块驱动程序)

int print(dev_t dev, char *str)

系统调用 print(9E) 入口点以显示有关已检测到的异常的消息。print(9E) 应调用 cmn_err(9F) 以代表系统将消息发布到控制台。以下示例说明了一个典型的 print() 入口点。

static int
 xxprint(dev_t dev, char *str)
 {
     cmn_err(CE_CONT, “xx: %s\n”, str);
     return (0);
 }

磁盘设备驱动程序

磁盘设备是一类重要的块设备驱动程序。

磁盘 ioctl

Solaris 磁盘驱动程序至少需要支持一组特定于 Solaris 磁盘驱动程序的 ioctl 命令。在 dkio(7I) 手册页中指定了这些 I/O 控制。磁盘 I/O 控制用于将磁盘信息传入/传出设备驱动程序。磁盘实用程序命令(如 format(1M)newfs(1M))支持 Solaris 磁盘设备。强制性的 Sun 磁盘 I/O 控制如下:

DKIOCINFO

返回描述磁盘控制器的信息

DKIOCGAPART

返回磁盘的分区映射

DKIOCSAPART

设置磁盘的分区映射

DKIOCGGEOM

返回磁盘的几何参数

DKIOCSGEOM

设置磁盘的几何参数

DKIOCGVTOC

返回磁盘的卷目录

DKIOCSVTOC

设置磁盘的卷目录

磁盘性能

Solaris DDI/DKI 提供了优化 I/O 传输以提高文件系统性能的工具。它是一种管理 I/O 请求列表以便优化文件系统磁盘访问的机制。有关对 I/O 请求进行排队的说明,请参见异步数据传输(块驱动程序)

diskhd 结构用于管理 I/O 请求链接表。

struct diskhd {
    long     b_flags;         /* not used, needed for consistency*/
    struct   buf *b_forw,    *b_back;       /* queue of unit queues */
    struct   buf *av_forw,    *av_back;    /* queue of bufs for this unit */
    long     b_bcount;            /* active flag */
};

diskhd 数据结构具有驱动程序可处理的两个 buf 指针。av_forw 指针指向第一个活动 I/O 请求。第二个指针 av_back 指向列表中的上一个活动请求。

一个指向此结构的指针以及一个指向要处理的当前 buf 结构的指针作为参数传递给 disksort(9F)disksort() 例程对 buf 请求排序以优化磁盘查找。然后此例程将 buf 指针插入 diskhd 列表。disksort() 程序将 buf 结构的 b_resid 中的值用作排序关键字。驱动程序负责设置此值。大多数 Sun 磁盘驱动程序使用柱面组作为排序关键字。此方法优化了文件系统读前访问。

将数据添加到 diskhd 列表后,设备需要传输这些数据。如果设备未忙于处理请求,则 xxstart() 例程会将第一个 buf 结构拉出 diskhd 列表并开始传输。

如果设备正忙,则驱动程序会从 xxstrategy() 入口点返回。当硬件执行完数据传输时,便会产生中断。随后会调用驱动程序的中断例程为设备提供服务。在提供中断服务后,驱动程序可以调用 start() 例程来处理 diskhd 列表中的下一个 buf 结构。

第 17 章 SCSI 目标驱动程序

Solaris DDI/DKI 将 SCSI 设备的软件接口分成以下两个主要部分:目标驱动程序和主机总线适配器 (host bus adapter, HBA) 驱动程序。目标驱动程序指 SCSI 总线上的设备(如磁盘或磁带机)的驱动程序。主机总线适配器驱动程序指主机上的 SCSI 控制器的驱动程序。SCSA 定义了这两个组件之间的接口。本章仅讨论目标驱动程序。有关主机总线适配器驱动程序的信息,请参见第 18 章


注 –

术语“主机总线适配器”和 "HBA" 等效于 SCSI 规范中定义的“主机适配器”。


本章介绍有关以下主题的信息:

目标驱动程序介绍

目标驱动程序可以为字符设备驱动程序,也可以为块设备驱动程序,具体取决于设备。磁带机的驱动程序通常为字符设备驱动程序,而磁盘则由块设备驱动程序处理。本章介绍如何编写 SCSI 目标驱动程序。本章还讨论了 SCSA 对 SCSI 目标设备的块驱动程序和字符驱动程序的其他要求。

以下参考文档提供了目标驱动程序和主机总线适配器驱动程序的设计者需要的补充信息。

Small Computer System Interface 2 (SCSI-2), ANSI/NCITS X3.131-1994》,Global Engineering Documents 发布,1998 年,ISBN 1199002488。

The Basics of SCSI》(第四版),ANCOT Corporation 出版,1998 年,ISBN 0963743988。

另请参阅硬件供应商所提供的关于目标设备的 SCSI 命令规范。

Sun 公用 SCSI 体系结构概述

Sun 公用 SCSI 体系结构 (Sun Common SCSI Architecture, SCSA) 是 Solaris DDI/DKI 编程接口,用于将 SCSI 命令从目标驱动程序传输到主机总线适配器驱动程序。该接口与主机总线适配器硬件的类型、平台、处理器体系结构以及通过该接口传输的 SCSI 命令无关。

只要符合 SCSA,目标驱动程序便可以将 SCSI 命令传送给目标设备,而无需了解主机总线适配器的硬件实现。

SCSA 在概念上将生成 SCSI 命令与通过 SCSI 总线传输命令和数据这两个过程分离开来。该体系结构定义了高级别软件组件与低级别软件组件之间的软件接口。较高级别的软件组件由一个或多个 SCSI 目标驱动程序组成,这些驱动程序可将 I/O 请求转换为适用于外围设备的 SCSI 命令。以下示例说明了 SCSI 体系结构。

图 17–1 SCSA 块图

图中显示了 Sun 公用 SCSI 体系结构相对于操作系统中的 SCSI 驱动程序的角色。

较低级别的软件组件由 SCSA 接口层和一个或多个主机总线适配器驱动程序组成。目标驱动程序负责生成执行所需功能需要的正确 SCSI 命令,并负责处理结果。

常规控制流程

以下步骤介绍读取或写入请求的常规控制流程(假定没有发生传输错误)。

  1. 调用目标驱动程序的 read(9E)write(9E) 入口点。使用 physio(9F) 锁定内存,准备 buf 结构并调用策略例程。

  2. 目标驱动程序的 strategy(9E) 例程将检查请求。然后,strategy() 通过使用 scsi_init_pkt(9F) 来分配 scsi_pkt(9S)。目标驱动程序初始化包,并使用 scsi_setup_cdb(9F) 函数设置 SCSI 命令描述符块 (command descriptor block, CDB)。目标驱动程序还指定超时。然后,该驱动程序提供一个指向回调函数的指针。完成命令后,主机总线适配器驱动程序将调用该回调函数。buf(9S) 指针应保存在 SCSI 包的目标专用空间中。

  3. 目标驱动程序使用 scsi_transport(9F) 将包提交给主机总线适配器驱动程序。然后,目标驱动程序可以自由接受其他请求。目标驱动程序不应在包的传输过程中对其进行访问。如果主机总线适配器驱动程序或目标支持排队,则可以在传输包时提交新请求。

  4. 一旦 SCSI 总线空闲而且目标不繁忙,主机总线适配器驱动程序便会选择该目标并传递 CDB。目标驱动程序将执行命令。然后,目标执行所请求的数据传输。

  5. 目标发送完成状态并且命令完成后,主机总线适配器驱动程序会通知目标驱动程序。要执行通知,主机应调用 SCSI 包中指定的完成函数。此时,主机总线适配器驱动程序不再负责处理该包,同时目标驱动程序重新获得了对该包的拥有权。

  6. SCSI 包的完成例程将分析返回的信息。然后,完成例程会确定 SCSI 操作是否成功。如果出现故障,目标驱动程序将再次调用 scsi_transport(9F) 以重试命令。如果主机总线适配器驱动程序不支持自动请求检测,则目标驱动程序必须在出现检查条件时,提交请求检测包才能检索检测数据。

  7. 成功完成上述操作后或者如果无法重试命令,目标驱动程序将调用 scsi_destroy_pkt(9F)scsi_destroy_pkt () 将同步数据。然后,scsi_destroy_pkt() 释放包。如果在释放包之前目标驱动程序需要访问数据,则调用 scsi_sync_pkt(9F)

  8. 最后,目标驱动程序将通知请求应用程序读取或写入事务已完成。该通知是在执行了字符设备的驱动程序中的 read(9E) 入口点并返回后进行的。否则,会通过 biodone(9F) 间接发出通知。

SCSA 允许以重叠方式和排队方式在进程的各个点执行多个此类操作。在该模型中,系统资源由主机总线适配器驱动程序负责管理。借助软件接口并使用不同复杂程度的 SCSI 总线适配器,可以在主机总线适配器驱动程序中执行目标驱动程序函数。

SCSA 函数

SCSA 定义了多种函数,用于管理资源的分配和释放、控制状态的检测和设置以及 SCSI 命令的传输。下表中列出了这些函数。

表 17–1 标准 SCSA 函数

函数名 

类别 

scsi_abort(9F)

错误处理 

scsi_alloc_consistent_buf(9F)

 

scsi_destroy_pkt(9F)

 

scsi_dmafree(9F)

 

scsi_free_consistent_buf(9F)

 

scsi_ifgetcap(9F)

传输信息和控制 

scsi_ifsetcap(9F)

 

scsi_init_pkt(9F)

资源管理 

scsi_poll(9F)

轮询 I/O 

scsi_probe(9F)

探测器函数 

scsi_reset(9F)

 

scsi_setup_cdb(9F)

CDB 初始化函数 

scsi_sync_pkt(9F)

 

scsi_transport(9F)

命令传输 

scsi_unprobe(9F)

 


注 –

如果驱动程序需要使用 SCSI-1 设备,请使用 makecom(9F)


硬件配置文件

由于 SCSI 设备不是自标识设备,因此目标驱动程序需要硬件配置文件。有关详细信息,请参见 driver.conf(4)scsi_free_consistent_buf(9F) 手册页。以下是典型的配置文件:

    name="xx" class="scsi" target=2 lun=0;

系统将在自动配置期间读取该文件。系统使用 class 属性标识驱动程序可能存在的父驱动程序。然后,系统尝试将该驱动程序连接至类为 scsi 的任何父驱动程序。所有主机总线适配器驱动程序都属于此类。首选使用 class 属性,而不是 parent 属性。采用此方法,任何在指定 targetlun ID 中查找预期设备的主机总线适配器驱动程序都可以连接至目标。目标驱动程序负责验证其 probe(9E) 例程中的类。

声明和数据结构

目标驱动程序必须包括头文件 <sys/scsi/scsi.h>

SCSI 目标驱动程序必须使用以下命令生成二进制模块:

ld -r xx xx.o -N"misc/scsi"

scsi_device 结构

调用 probe(9E)attach(9E) 例程之前,主机总线适配器驱动程序将为目标驱动程序分配并初始化 scsi_device(9S) 结构。此结构可存储有关每个 SCSI 逻辑单元的信息,包括指向信息区(包含通用信息和特定于设备的信息)的指针。对于连接到系统的每个逻辑单元,都存在一个 scsi_device(9S) 结构。目标驱动程序可以通过调用 ddi_get_driver_private(9F) 来检索指向此结构的指针。


注意 – 注意 –

由于主机总线适配器驱动程序使用目标设备的 dev_info 结构中的专用字段,因此目标驱动程序不能使用 ddi_set_driver_private(9F)


scsi_device(9S) 结构包含以下字段:

struct scsi_device {
    struct scsi_address           sd_address;    /* opaque address */
    dev_info_t                    *sd_dev;       /* device node */
    kmutex_t                      sd_mutex;
    void                          *sd_reserved;
    struct scsi_inquiry           *sd_inq;
    struct scsi_extended_sense    *sd_sense;
    caddr_t                       sd_private;
};

其中:

sd_address

为了进行 SCSI 资源分配而传递给例程的数据结构。

sd_dev

指向目标的 dev_info 结构的指针。

sd_mutex

供目标驱动程序使用的互斥锁。此互斥锁由主机总线适配器驱动程序初始化,并可被目标驱动程序用作每设备互斥锁。请勿在调用 scsi_transport(9F)scsi_poll(9F) 期间持有此互斥锁。有关互斥锁的更多信息,请参见第 3 章

sd_inq

目标设备的 SCSI 查询数据的指针。scsi_probe(9F) 例程将分配缓冲区,使用查询数据填充该缓冲区,并将该缓冲区连接到此字段。

sd_sense

指向用于包含设备中的 SCSI 请求检测数据的缓冲区的指针。目标驱动程序必须分配和管理此缓冲区。请参见attach() 入口点(SCSI 目标驱动程序)

sd_private

供目标驱动程序使用的指针字段。此字段通常用于存储指向专用目标驱动程序状态结构的指针。

scsi_pkt 结构(目标驱动程序)

scsi_pkt 结构包含以下字段:

struct scsi_pkt {
    opaque_t  pkt_ha_private;         /* private data for host adapter */
    struct scsi_address pkt_address;  /* destination packet is for */
    opaque_t  pkt_private;            /* private data for target driver */
    void     (*pkt_comp)(struct scsi_pkt *);  /* completion routine */
    uint_t   pkt_flags;               /* flags */
    int      pkt_time;                /* time allotted to complete command */
    uchar_t  *pkt_scbp;               /* pointer to status block */
    uchar_t  *pkt_cdbp;               /* pointer to command block */
    ssize_t  pkt_resid;               /* data bytes not transferred */
    uint_t   pkt_state;               /* state of command */
    uint_t   pkt_statistics;          /* statistics */
    uchar_t  pkt_reason;              /* reason completion called */
};

其中:

pkt_address

scsi_init_pkt(9F) 设置的目标设备的地址。

pkt_private

用于存储目标驱动程序的专用数据的位置。pkt_private 通常用于保存命令的 buf(9S) 指针。

pkt_comp

完成例程的地址。主机总线适配器驱动程序将在传输命令后调用此例程。传输命令并不表示命令已成功。目标可能处于繁忙状态。另一种可能性是在经过超时时间段之前目标未响应。请参见 pkt_time 字段的说明。目标驱动程序必须在该字段中提供有效值。如果不需要通知驱动程序,则该值可以为 NULL


注 –

有两种不同的 SCSI 回调例程。pkt_comp 字段标识完成回调例程,该例程将在主机总线适配器完成其处理时调用。此外,还提供了资源回调例程,该例程将在当前尚不可用资源可能可用时调用。请参见 scsi_init_pkt(9F) 手册页。


pkt_flags

提供其他控制信息,例如,在没有断开连接权限的情况下传输命令 (FLAG_NODISCON),或禁用回调 (FLAG_NOINTR)。有关详细信息,请参见 scsi_pkt(9S) 手册页。

pkt_time

超时值(以秒为单位)。如果命令在该时间内未完成,则主机总线适配器将调用完成例程,并将 pkt_reason 设置为 CMD_TIMEOUT。目标驱动程序应将该字段设置为大于命令可能需要的最长时间。如果超时值为零,则不请求超时。超时从在 SCSI 总线上传输命令时开始。

pkt_scbp

指向 SCSI 状态完成块的指针。该字段由主机总线适配器驱动程序填充。

pkt_cdbp

指向 SCSI 命令描述符块(要发送到目标设备的实际命令)的指针。主机总线适配器驱动程序不会解释该字段。目标驱动程序必须使用目标设备可以处理的命令填充该字段。

pkt_resid

剩余操作。pkt_resid 字段有两种不同的用途,具体取决于如何使用 pkt_resid。使用 pkt_resid 为命令 scsi_init_pkt(9F) 分配 DMA 资源时,pkt_resid 指示不可分配的字节数。由于 DMA 硬件具有分散/集中限制或其他设备限制,因此可能无法分配 DMA 资源。传输命令后,pkt_resid 指示不可传输的数据字节数。该字段由主机总线适配器驱动程序在调用完成例程之前填充。

pkt_state

指示命令的状态。主机总线适配器驱动程序在命令执行过程中填充该字段。该字段的每一位分别根据以下五种命令状态设置:

  • STATE_GOT_BUS-已获取总线

  • STATE_GOT_TARGET-已选定目标

  • STATE_SENT_CMD-已发送命令

  • STATE_XFERRED_DATA-已传输数据(如果适用)

  • STATE_GOT_STATUS-已从设备接收状态

pkt_statistics

包含与传输相关的统计信息(由主机总线适配器驱动程序设置)。

pkt_reason

提供调用完成例程的原因。完成例程会对该字段进行解码。然后,执行相应的操作。如果命令完成(即未发生传输错误),则该字段将设置为 CMD_CMPLT。如果该字段中存在其他值,则指示发生了错误。完成命令后,目标驱动程序应检查 pkt_scbp 字段以查看条件状态。有关更多信息,请参见 scsi_pkt(9S) 手册页。

SCSI 目标驱动程序的自动配置

SCSI 目标驱动程序必须实现标准自动配置例程 _init(9E)_fini(9E)_info(9E)。有关更多信息,请参见可装入驱动程序接口

此外,还需要以下例程,但这些例程必须执行特定的 SCSI 和 SCSA 处理:

probe() 入口点(SCSI 目标驱动程序)

SCSI 目标设备不是自标识设备,因此目标驱动程序必须具有 probe(9E) 例程。该例程必须确定所需类型的设备是否存在以及是否正在响应。

probe(9E) 例程的常规结构和返回代码与其他设备驱动程序的结构和返回代码相同。SCSI 目标驱动程序必须在其 probe(9E) 入口点中使用 scsi_probe(9F) 例程。scsi_probe(9F) 向设备发送 SCSI 查询命令并返回指示结果的代码。如果 SCSI 查询命令成功,则 scsi_probe(9F) 将分配 scsi_inquiry(9S) 结构并使用设备的查询数据填充该结构。从 scsi_probe(9F) 返回之后,scsi_device(9S) 结构的 sd_inq 字段将指向此 scsi_inquiry(9S) 结构。

由于 probe(9E) 必须是无状态的,因此目标驱动程序必须在 probe(9E) 返回之前调用 scsi_unprobe(9F),即使 scsi_probe(9F) 失败也是如此。

示例 17–1 显示了典型的 probe(9E) 例程。该示例中的例程从其 dev_info 结构的专用字段中检索 scsi_device(9S) 结构。该例程还检索设备的 SCSI 目标和逻辑单元号,以便列显在消息中。然后,probe(9E) 例程将调用 scsi_probe(9F) 以验证预期设备(在本例中为打印机)是否存在。

如果成功,scsi_probe(9F) 会将 scsi_inquiry(9S) 结构中设备的 SCSI 查询数据连接到 scsi_device(9S) 结构的 sd_inq 字段。然后,驱动程序便可以确定设备类型是否为打印机,相关情况将在 inq_dtype 字段中报告。如果设备是打印机,则使用 scsi_log(9F) 报告类型,并使用 scsi_dname(9F) 将设备类型转换为字符串。


示例 17–1 SCSI 目标驱动程序 probe(9E) 例程

static int
xxprobe(dev_info_t *dip)
{
    struct scsi_device *sdp;
    int rval, target, lun;
    /*
     * Get a pointer to the scsi_device(9S) structure
     */
    sdp = (struct scsi_device *)ddi_get_driver_private(dip);

    target = sdp->sd_address.a_target;
    lun = sdp->sd_address.a_lun;
    /*
     * Call scsi_probe(9F) to send the Inquiry command. It will
     * fill in the sd_inq field of the scsi_device structure.
     */
    switch (scsi_probe(sdp, NULL_FUNC)) {
    case SCSIPROBE_FAILURE:
    case SCSIPROBE_NORESP:
    case SCSIPROBE_NOMEM:
       /*
        * In these cases, device might be powered off,
        * in which case we might be able to successfully
        * probe it at some future time - referred to
        * as `deferred attach'.
        */
        rval = DDI_PROBE_PARTIAL;
        break;
    case SCSIPROBE_NONCCS:
    default:
        /*
         * Device isn't of the type we can deal with,
         * and/or it will never be usable.
         */
        rval = DDI_PROBE_FAILURE;
        break;
    case SCSIPROBE_EXISTS:
        /*
         * There is a device at the target/lun address. Check
         * inq_dtype to make sure that it is the right device
         * type. See scsi_inquiry(9S)for possible device types.
         */
        switch (sdp->sd_inq->inq_dtype) {
        case DTYPE_PRINTER:
        scsi_log(sdp, "xx", SCSI_DEBUG,
           "found %s device at target%d, lun%d\n",
            scsi_dname((int)sdp->sd_inq->inq_dtype),
            target, lun);
        rval = DDI_PROBE_SUCCESS;
        break;
        case DTYPE_NOTPRESENT:
        default:
        rval = DDI_PROBE_FAILURE;
        break;     
        }    
    }
    scsi_unprobe(sdp);
    return (rval);
}

更全面的 probe(9E) 例程可以检查 scsi_inquiry(9S) 以确保设备是特定驱动程序期望的类型。

attach() 入口点(SCSI 目标驱动程序)

probe(9E) 例程验证预期设备是否存在后,将调用 attach(9E)attach() 执行以下任务:

SCSI 目标驱动程序需要再次调用 scsi_probe(9F),以检索设备的查询数据。该驱动程序还必须创建 SCSI 请求检测包。如果连接成功,则 attach() 函数不应调用 scsi_unprobe(9F)

以下三个例程可用于创建请求检测包:scsi_alloc_consistent_buf(9F)scsi_init_pkt(9F)scsi_setup_cdb(9F)scsi_alloc_consistent_buf(9F) 分配适用于一致 DMA 的缓冲区 。然后,scsi_alloc_consistent_buf() 返回指向 buf(9S) 结构的指针。一致缓冲区的优点在于无需显式同步数据。换句话说,目标驱动程序可以在回调之后访问数据。必须使用检测缓冲区的地址初始化设备的 scsi_device(9S) 结构的 sd_sense 元素。scsi_init_pkt(9F) 创建并部分初始化 scsi_pkt(9S) 结构。scsi_setup_cdb(9F) 创建 SCSI 命令描述符块,此时是通过创建 SCSI 请求检测命令来实现。

请注意,SCSI 设备不是自标识设备,并且没有 reg 属性。因此,驱动程序必须设置 pm-hardware-state 属性。设置 pm-hardware-state 将会通知框架需要暂停该设备然后将其恢复。

以下示例给出了 SCSI 目标驱动程序的 attach() 例程。


示例 17–2 SCSI 目标驱动程序 attach(9E) 例程

static int
xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
    struct xxstate         *xsp;
    struct scsi_pkt        *rqpkt = NULL;
    struct scsi_device     *sdp;
    struct buf         *bp = NULL;
    int            instance;
    instance = ddi_get_instance(dip);
    switch (cmd) {
        case DDI_ATTACH:
        break;
        case DDI_RESUME:
        /* For information, see the "Directory Memory Access (DMA)" */
        /* chapter in this book. */
        default:
        return (DDI_FAILURE);
    }
    /*
     * Allocate a state structure and initialize it.
     */
    xsp = ddi_get_soft_state(statep, instance);
    sdp = (struct scsi_device *)ddi_get_driver_private(dip);
    /*
     * Cross-link the state and scsi_device(9S) structures.
     */
    sdp->sd_private = (caddr_t)xsp;
    xsp->sdp = sdp;
    /*
     * Call scsi_probe(9F) again to get and validate inquiry data.
     * Allocate a request sense buffer. The buf(9S) structure
     * is set to NULL to tell the routine to allocate a new one.
     * The callback function is set to NULL_FUNC to tell the
     * routine to return failure immediately if no
     * resources are available.
     */
    bp = scsi_alloc_consistent_buf(&sdp->sd_address, NULL,
    SENSE_LENGTH, B_READ, NULL_FUNC, NULL);
    if (bp == NULL)
        goto failed;
    /*
     * Create a Request Sense scsi_pkt(9S) structure.
     */
    rqpkt = scsi_init_pkt(&sdp->sd_address, NULL, bp,
    CDB_GROUP0, 1, 0, PKT_CONSISTENT, NULL_FUNC, NULL);
    if (rqpkt == NULL)
        goto failed;
    /*
     * scsi_alloc_consistent_buf(9F) returned a buf(9S) structure.
     * The actual buffer address is in b_un.b_addr.
     */
    sdp->sd_sense = (struct scsi_extended_sense *)bp->b_un.b_addr;
    /*
     * Create a Group0 CDB for the Request Sense command
     */
    if (scsi_setup_cdb((union scsi_cdb *)rqpkt->pkt_cdbp,
        SCMD_REQUEST_SENSE, 0, SENSE__LENGTH, 0) == 0)
         goto failed;;
    /*
     * Fill in the rest of the scsi_pkt structure.
     * xxcallback() is the private command completion routine.
     */
    rqpkt->pkt_comp = xxcallback;
    rqpkt->pkt_time = 30; /* 30 second command timeout */
    rqpkt->pkt_flags |= FLAG_SENSING;
    xsp->rqs = rqpkt;
    xsp->rqsbuf = bp;
    /*
     * Create minor nodes, report device, and do any other initialization. */
     * Since the device does not have the 'reg' property,
     * cpr will not call its DDI_SUSPEND/DDI_RESUME entries.
     * The following code is to tell cpr that this device
     * needs to be suspended and resumed.
     */
    (void) ddi_prop_update_string(device, dip,
     "pm-hardware-state", "needs-suspend-resume");
    xsp->open = 0;
    return (DDI_SUCCESS);
failed:
    if (bp)
        scsi_free_consistent_buf(bp);
    if (rqpkt)
        scsi_destroy_pkt(rqpkt);
    sdp->sd_private = (caddr_t)NULL;
    sdp->sd_sense = NULL;
    scsi_unprobe(sdp);
    /* Free any other resources, such as the state structure. */
    return (DDI_FAILURE);
}

detach() 入口点(SCSI 目标驱动程序)

detach(9E) 入口点与 attach(9E) 是反向的入口点。detach() 必须释放在 attach() 中分配的所有资源。如果成功,则 detach 应调用 scsi_unprobe(9F)。以下示例给出了目标驱动程序的 detach() 例程。


示例 17–3 SCSI 目标驱动程序 detach(9E) 例程

static int
xxdetach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
    struct xxstate *xsp;
    switch (cmd) {
    case DDI_DETACH:
      /*
       * Normal detach(9E) operations, such as getting a
       * pointer to the state structure
       */
      scsi_free_consistent_buf(xsp->rqsbuf);
      scsi_destroy_pkt(xsp->rqs);
      xsp->sdp->sd_private = (caddr_t)NULL;
      xsp->sdp->sd_sense = NULL;
      scsi_unprobe(xsp->sdp);
      /*
       * Remove minor nodes.
       * Free resources, such as the state structure and properties.
       */
          return (DDI_SUCCESS);
    case DDI_SUSPEND:
      /* For information, see the "Directory Memory Access (DMA)" */
      /* chapter in this book. */
    default:
      return (DDI_FAILURE);
    }
}

getinfo() 入口点(SCSI 目标驱动程序)

SCSI 目标驱动程序的 getinfo(9E) 例程与其他驱动程序的相应例程基本相同(有关 DDI_INFO_DEVT2INSTANCE 案例的更多信息,请参见 getinfo() 入口点)。但是,如果是 getinfo() 例程的 DDI_INFO_DEVT2DEVINFO,则目标驱动程序必须返回指向其 dev_info 节点的指针。该指针可以保存在驱动程序状态结构中,也可以从 scsi_device(9S) 结构的 sd_dev 字段中检索。以下示例给出了替换 SCSI 目标驱动程序 getinfo() 代码段。


示例 17–4 替代 SCSI 目标驱动程序 getinfo() 代码段

case DDI_INFO_DEVT2DEVINFO:
    dev = (dev_t)arg;
    instance = getminor(dev);
    xsp = ddi_get_soft_state(statep, instance);
    if (xsp == NULL)
        return (DDI_FAILURE);
    *result = (void *)xsp->sdp->sd_dev;
    return (DDI_SUCCESS);

资源分配

要向设备发送 SCSI 命令,目标驱动程序必须创建并初始化 scsi_pkt(9S) 结构。然后,必须将该结构传递到主机总线适配器驱动程序。

scsi_init_pkt() 函数

scsi_init_pkt(9F) 例程分配 scsi_pkt(9S) 结构并将该结构调整归零。scsi_init_pkt() 还设置指向 pkt_private*pkt_scbp*pkt_cdbp 的指针。此外,scsi_init_pkt() 还提供回调机制来处理资源不可用的情况。该函数的语法如下:

struct scsi_pkt *scsi_init_pkt(struct scsi_address *ap,
     struct scsi_pkt *pktp, struct buf *bp, int cmdlen,
     int statuslen, int privatelen, int flags,
     int (*callback)(caddr_t), caddr_t arg)

其中:

ap

指向 scsi_address 结构的指针。ap 是设备的 scsi_device(9S) 结构的 sd_address 字段。

pktp

指向要初始化的 scsi_pkt(9S) 结构的指针。如果将该指针设置为 NULL,则会分配一个新包。

bp

指向 buf(9S) 结构的指针。如果该指针为具有有效字节计数的非 null 值,则会分配 DMA 资源。

cmdlen

SCSI 命令描述符块的长度(以字节为单位)。

statuslen

SCSI 状态完成块的必需长度(以字节为单位)。

privatelen

要为 pkt_private 字段分配的字节数。

flags

标志集:

  • PKT_CONSISTENT-如果 DMA 缓冲区是使用 scsi_alloc_consistent_buf(9F) 分配的,则必须设置该位。在这种情况下,主机总线适配器驱动程序将保证在执行目标驱动程序的命令完成回调之前正确同步数据传输。

  • PKT_DMA_PARTIAL-如果驱动程序接受部分 DMA 映射,则可以设置该位。如果设置了该位,scsi_init_pkt(9F) 将分配 DMA 资源并设置 DDI_DMA_PARTIAL 标志。可以返回 scsi_pkt(9S) 结构的 pkt_resid 字段的返回值可以是非零的剩余值。非零值表示 scsi_init_pkt(9F) 无法分配的 DMA 资源字节数。

callback

指定资源不可用时要执行的操作。如果设置为 NULL_FUNCscsi_init_pkt(9F) 将立即返回值 NULL。如果设置为 SLEEP_FUNC,则在资源可用之前,scsi_init_pkt() 不会返回。当资源可能可用时,会将任何其他有效的内核地址解释为要调用的函数的地址。

arg

要传递给回调函数的参数。

传输之前,scsi_init_pkt() 例程将同步数据。如果驱动程序需要在传输后访问数据,则驱动程序应调用 scsi_sync_pkt(9F) 以刷新任何中间高速缓存。可以使用 scsi_sync_pkt() 例程来同步所有高速缓存的数据。

scsi_sync_pkt() 函数

如果在更改数据之后目标驱动程序需要重新提交包,则必须在调用 scsi_transport(9F) 之前调用 scsi_sync_pkt(9F)。但是,如果目标驱动程序不需要访问数据,则在传输之后不需要调用 scsi_sync_pkt()

scsi_destroy_pkt() 函数

如有必要,scsi_destroy_pkt(9F) 例程将同步与包关联的任何剩余高速缓存数据。然后,该例程会释放包以及关联的命令、状态和目标驱动程序专用的数据区。应在命令完成例程中调用该例程。

scsi_alloc_consistent_buf() 函数

对于大多数 I/O 请求,驱动程序不直接访问传递到驱动程序入口点的数据缓冲区。该缓冲区仅传递到 scsi_init_pkt(9F)。如果某个驱动程序发送的 SCSI 命令是针对该驱动程序本身检查的缓冲区,那么这些缓冲区应该支持 DMA。SCSI 请求检测命令就是一个很好的示例。scsi_alloc_consistent_buf(9F) 例程分配 buf(9S) 结构和适用于 DMA 一致操作的数据缓冲区。HBA 首先会执行任何必需的缓冲区同步,然后再执行命令完成回调。


注 –

scsi_alloc_consistent_buf(9F) 将使用珍贵的系统资源。因此,应有节制地使用 scsi_alloc_consistent_buf()


scsi_free_consistent_buf() 函数

scsi_free_consistent_buf(9F) 释放 buf(9S) 结构和使用 scsi_alloc_consistent_buf(9F) 分配的关联数据缓冲区。有关示例,请参见attach() 入口点(SCSI 目标驱动程序)detach() 入口点(SCSI 目标驱动程序)

生成和传输命令

主机总线适配器驱动程序负责向设备传输命令。此外,该驱动程序还负责处理低级别的 SCSI 协议。scsi_transport(9F) 例程将包提交给主机总线适配器驱动程序以进行传输。目标驱动程序负责创建有效的 scsi_pkt(9S) 结构。

生成命令

scsi_init_pkt(9F) 例程可为 SCSI CDB 分配空间,在必要时分配 DMA 资源以及设置 pkt_flags 字段,如以下示例所示:

pkt = scsi_init_pkt(&sdp->sd_address, NULL, bp,
CDB_GROUP0, 1, 0, 0, SLEEP_FUNC, NULL);

该示例在按传递的 buf(9S) 结构指针中的指定分配 DMA 资源的同时创建了一个新包。为 Group 0(6 字节)命令分配了 SCSI CDB。pkt_flags 字段被设置为零,但没有为 pkt_private 字段分配空间。由于设置了 SLEEP_FUNC 参数,对 scsi_init_pkt(9F) 的调用将会在当前没有可用资源的情况下无限期等待资源。

下一步是使用 scsi_setup_cdb(9F) 函数初始化 SCSI CDB。

    if (scsi_setup_cdb((union scsi_cdb *)pkt->pkt_cdbp,
     SCMD_READ, bp->b_blkno, bp->b_bcount >> DEV_BSHIFT, 0) == 0)
     goto failed;

该示例将生成 Group 0 命令描述符块。该示例按如下所示填充 pkt_cdbp 字段:


注 –

scsi_setup_cdb(9F) 不支持在 SCSI 命令块的第 1 个字节的 5-7 位中设置目标设备的逻辑单元号 (logical unit number, LUN)。此要求由 SCSI-1 定义。对于需要在命令块中设置 LUN 位的 SCSI-1 设备,请使用 makecom_g0(9F) 或某些等效的函数,而不是使用 scsi_setup_cdb(9F)


初始化 SCSI CDB 之后,应初始化包中的三个其他字段,并在状态结构中存储为指向包的指针。

pkt->pkt_private = (opaque_t)bp;
pkt->pkt_comp = xxcallback;
pkt->pkt_time = 30;
xsp->pkt = pkt;

buf(9S) 指针保存在 pkt_private 字段中,以备将来在完成例程中使用。

设置目标功能

目标驱动程序使用 scsi_ifsetcap(9F) 设置主机适配器驱动程序的功能。上限是一个名称/值对,由一个以 null 结尾的字符串和一个整数值组成。可以使用 scsi_ifgetcap(9F) 检索功能的当前值。scsi_ifsetcap(9F) 允许为总线上的所有目标设置功能。

但是,通常建议不要设置非目标驱动程序拥有的目标的功能。并非所有 HBA 驱动程序都支持这种做法。缺省情况下,某些功能(如断开连接和同步)可以由 HBA 驱动程序设置。其他功能可能需要由目标驱动程序显式设置。例如,Wide-xfer 和标记排队功能必须由目标驱动程序设置。

传输命令

填充 scsi_pkt(9S) 结构之后,应使用 scsi_transport(9F) 将该结构提交给总线适配器驱动程序:

    if (scsi_transport(pkt) != TRAN_ACCEPT) {
     bp->b_resid = bp->b_bcount;
     bioerror(bp, EIO);
     biodone(bp);
     }

scsi_transport(9F) 返回的其他值如下所示:


注 –

在调用 scsi_transport(9F) 过程中,不能持有 scsi_device(9S) 结构中的互斥锁 sd_mutex


如果 scsi_transport(9F) 返回 TRAN_ACCEPT,则该包将由主机总线适配器驱动程序负责。调用命令完成例程之前,目标驱动程序不应该访问该包。

同步 scsi_transport() 函数

如果在包中设置了 FLAG_NOINTR,则在命令完成之前,scsi_transport(9F) 不会返回。不会执行回调。


注 –

请勿在中断上下文中使用 FLAG_NOINTR


命令完成

当主机总线适配器驱动程序完成命令后,将调用包的完成回调例程。然后,驱动程序会将指向 scsi_pkt(9S) 结构的指针作为参数传递。对包进行解码后,完成例程将执行相应的操作。

示例 17–5 显示了一个简单的完成回调例程。该代码检查传输是否失败。如果存在失败情况,该例程将放弃运行,而不会重试命令。如果目标繁忙,则需要额外的代码以便在以后重新提交命令。

如果命令产生检查条件,则目标驱动程序需要发送请求检测命令,除非启用了自动请求检测。

否则,命令成功。在结束命令处理时,命令将销毁包并调用 biodone(9F)

如果发生传输错误(如总线重置或奇偶校验问题),则目标驱动程序可以使用 scsi_transport(9F) 重新提交该包。重新提交之前,无需更改包中的任何值。

以下示例不会尝试重试未完成的命令。


注 –

通常,在中断上下文中会调用目标驱动程序的回调函数。因此,回调函数绝不应处于休眠状态。



示例 17–5 SCSI 驱动程序的完成例程

static void
xxcallback(struct scsi_pkt *pkt)
{
    struct buf        *bp;
    struct xxstate    *xsp;
    minor_t           instance;
    struct scsi_status *ssp;
    /*
     * Get a pointer to the buf(9S) structure for the command
     * and to the per-instance data structure.
     */
    bp = (struct buf *)pkt->pkt_private;
    instance = getminor(bp->b_edev);
    xsp = ddi_get_soft_state(statep, instance);
    /*
     * Figure out why this callback routine was called
     */
    if (pkt->pkt_reason != CMP_CMPLT) {
       bp->b_resid = bp->b_bcount;
       bioerror(bp, EIO);
       scsi_destroy_pkt(pkt);          /* Release resources */
       biodone(bp);                    /* Notify waiting threads */ ;
    } else {
       /*
        * Command completed, check status.
        * See scsi_status(9S)
        */
       ssp = (struct scsi_status *)pkt->pkt_scbp;
       if (ssp->sts_busy) {
           /* error, target busy or reserved */
       } else if (ssp->sts_chk) {
           /* Send a request sense command. */
       } else {
        bp->b_resid = pkt->pkt_resid;  /* Packet completed OK */
        scsi_destroy_pkt(pkt);
        biodone(bp);
       }
    }
}

重新使用包

目标驱动程序可以采用以下方式重新使用包:

自动请求检测模式

如果使用排队(无论是标记排队还是无标记排队),则最好使用自动请求检测模式。任何后续命令都会清除应急处理状态 (contingent allegiance condition),并因此导致检测数据丢失。大多数 HBA 驱动程序会在执行目标驱动程序回调之前开始下一条命令。其他 HBA 驱动程序可以使用单独的、较低优先级的线程执行回调。采用该方法时,如果存在检查状况,那么向目标驱动程序通知包已完成所需的时间可能会增加。在这种情况下,目标驱动程序可能无法及时提交请求检测命令以检索检测数据。

为了避免检测数据丢失,HBA 驱动程序或控制器应在检测到检查条件时发出请求检测命令。该模式称为自动请求检测模式。请注意,并非所有 HBA 驱动程序都可以使用自动请求检测模式,而某些驱动程序只能在启用了自动请求检测模式时运行。

目标驱动程序使用 scsi_ifsetcap(9F) 来启用自动请求检测模式。以下示例说明了如何启用自动请求检测。


示例 17–6 启用自动请求检测模式

static int
xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
    struct xxstate *xsp;
    struct scsi_device *sdp = (struct scsi_device *)
    ddi_get_driver_private(dip);
   /*
    * Enable auto-request-sense; an auto-request-sense cmd might
    * fail due to a BUSY condition or transport error. Therefore,
    * it is recommended to allocate a separate request sense
    * packet as well.
    * Note that scsi_ifsetcap(9F) can return -1, 0, or 1
    */
    xsp->sdp_arq_enabled =
    ((scsi_ifsetcap(ROUTE, “auto-rqsense”, 1, 1) == 1) ? 1 :
0);
   /*
    * If the HBA driver supports auto request sense then the
    * status blocks should be sizeof (struct scsi_arq_status);
    */
else
   /*
    * One byte is sufficient
    */
    xsp->sdp_cmd_stat_size =  (xsp->sdp_arq_enabled ?
    sizeof (struct scsi_arq_status) : 1);
   /* ... */
}

如果使用 scsi_init_pkt(9F) 分配了一个包并且该包需要自动请求检测,则需要增加空间。目标驱动程序必须为状态块请求此空间,以便存储自动请求检测结构。请求检测命令中使用的检测长度即 struct scsi_extended_sense 中的 sizeof。通过为状态块分配 struct scsi_status 中的 sizeof,可以针对各个包禁用自动请求检测。

可像往常一样使用 scsi_transport(9F) 提交包。当该包中出现检查条件时,主机适配器驱动程序将执行以下步骤:

目标驱动程序的回调例程应通过检查 pkt_state 中的 STATE_ARQ_DONE 位,来验证检测数据是否可用。STATE_ARQ_DONE 表明出现了检查条件,并且已执行请求检测。如果包中暂时禁用了自动请求检测,则无法保证对检测数据的后续检索。

然后,目标驱动程序应验证自动请求检测命令是否已成功完成,并对检测数据进行解码。

转储处理

在系统出现故障或执行检查点操作时,dump(9E) 入口点会将部分虚拟地址空间直接复制到指定的设备。请参见 cpr(7)dump(9E) 手册页。dump(9E) 入口点必须在不使用中断的情况下能够执行该操作。

dump() 的参数如下所示:

dev

转储设备的设备编号

addr

开始转储的内核虚拟地址

blkno

设备上的第一个目标块

nblk

要转储的块数


示例 17–7 dump(9E) 例程

static int
xxdump(dev_t dev, caddr_t addr, daddr_t blkno, int nblk)
{
    struct xxstate     *xsp;
    struct buf         *bp;
    struct scsi_pkt    *pkt;
    int    rval;
    int    instance;

    instance = getminor(dev);
    xsp = ddi_get_soft_state(statep, instance);

    if (tgt->suspended) {
    (void) pm_raise_power(DEVINFO(tgt), 0, 1);
    }

    bp = getrbuf(KM_NOSLEEP);
    if (bp == NULL) {
    return (EIO);
    }

/* Calculate block number relative to partition. */
    
bp->b_un.b_addr = addr;
    bp->b_edev = dev;
    bp->b_bcount = nblk * DEV_BSIZE;
    bp->b_flags = B_WRITE | B_BUSY;
    bp->b_blkno = blkno;

    pkt = scsi_init_pkt(ROUTE(tgt), NULL, bp, CDB_GROUP1,
    sizeof (struct scsi_arq_status),
    sizeof (struct bst_pkt_private), 0, NULL_FUNC, NULL);
    if (pkt == NULL) {
    freerbuf(bp);
    return (EIO);
    }
    (void) scsi_setup_cdb((union scsi_cdb *)pkt->pkt_cdbp,
        SCMD_WRITE_G1, blkno, nblk, 0);
    /*
     * While dumping in polled mode, other cmds might complete
     * and these should not be resubmitted. we set the
     * dumping flag here which prevents requeueing cmds.
     */
    tgt->dumping = 1;
    rval = scsi_poll(pkt);
    tgt->dumping = 0;

    scsi_destroy_pkt(pkt);
    freerbuf(bp);

    if (rval != DDI_SUCCESS) {
    rval = EIO;
    }

    return (rval);
}

SCSI 选项

SCSA 定义了一个全局变量 scsi_options,用于进行控制和调试。在文件 <sys/scsi/conf/autoconf.h> 中,可以找到 scsi_options 中定义的位。scsi_options 按以下方式使用这些位:

SCSI_OPTIONS_DR

启用全局断开连接或重新连接。

SCSI_OPTIONS_FAST

启用全局 FAST SCSI 支持: 每秒传输 10 MB。除非设置了 SCSI_OPTIONS_FAST (0x100) 位,否则 HBA 不应采用 FAST SCSI 模式运行。

SCSI_OPTIONS_FAST20

启用全局 FAST20 SCSI 支持: 每秒传输 20 MB。除非设置了 SCSI_OPTIONS_FAST20 (0x400) 位,否则 HBA 不应采用 FAST20 SCSI 模式运行。

SCSI_OPTIONS_FAST40

启用全局 FAST40 SCSI 支持: 每秒传输 40 MB。除非设置了 SCSI_OPTIONS_FAST40 (0x800) 位,否则 HBA 不应采用 FAST40 SCSI 模式运行。

SCSI_OPTIONS_FAST80

启用全局 FAST80 SCSI 支持: 每秒传输 80 MB。除非设置了 SCSI_OPTIONS_FAST80 (0x1000) 位,否则 HBA 不应采用 FAST80 SCSI 模式运行。

SCSI_OPTIONS_FAST160

启用全局 FAST160 SCSI 支持: 每秒传输 160 MB。除非设置了 SCSI_OPTIONS_FAST160 (0x2000) 位,否则 HBA 不应采用 FAST160 SCSI 模式运行。

SCSI_OPTIONS_FAST320

启用全局 FAST320 SCSI 支持: 每秒传输 320 MB。除非设置了 SCSI_OPTIONS_FAST320 (0x4000) 位,否则 HBA 不应采用 FAST320 SCSI 模式运行。

SCSI_OPTIONS_LINK

启用全局链接支持。

SCSI_OPTIONS_PARITY

启用全局奇偶校验支持。

SCSI_OPTIONS_QAS

启用“快速仲裁选择”功能。QAS(快速仲裁选择)用于降低设备仲裁并访问总线时的协议开销。只有 Ultra4 (FAST160) SCSI 设备支持 QAS,但是并非所有此类设备都支持 QAS。除非设置了 SCSI_OPTIONS_QAS (0x100000) 位,否则 HBA 不应采用 QAS SCSI 模式运行。请查阅相应的 Sun 硬件文档,以确定您的计算机是否支持 QAS。

SCSI_OPTIONS_SYNC

启用全局同步传输功能。

SCSI_OPTIONS_TAG

启用全局标记排队支持。

SCSI_OPTIONS_WIDE

启用全局 WIDE SCSI。


注 –

设置 scsi_options 会影响系统中存在的所有主机总线适配器驱动程序和所有目标驱动程序。有关控制特定主机适配器的这些选项的信息,请参阅 scsi_hba_attach(9F) 手册页。


第 18 章 SCSI 主机总线适配器驱动程序

本章介绍有关创建 SCSI 主机总线适配器 (host bus adapter, HBA) 驱动程序的信息。本章提供了用于说明典型 HBA 驱动程序的结构的样例代码。样例代码说明了如何使用 Sun 公用 SCSI 体系结构 (Sun Common SCSI Architecture, SCSA) 提供的 HBA 驱动程序接口。本章介绍有关以下主题的信息:

主机总线适配器驱动程序介绍

第 17 章中所述,DDI/DKI 可将 SCSI 设备的软件接口分成以下两个主要部分:

目标设备是指连接到 SCSI 总线上的设备,如磁盘或磁带机。目标驱动程序是指作为设备驱动程序安装的软件组件。SCSI 总线上的每个目标设备都由一个目标驱动程序实例控制。

主机总线适配器设备是指 HBA 硬件,如 SBus 或 PCI SCSI 适配卡。主机总线适配器驱动程序是指作为设备驱动程序安装的软件组件。例如,SPARC 计算机中的 esp 驱动程序、x86 计算机中的 ncrs 驱动程序以及适用于这两种体系结构的 isp 驱动程序。一个 HBA 驱动程序实例可控制系统中配置的它的各个主机总线适配器设备。

Sun 公用 SCSI 体系结构 (Sun Common SCSI Architecture, SCSA) 定义了目标组件和 HBA 组件之间的接口。


注 –

了解 SCSI 目标驱动程序是编写有效的 SCSI HBA 驱动程序的基本先决条件。有关 SCSI 目标驱动程序的信息,请参见第 17 章。目标驱动程序开发者通过阅读本章也会有所收益。


主机总线适配器驱动程序负责执行以下任务:

SCSI 接口

SCSA 是 DDI/DKI 编程接口,用于将 SCSI 命令从目标驱动程序传送到主机适配器驱动程序。通过与 SCSA 保持兼容,目标驱动程序可以将 SCSI 命令和序列的任何组合轻松传递到目标设备。无需了解主机适配器的硬件实现。从概念上讲,SCSA 会将生成 SCSI 命令与将命令(和数据)传输到 SCSI 总线这两个过程分离开来。SCSA 通过 HBA 传输层管理目标驱动程序与 HBA 驱动程序之间的连接,如下图所示:

图 18–1 SCSA 接口

图中显示了目标驱动程序与 SCSI 设备之间的主机总线适配器传输层。

HBA 传输层是一个硬件和软件层,负责将 SCSI 命令传输到 SCSI 目标设备。HBA 驱动程序会响应 SCSI 目标驱动程序通过 SCSA 发出的请求,提供资源分配、DMA 管理和传输服务。另外,主机适配器驱动程序还会管理主机适配器硬件以及执行命令所需的 SCSI 协议。完成命令后,HBA 驱动程序将调用目标驱动程序的 SCSI pkt 命令完成例程。

以下示例说明了此流程,并重点介绍了如何将信息从目标驱动程序传送到 SCSA,然后再传送到 HBA 驱动程序。下图还说明了典型的传输入口点和函数调用。

图 18–2 传输层流程

图中显示了命令如何通过 HBA 传输层。

SCSA HBA 接口

SCSA HBA 接口包括 HBA 入口点、HBA 数据结构和 HBA 框架。

SCSA HBA 入口点摘要

SCSA 定义了许多 HBA 驱动程序入口点。下表中列出了这些入口点。配置连接到 HBA 驱动程序的目标驱动程序实例时,系统将会调用这些入口点。另外,目标驱动程序发出 SCSA 请求时,也会调用这些入口点。有关更多信息,请参见SCSA HBA 驱动程序入口点

表 18–1 SCSA HBA 入口点摘要

函数名 

调用原因 

tran_abort(9E)

目标驱动程序调用 scsi_abort(9F)

tran_bus_reset(9E)

系统重置总线 

tran_destroy_pkt(9E)

目标驱动程序调用 scsi_destroy_pkt(9F)

tran_dmafree(9E)

目标驱动程序调用 scsi_dmafree(9F)

tran_getcap(9E)

目标驱动程序调用 scsi_ifgetcap(9F)

tran_init_pkt(9E)

目标驱动程序调用 scsi_init_pkt(9F)

tran_quiesce(9E)

系统使总线处于静止状态 

tran_reset(9E)

目标驱动程序调用 scsi_reset(9F)

tran_reset_notify(9E)

目标驱动程序调用 scsi_reset_notify(9F)

tran_setcap(9E)

目标驱动程序调用 scsi_ifsetcap(9F)

tran_start(9E)

目标驱动程序调用 scsi_transport(9F)

tran_sync_pkt(9E)

目标驱动程序调用 scsi_sync_pkt(9F)

tran_tgt_free(9E)

系统分离目标设备实例 

tran_tgt_init(9E)

系统连接目标设备实例 

tran_tgt_probe(9E)

目标驱动程序调用 scsi_probe(9F)

tran_unquiesce(9E)

系统恢复总线上的活动 

SCSA HBA 数据结构

SCSA 定义了多种数据结构,以便可在目标驱动程序和 HBA 驱动程序之间交换信息。其中包括以下数据结构:

scsi_hba_tran() 结构

HBA 驱动程序的每个实例都必须在 attach(9E) 入口点中使用 scsi_hba_tran_alloc(9F) 函数分配 scsi_hba_tran(9S) 结构。scsi_hba_tran_alloc() 函数可初始化 scsi_hba_tran 结构。HBA 驱动程序必须初始化传输结构中的特定向量才能指向 HBA 驱动程序中的入口点。初始化 scsi_hba_tran 结构后,HBA 驱动程序通过调用 scsi_hba_attach_setup(9F) 函数将传输结构导出到 SCSA。


注意 – 注意 –

由于 SCSA 将指向传输结构的指针保存在 devinfo 节点的驱动程序专用字段中,因此 HBA 驱动程序决不能使用 ddi_set_driver_private(9F)。但是,HBA 驱动程序可以使用 ddi_get_driver_private(9F) 来检索指向传输结构的指针。


SCSA 接口要求 HBA 驱动程序提供许多可通过 scsi_hba_tran 结构调用的入口点。有关更多信息,请参见SCSA HBA 驱动程序入口点

scsi_hba_tran 结构包含以下字段:

struct scsi_hba_tran {
    dev_info_t          *tran_hba_dip;          /* HBAs dev_info pointer */
    void                *tran_hba_private;      /* HBA softstate */
    void                *tran_tgt_private;      /* HBA target private pointer */
    struct scsi_device  *tran_sd;               /* scsi_device */
    int                 (*tran_tgt_init)();     /* Transport target */
                                                /* Initialization */
    int                 (*tran_tgt_probe)();    /* Transport target probe */
    void                (*tran_tgt_free)();     /* Transport target free */
    int                 (*tran_start)();        /* Transport start */
    int                 (*tran_reset)();        /* Transport reset */
    int                 (*tran_abort)();        /* Transport abort */
    int                 (*tran_getcap)();       /* Capability retrieval */
    int                 (*tran_setcap)();       /* Capability establishment */
    struct scsi_pkt     *(*tran_init_pkt)();    /* Packet and DMA allocation */
    void                (*tran_destroy_pkt)();  /* Packet and DMA */
                                                /* Deallocation */
    void                (*tran_dmafree)();      /* DMA deallocation */
    void                (*tran_sync_pkt)();     /* Sync DMA */
    void                (*tran_reset_notify)(); /* Bus reset notification */
    int                 (*tran_bus_reset)();    /* Reset bus only */
    int                 (*tran_quiesce)();      /* Quiesce a bus */
    int                 (*tran_unquiesce)();    /* Unquiesce a bus */
    int                 tran_interconnect_type; /* transport interconnect */
};

下面的描述提供了有关这些 scsi_hba_tran 结构字段的更多信息:

tran_hba_dip

指向 HBA 设备实例 dev_info 结构的指针。函数 scsi_hba_attach_setup(9F) 可用于设置此字段。

tran_hba_private

指向 HBA 驱动程序维护的专用数据的指针。通常,tran_hba_private 包含指向 HBA 驱动程序状态结构的指针。

tran_tgt_private

指向使用克隆时 HBA 驱动程序维护的专用数据的指针。通过在调用 scsi_hba_attach_setup(9F) 时指定 SCSI_HBA_TRAN_CLONE,可对每个目标克隆一次 scsi_hba_tran(9S) 结构。借助该方法,HBA 可将此字段初始化为指向 tran_tgt_init(9E) 入口点中按目标实例的数据结构。如果未指定 SCSI_HBA_TRAN_CLONE,则 tran_tgt_privateNULL,并且决不能引用 tran_tgt_private。有关更多信息,请参见传输结构克隆

tran_sd

指向克隆时使用的按目标实例的 scsi_device(9S) 结构的指针。如果将 SCSI_HBA_TRAN_CLONE 传递给 scsi_hba_attach_setup(9F),则 tran_sd 会初始化指向按目标的 scsi_device 结构。在代表目标调用任何 HBA 函数之前,将进行此初始化。如果未指定 SCSI_HBA_TRAN_CLONE,则 tran_sdNULL,并且决不能引用 tran_sd。有关更多信息,请参见传输结构克隆

tran_tgt_init

指向初始化目标设备实例时调用的 HBA 驱动程序入口点的指针。如果无需进行按目标的初始化,则 HBA 可保持将 tran_tgt_init 设置为 NULL

tran_tgt_probe

指向在目标驱动程序实例调用 scsi_probe(9F) 时调用的 HBA 驱动程序入口点的指针。调用该例程可探测目标设备是否存在。如果此 HBA 无需进行目标探测自定义,则 HBA 应将 tran_tgt_probe 设置为 scsi_hba_probe(9F)

tran_tgt_free

指向在目标设备实例被销毁时调用的 HBA 驱动程序入口点的指针。如果无需进行按目标的取消分配,则 HBA 可保持将 tran_tgt_free 设置为 NULL

tran_start

指向在目标驱动程序调用 scsi_transport(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_reset

指向在目标驱动程序调用 scsi_reset(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_abort

指向在目标驱动程序调用 scsi_abort(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_getcap

指向在目标驱动程序调用 scsi_ifgetcap(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_setcap

指向在目标驱动程序调用 scsi_ifsetcap(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_init_pkt

指向在目标驱动程序调用 scsi_init_pkt(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_destroy_pkt

指向在目标驱动程序调用 scsi_destroy_pkt(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_dmafree

指向在目标驱动程序调用 scsi_dmafree(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_sync_pkt

指向在目标驱动程序调用 scsi_sync_pkt(9F) 时调用的 HBA 驱动程序入口点的指针。

tran_reset_notify

指向在目标驱动程序调用 tran_reset_notify(9E) 时调用的 HBA 驱动程序入口点的指针。

tran_bus_reset

重置 SCSI 总线但不重置目标的函数项。

tran_quiesce

等待所有未完成的命令完成并阻塞(或排队)任何发出的 I/O 请求的函数项。

tran_unquiesce

允许 I/O 活动在 SCSI 总线上恢复的函数项。

tran_interconnect_type

表示 services.h 头文件中定义的传输互连类型的整数值。

scsi_address 结构

scsi_address(9S) 结构可为目标驱动程序实例分配和传输的各个 SCSI 命令提供传输及寻址信息。

scsi_address 结构包含以下字段:

struct scsi_address {
    struct scsi_hba_tran    *a_hba_tran;    /* Transport vectors */
    ushort_t                a_target;       /* Target identifier */
    uchar_t                 a_lun;          /* LUN on that target */
    uchar_t                 a_sublun;       /* Sub LUN on that LUN */
                                            /* Not used */
};
a_hba_tran

指向 HBA 驱动程序分配和初始化的 scsi_hba_tran(9S) 结构的指针。如果将 SCSI_HBA_TRAN_CLONE 指定为 scsi_hba_attach_setup(9F) 的标志, 则 a_hba_tran 指向该结构的副本。

a_target

标识 SCSI 总线上的 SCSI 目标。

a_lun

标识 SCSI 目标的 SCSI 逻辑单元。

scsi_device 结构

HBA 框架可为目标设备的各个实例分配和初始化 scsi_device(9S) 结构。该框架调用 HBA 驱动程序的 tran_tgt_init(9E) 入口点之前,将进行分配和初始化。此结构可存储有关每个 SCSI 逻辑单元的信息,包括指向信息区(包含通用信息和特定于设备的信息)的指针。对于连接到系统的每个目标设备实例,都存在一个 scsi_device(9S) 结构。

如果按目标的初始化成功,则 HBA 框架会使用 ddi_set_driver_private(9F) 将目标驱动程序的按实例的专用数据设置为指向 scsi_device(9S) 结构。请注意,如果 tran_tgt_init () 返回成功信息或该向量为 null,则表明初始化成功。

scsi_device(9S) 结构包含以下字段:

struct scsi_device {
    struct scsi_address           sd_address;    /* routing information */
    dev_info_t                    *sd_dev;       /* device dev_info node */
    kmutex_t                      sd_mutex;      /* mutex used by device */
    void                          *sd_reserved;
    struct scsi_inquiry           *sd_inq;
    struct scsi_extended_sense    *sd_sense;
    caddr_t                       sd_private;    /* for driver's use */
};

其中:

sd_address

为了进行 SCSI 资源分配而传递给例程的数据结构。

sd_dev

指向目标的 dev_info 结构的指针。

sd_mutex

供目标驱动程序使用的互斥锁。此互斥锁通过 HBA 框架进行初始化。目标驱动程序可将此互斥锁用作按设备的互斥锁。在调用 scsi_transport(9F)scsi_poll(9F) 期间,不应持有此互斥锁。有关互斥锁的更多信息,请参见第 3 章

sd_inq

目标设备的 SCSI 查询数据的指针。scsi_probe(9F) 例程可用于分配缓冲区、填充该缓冲区并将该缓冲区附加到此字段。

sd_sense

指向用于包含设备中的请求检测数据的缓冲区的指针。目标驱动程序必须分配和管理此缓冲区本身。有关更多信息,请参见attach() 入口点中目标驱动程序的 attach(9E) 例程。

sd_private

供目标驱动程序使用的指针字段。此字段通常用于存储指向专用目标驱动程序状态结构的指针。

scsi_pkt 结构 (HBA)

要执行 SCSI 命令,目标驱动程序必须首先为该命令分配 scsi_pkt(9S) 结构。然后,目标驱动程序必须指定其自身的专用数据区长度、命令状态和命令长度。HBA 驱动程序负责实现 tran_init_pkt(9E) 入口点中的包分配。另外,HBA 驱动程序还负责释放其 tran_destroy_pkt(9E) 入口点中的包。有关更多信息,请参见scsi_pkt 结构(目标驱动程序)

scsi_pkt(9S) 结构包含以下字段:

struct scsi_pkt {
    opaque_t pkt_ha_private;             /* private data for host adapter */
    struct scsi_address pkt_address;     /* destination address */
    opaque_t pkt_private;                /* private data for target driver */
    void (*pkt_comp)(struct scsi_pkt *); /* completion routine */
    uint_t  pkt_flags;                   /* flags */
    int     pkt_time;                    /* time allotted to complete command */
    uchar_t *pkt_scbp;                   /* pointer to status block */
    uchar_t *pkt_cdbp;                   /* pointer to command block */
    ssize_t pkt_resid;                   /* data bytes not transferred */
    uint_t  pkt_state;                   /* state of command */
    uint_t  pkt_statistics;              /* statistics */
    uchar_t pkt_reason;                  /* reason completion called */
};

其中:

pkt_ha_private

指向按命令的 HBA 驱动程序专用数据的指针。

pkt_address

指向用于为此命令提供地址信息的 scsi_address(9S) 结构的指针。

pkt_private

指向按包的目标驱动程序专用数据的指针。

pkt_comp

指向在传输层完成此命令时 HBA 驱动程序调用的目标驱动程序完成例程的指针。

pkt_flags

命令的标志。

pkt_time

指定命令的完成超时时间(以秒为单位)。

pkt_scbp

指向命令的状态完成块的指针。

pkt_cdbp

指向命令的命令描述符块 (command descriptor block, CDB) 的指针。

pkt_resid

命令完成时传送的数据字节计数。此字段也可能会用于指定尚未分配资源的数据量。在传输过程中,HBA 必须修改此字段。

pkt_state

命令的状态。在传输过程中,HBA 必须修改此字段。

pkt_statistics

提供命令在传输层中发生的事件的历史记录。在传输过程中,HBA 必须修改此字段。

pkt_reason

命令完成的原因。在传输过程中,HBA 必须修改此字段。

按目标实例的数据

在执行 attach(9E) 期间,HBA 驱动程序必须分配 scsi_hba_tran(9S) 结构。然后,HBA 驱动程序必须将此传输结构中的向量初始化为指向 HBA 驱动程序所需的入口点。此 scsi_hba_tran 结构随后将传递给 scsi_hba_attach_setup(9F)

scsi_hba_tran 结构包含 tran_hba_private 字段,该字段可用于引用 HBA 驱动程序的按实例状态。

每个 scsi_address(9S) 结构都包含一个指向 scsi_hba_tran 结构的指针。此外,scsi_address 结构还为特定的目标设备提供了目标(即 a_target)和逻辑单元 (a_lun) 地址。 通过 scsi_device(9S) 结构可直接或间接向 HBA 驱动程序的每个入口点传递一个指向 scsi_address 结构的指针。因此,HBA 驱动程序可以引用其自身的状态。HBA 驱动程序还可以标识已寻址的目标设备。

下图说明了用于传输操作的 HBA 数据结构。

图 18–3 HBA 传输结构

图中显示了 HBA 传输层中涉及的结构的关系。

传输结构克隆

如果 HBA 驱动程序需要维护 scsi_hba_tran(9S) 结构中按目标的专用数据,则克隆可能会非常有用。克隆还可用于维护比 scsi_address(9S) 结构中所提供的更为复杂的地址。

在克隆过程中,HBA 驱动程序仍必须在执行 attach(9E) 期间分配 scsi_hba_tran 结构。此外,HBA 驱动程序还必须初始化 HBA 驱动程序的 tran_hba_private 软状态指针和入口点向量。当框架开始将目标驱动程序实例连接到 HBA 驱动程序时,将会产生差异。调用 HBA 驱动程序的 tran_tgt_init(9E) 入口点之前,框架会克隆与 HBA 的该实例关联的 scsi_hba_tran 结构。相应地,为特定目标设备实例分配和初始化的每个 scsi_address 结构都会指向 scsi_hba_tran 结构的按目标实例的副本scsi_address 结构不会指向 HBA 驱动程序在执行 attach() 期间分配的 scsi_hba_tran 结构。

指定克隆时,HBA 驱动程序可以使用两个重要的指针。这些指针包含在 scsi_hba_tran 结构中。第一个指针是 tran_tgt_private 字段,驱动程序可以使用该指针指向按目标的 HBA 专用数据。tran_tgt_private 指针非常有用,例如在 HBA 驱动程序需要维护比 a_targeta_lun 所提供的更为复杂的地址的情况下。第二个指针是 tran_sd 字段,该指针指向引用特定目标设备的 scsi_device(9S) 结构。

指定克隆时,HBA 驱动程序必须分配和初始化按目标的数据。HBA 驱动程序随后必须在执行其 tran_tgt_init(9E) 入口点过程中将 tran_tgt_private 字段初始化为指向此数据。 HBA 驱动程序必须在执行其 tran_tgt_free(9E) 入口点过程中释放按目标的数据。

克隆时,框架会在调用 HBA 驱动程序 tran_tgt_init() 入口点之前将 tran_sd 字段初始化为指向 scsi_device 结构。该驱动程序通过将 SCSI_HBA_TRAN_CLONE 标志传递给 scsi_hba_attach_setup(9F) 来请求克隆。下图说明了用于克隆传输操作的 HBA 数据结构。

图 18–4 克隆传输操作

图中显示了克隆的 HBA 结构的示例。

SCSA HBA 函数

SCSA 还提供了许多函数。下表中列出了这些函数,供 HBA 驱动程序使用。

表 18–2 SCSA HBA 函数

函数名 

进行调用的驱动程序入口点 

scsi_hba_init(9F)

_init(9E)

scsi_hba_fini(9F)

_fini(9E)

scsi_hba_attach_setup(9F)

attach(9E)

scsi_hba_detach(9F)

detach(9E)

scsi_hba_tran_alloc(9F)

attach(9E)

scsi_hba_tran_free(9F)

detach(9E)

scsi_hba_probe(9F)

tran_tgt_probe(9E)

scsi_hba_pkt_alloc(9F)

tran_init_pkt(9E)

scsi_hba_pkt_free(9F)

tran_destroy_pkt(9E)

scsi_hba_lookup_capstr(9F)

tran_getcap(9E)tran_setcap(9E)

HBA 驱动程序的相关性和配置问题

除将 SCSA HBA 入口点、结构和函数合并到驱动程序中外,开发者还必须处理驱动程序的相关性和配置问题。这些问题涉及配置属性、相关性声明、状态结构和按命令的结构、模块初始化入口点及自动配置入口点。

声明和结构

HBA 驱动程序必须包含以下头文件:

#include <sys/scsi/scsi.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>

要向系统通知模块依赖于 SCSA 例程,必须使用以下命令生成驱动程序二进制代码。有关 SCSA 例程的更多信息,请参见SCSA HBA 接口


% ld -r xx.o -o xx -N "misc/scsi"

样例代码是由 QLogic Intelligent SCSI 外围设备的简化 isp 驱动程序派生而来。isp 驱动程序支持 WIDE SCSI,每个目标最多 15 个目标设备和 8 个逻辑单元 (logical unit, LUN)。

每个命令的结构

通常,HBA驱动程序需要定义结构以维护目标驱动程序提交的各个命令的状态。按命令的结构的布局完全取决于设备驱动程序编写者。该布局需要反映硬件的功能和特征以及驱动程序使用的软件算法。

以下结构是每个命令的结构的示例。本章中的其余代码段将使用此结构说明 HBA 接口。

struct isp_cmd {
     struct isp_request     cmd_isp_request;
     struct isp_response    cmd_isp_response;
     struct scsi_pkt        *cmd_pkt;
     struct isp_cmd         *cmd_forw;
     uint32_t               cmd_dmacount;
     ddi_dma_handle_t       cmd_dmahandle;
     uint_t                 cmd_cookie;
     uint_t                 cmd_ncookies;
     uint_t                 cmd_cookiecnt;
     uint_t                 cmd_nwin;
     uint_t                 cmd_curwin;
     off_t                  cmd_dma_offset;
     uint_t                 cmd_dma_len;
     ddi_dma_cookie_t       cmd_dmacookies[ISP_NDATASEGS];
     u_int                  cmd_flags;
     u_short                cmd_slot;
     u_int                  cmd_cdblen;
     u_int                  cmd_scblen;
 };

模块初始化入口点

本节介绍 SCSI HBA 驱动程序执行的操作的入口点。

以下 SCSI HBA 驱动程序代码说明了典型的 dev_ops(9S) 结构。该驱动程序必须将此结构中的 devo_bus_ops 字段初始化为 NULL。SCSI HBA 驱动程序可提供特殊用途的叶驱动程序接口,在这种情况下,devo_cb_ops 字段可能会指向 cb_ops(9S) 结构。在此示例中,由于未导出任何叶驱动程序接口,因此 devo_cb_ops 字段会初始化为 NULL

_init() 入口点(SCSI HBA 驱动程序)

_init(9E) 函数用于初始化可装入模块。_init() 在可装入模块中的其他任何例程之前调用。

在 SCSI HBA 中,_init() 函数在调用 mod_install(9F) 之前,必须先调用 scsi_hba_init(9F) 来通知框架是否存在 HBA 驱动程序。如果 scsi_hba__init () 返回非零值,则 _init() 应返回该值。否则,_init() 必须返回 mod_install(9F) 所返回的值。

该驱动程序在调用 mod_install(9F) 之前应初始化任何必需的全局状态。

如果 mod_install() 失败,则 _init() 函数必须释放分配的所有全局资源。_init() 必须在返回之前调用 scsi_hba_fini(9F)

以下示例使用全局互斥锁说明如何分配对驱动程序的所有实例而言具有全局性的数据。该代码声明了全局互斥锁和软状态结构信息。全局互斥锁和软状态是在执行 _init() 的过程中初始化的。

_fini() 入口点(SCSI HBA 驱动程序)

如果系统准备尝试卸载 SCSI HBA 驱动程序,则会调用 _fini(9E) 函数。 _fini() 函数必须调用 mod_remove(9F) 来确定是否可以卸载该驱动程序。如果 mod_remove() 返回 0,则可以卸载该模块。HBA 驱动程序必须取消分配 _init(9E) 中分配的所有全局资源。HBA 驱动程序还必须调用 scsi_hba_fini(9F)

_fini() 必须返回 mod_remove() 所返回的值。


注 –

除非 mod_remove(9F) 返回 0,否则 HBA 驱动程序决不能释放任何资源或调用 scsi_hba_fini(9F)


示例 18–1 说明了 SCSI HBA 的模块初始化。


示例 18–1 SCSI HBA 的模块初始化

static struct dev_ops isp_dev_ops = {
    DEVO_REV,       /* devo_rev */
    0,              /* refcnt  */
    isp_getinfo,    /* getinfo */
    nulldev,        /* probe */
    isp_attach,     /* attach */
    isp_detach,     /* detach */
    nodev,          /* reset */
    NULL,           /* driver operations */
    NULL,           /* bus operations */
    isp_power,      /* power management */
};

/*
 * Local static data
 */
static kmutex_t      isp_global_mutex;
static void          *isp_state;

int
_init(void)
{
    int     err;
    
    if ((err = ddi_soft_state_init(&isp_state,
        sizeof (struct isp), 0)) != 0) {
        return (err);
    }
    if ((err = scsi_hba_init(&modlinkage)) == 0) {
        mutex_init(&isp_global_mutex, "isp global mutex",
        MUTEX_DRIVER, NULL);
        if ((err = mod_install(&modlinkage)) != 0) {
            mutex_destroy(&isp_global_mutex);
            scsi_hba_fini(&modlinkage);
            ddi_soft_state_fini(&isp_state);    
        }
    }
    return (err);
}

int
_fini(void)
{
    int     err;
    
    if ((err = mod_remove(&modlinkage)) == 0) {
        mutex_destroy(&isp_global_mutex);
        scsi_hba_fini(&modlinkage);
        ddi_soft_state_fini(&isp_state);
    }
    return (err);
}

自动配置入口点

dev_ops(9S) 结构与每个设备驱动程序关联。通过该结构,内核可以查找驱动程序的自动配置入口点。有关这些自动配置例程的完整说明,请参见第 6 章。本节仅介绍与 SCSI HBA 驱动程序执行的操作关联的那些入口点。这些入口点包括 attach(9E)detach(9E)

attach() 入口点(SCSI HBA 驱动程序)

在为设备配置和附加驱动程序实例时,SCSI HBA 驱动程序的 attach(9E) 入口点将执行多个任务。对于实际设备的典型驱动程序,必须处理以下操作系统和硬件问题:

软状态结构

分配按设备实例的软状态结构时,如果发生错误,驱动程序必须仔细清理。

DMA

HBA 驱动程序必须通过正确初始化 ddi_dma_attr_t 结构来描述其 DMA 引擎的属性。

static ddi_dma_attr_t isp_dma_attr = {
     DMA_ATTR_V0,        /* ddi_dma_attr version */
     0,                  /* low address */
     0xffffffff,         /* high address */
     0x00ffffff,         /* counter upper bound */
     1,                  /* alignment requirements */
     0x3f,               /* burst sizes */
     1,                  /* minimum DMA access */
     0xffffffff,         /* maximum DMA access */
     (1<<24)-1,          /* segment boundary restrictions */
     1,                  /* scatter-gather list length */
     512,                /* device granularity */
     0                   /* DMA flags */
};

如果该驱动程序提供 DMA,则还应检查其硬件是否已安装在支持 DMA 的槽中:

if (ddi_slaveonly(dip) == DDI_SUCCESS) {
    return (DDI_FAILURE);
}

传输结构

驱动程序应进一步分配和初始化此实例的传输结构。tran_hba_private 字段会设置为指向此实例的软状态结构。如果无需特殊的探测自定义,则可将 tran_tgt_probe 字段设置为 NULL 以实现缺省行为。

tran = scsi_hba_tran_alloc(dip, SCSI_HBA_CANSLEEP);

isp->isp_tran                   = tran;
isp->isp_dip                    = dip;

tran->tran_hba_private          = isp;
tran->tran_tgt_private          = NULL;
tran->tran_tgt_init             = isp_tran_tgt_init;
tran->tran_tgt_probe            = scsi_hba_probe;
tran->tran_tgt_free             = (void (*)())NULL;

tran->tran_start                = isp_scsi_start;
tran->tran_abort                = isp_scsi_abort;
tran->tran_reset                = isp_scsi_reset;
tran->tran_getcap               = isp_scsi_getcap;
tran->tran_setcap               = isp_scsi_setcap;
tran->tran_init_pkt             = isp_scsi_init_pkt;
tran->tran_destroy_pkt          = isp_scsi_destroy_pkt;
tran->tran_dmafree              = isp_scsi_dmafree;
tran->tran_sync_pkt             = isp_scsi_sync_pkt;
tran->tran_reset_notify         = isp_scsi_reset_notify;
tran->tran_bus_quiesce          = isp_tran_bus_quiesce
tran->tran_bus_unquiesce        = isp_tran_bus_unquiesce
tran->tran_bus_reset            = isp_tran_bus_reset
tran->tran_interconnect_type    = isp_tran_interconnect_type

附加 HBA 驱动程序

驱动程序应附加此设备实例并执行错误清理(如有必要)。

i = scsi_hba_attach_setup(dip, &isp_dma_attr, tran, 0);
if (i != DDI_SUCCESS) {
    /* do error recovery */
    return (DDI_FAILURE);
}

寄存器映射

驱动程序应在其设备的寄存器中进行映射。驱动程序需要指定以下项:

ddi_device_acc_attr_t    dev_attributes;

     dev_attributes.devacc_attr_version = DDI_DEVICE_ATTR_V0;
     dev_attributes.devacc_attr_dataorder = DDI_STRICTORDER_ACC;
     dev_attributes.devacc_attr_endian_flags = DDI_STRUCTURE_LE_ACC;

     if (ddi_regs_map_setup(dip, 0, (caddr_t *)&isp->isp_reg,
     0, sizeof (struct ispregs), &dev_attributes,
     &isp->isp_acc_handle) != DDI_SUCCESS) {
        /* do error recovery */
        return (DDI_FAILURE);
     }

添加中断处理程序

驱动程序必须首先获取 iblock cookie 才能初始化驱动程序处理程序中使用的所有互斥锁。仅当初始化这些互斥锁后才能添加中断处理程序。

i = ddi_get_iblock_cookie(dip, 0, &isp->iblock_cookie};
if (i != DDI_SUCCESS) {
    /* do error recovery */
    return (DDI_FAILURE);
}

mutex_init(&isp->mutex, "isp_mutex", MUTEX_DRIVER,
(void *)isp->iblock_cookie);
i = ddi_add_intr(dip, 0, &isp->iblock_cookie,
0, isp_intr, (caddr_t)isp);
if (i != DDI_SUCCESS) {
    /* do error recovery */
    return (DDI_FAILURE);
}

如果需要高级处理程序,则应对驱动程序进行编码以提供此类处理程序。否则,驱动程序必须能够停止附加操作。有关高级中断处理的说明,请参见处理高级别中断

创建可管理电源的组件

如果主机总线适配器只需要在所有目标适配器的电源级别为 0 时关闭电源,则使用电源管理,HBA 驱动程序只需提供 power(9E) 入口点。请参阅第 12 章。另外,HBA 驱动程序还需要创建 pm-components(9P) 属性,用于描述设备实现的组件。

由于这些组件将缺省为空闲,并且电源管理框架的缺省相关性处理会确保主机总线适配器在目标适配器每次通电时也随之通电,因此无需再执行任何操作。如果自动启用自动电源管理,则该处理还将在所有目标适配器都断电时关闭主机总线适配器电源。

报告附加状态

最后,驱动程序应报告已附加的此驱动程序实例并返回成功信息。

ddi_report_dev(dip);
    return (DDI_SUCCESS);

detach() 入口点(SCSI HBA 驱动程序)

驱动程序会执行标准分离操作,包括调用 scsi_hba_detach(9F)

SCSA HBA 驱动程序入口点

HBA 驱动程序可以通过 SCSA 接口与目标驱动程序协同工作。SCSA 接口要求 HBA 驱动程序提供许多可通过 scsi_hba_tran(9S) 结构调用的入口点。

这些入口点分为以下五个功能组:

下表按功能组列出了 SCSA HBA 入口点。

表 18–3 SCSA 入口点

功能组 

组内入口点 

说明 

目标驱动程序实例初始化 

tran_tgt_init(9E)

执行按目标的初始化(可选) 

 

tran_tgt_probe(9E)

探测 SCSI 总线是否存在目标(可选) 

 

tran_tgt_free(9E)

执行按目标的取消分配(可选) 

资源分配 

tran_init_pkt(9E)

分配 SCSI 包和 DMA 资源 

 

tran_destroy_pkt(9E)

释放 SCSI 包和 DMA 资源 

 

tran_sync_pkt(9E)

执行 DMA 前后同步内存 

 

tran_dmafree(9E)

释放 DMA 资源 

命令传输 

tran_start(9E)

传输 SCSI 命令 

功能管理 

tran_getcap(9E)

查询功能值 

 

tran_setcap(9E)

设置功能值 

中止和重置 

tran_abort(9E)

中止未完成的 SCSI 命令 

 

tran_reset(9E)

重置目标设备或 SCSI 总线 

 

tran_bus_reset(9E)

重置 SCSI 总线 

 

tran_reset_notify(9E)

请求向目标发出总线重置通知(可选) 

动态重新配置 

tran_quiesce(9E)

停止总线上的活动 

 

tran_unquiesce(9E)

恢复总线上的活动 

目标驱动程序实例初始化

以下各节介绍了目标入口点。

tran_tgt_init() 入口点

使用 tran_tgt_init(9E) 入口点,HBA 可以分配和初始化按目标的任何资源。此外,tran_tgt_init() 还允许 HBA 将设备地址限定为该特定 HBA 的有效且可支持的地址。如果返回 DDI_FAILURE,则不会探测或附加该设备的目标驱动程序实例。

无需使用 tran_tgt_init()。如果未提供 tran_tgt_init(),则框架会尝试探测和附加相应目标驱动程序的所有可能实例。

static int
isp_tran_tgt_init(
    dev_info_t            *hba_dip,
    dev_info_t            *tgt_dip,
    scsi_hba_tran_t       *tran,
    struct scsi_device    *sd)
{
    return ((sd->sd_address.a_target < N_ISP_TARGETS_WIDE &&
        sd->sd_address.a_lun < 8) ? DDI_SUCCESS : DDI_FAILURE);
}

tran_tgt_probe() 入口点

使用 tran_tgt_probe(9E) 入口点,HBA 可以自定义 scsi_probe(9F) 的操作(如有必要)。仅当目标驱动程序调用 scsi_probe() 时,才会调用此入口点。

HBA 驱动程序可以通过调用 scsi_hba_probe(9F) 并返回其返回值来保留 scsi_probe () 的正常操作。

无需使用此入口点。如果不需要此入口点,则 HBA 驱动程序应将 scsi_hba_tran(9S) 结构中的 tran_tgt_probe 向量设置为指向 scsi_hba_probe()

scsi_probe() 可用于分配 scsi_inquiry(9S) 结构,并将 scsi_device(9S) 结构的 sd_inq 字段设置为指向 scsi_inquiry 中的数据。scsi_hba_probe() 可自动处理此任务。scsi_unprobe(9F) 随后将释放 scsi_inquiry 数据。

除分配 scsi_inquiry 数据以外,tran_tgt_probe() 必须是无状态的,因为同一 SCSI 设备可能会多次调用 tran_tgt_probe()。通常,scsi_inquiry 数据的分配通过 scsi_hba_probe() 来处理。


注 –

scsi_inquiry(9S) 结构的分配通过 scsi_hba_probe() 自动处理。此信息仅在需要自定义 scsi_probe() 处理时才有意义。


static int
isp_tran_tgt_probe(
    struct scsi_device    *sd,
    int                   (*callback)())
{
    /*
     * Perform any special probe customization needed.
     * Normal probe handling.
     */
    return (scsi_hba_probe(sd, callback));
}

tran_tgt_free() 入口点

使用 tran_tgt_free(9E) 入口点,HBA 可以执行目标实例的所有取消分配或清理过程。此入口点是可选的。

static void
isp_tran_tgt_free(
    dev_info_t            *hba_dip,
    dev_info_t            *tgt_dip,
    scsi_hba_tran_t       *hba_tran,
    struct scsi_device    *sd)
{
    /*
     * Undo any special per-target initialization done
     * earlier in tran_tgt_init(9F) and tran_tgt_probe(9F)
     */
}

资源分配

以下各节讨论了资源分配。

tran_init_pkt() 入口点

tran_init_pkt(9E) 入口点可为目标驱动程序请求分配和初始化 scsi_pkt(9S) 结构和 DMA 资源。

目标驱动程序调用 SCSA 函数 scsi_init_pkt(9F) 时,将会调用 tran_init_pkt(9E) 入口点。

每次调用 tran_init_pkt(9E) 入口点时,都会请求执行以下三种可能服务中的一种或多种:

分配和初始化 scsi_pkt(9S) 结构

如果 pktNULL,则 tran_init_pkt(9E) 入口点必须通过 scsi_hba_pkt_alloc(9F) 分配 scsi_pkt(9S) 结构。

scsi_hba_pkt_alloc(9F) 可为以下各项分配空间:

除以下成员外,必须将 scsi_pkt(9S) 结构成员(包括 pkt)初始化为零:

这些成员是指向存储字段值的内存空间的指针,如下图所示。有关更多信息,请参阅scsi_pkt 结构 (HBA)

图 18–5 scsi_pkt(9S) 结构指针

图中显示了包含指向值而未初始化为零的那些成员的 scsi_pkt 结构。

以下示例说明了 scsi_pkt 结构的分配和初始化。


示例 18–2 SCSI 包结构的 HBA 驱动程序初始化

static struct scsi_pkt                 *
isp_scsi_init_pkt(
    struct scsi_address    *ap,
    struct scsi_pkt        *pkt,
    struct buf             *bp,
    int                    cmdlen,
    int                    statuslen,
    int                    tgtlen,
    int                    flags,
    int                    (*callback)(),
    caddr_t                arg)
{
    struct isp_cmd         *sp;
    struct isp             *isp;
    struct scsi_pkt        *new_pkt;

    ASSERT(callback == NULL_FUNC || callback == SLEEP_FUNC);

    isp = (struct isp *)ap->a_hba_tran->tran_hba_private;
   /*
    * First step of isp_scsi_init_pkt:  pkt allocation
    */
    if (pkt == NULL) {
        pkt = scsi_hba_pkt_alloc(isp->isp_dip, ap, cmdlen,
            statuslen, tgtlen, sizeof (struct isp_cmd),
            callback, arg);
        if (pkt == NULL) {
            return (NULL);
       }

       sp = (struct isp_cmd *)pkt->pkt_ha_private;
      /*
       * Initialize the new pkt
       */
       sp->cmd_pkt         = pkt;
       sp->cmd_flags       = 0;
       sp->cmd_scblen      = statuslen;
       sp->cmd_cdblen      = cmdlen;
       sp->cmd_dmahandle   = NULL;
       sp->cmd_ncookies    = 0;
       sp->cmd_cookie      = 0; 
       sp->cmd_cookiecnt   = 0;
       sp->cmd_nwin        = 0;
       pkt->pkt_address    = *ap;
       pkt->pkt_comp       = (void (*)())NULL;
       pkt->pkt_flags      = 0;
       pkt->pkt_time       = 0;
       pkt->pkt_resid      = 0;
       pkt->pkt_statistics = 0;
       pkt->pkt_reason     = 0;

       new_pkt = pkt;
    } else {
       sp = (struct isp_cmd *)pkt->pkt_ha_private;
       new_pkt = NULL;
    }
   /*
    * Second step of isp_scsi_init_pkt:  dma allocation/move
    */
    if (bp && bp->b_bcount != 0) {
        if (sp->cmd_dmahandle == NULL) {
            if (isp_i_dma_alloc(isp, pkt, bp,
            flags, callback) == 0) {
            if (new_pkt) {
                scsi_hba_pkt_free(ap, new_pkt);
            }
            return ((struct scsi_pkt *)NULL);
        }
        } else {
            ASSERT(new_pkt == NULL);
            if (isp_i_dma_move(isp, pkt, bp) == 0) {
                return ((struct scsi_pkt *)NULL);
            }
        }
    }
    return (pkt);
}

分配 DMA 资源

如果符合以下条件,则 tran_init_pkt(9E) 入口点必须分配用于数据传送的 DMA 资源:

HBA 驱动程序需要跟踪如何为特定命令分配 DMA 资源。按包的 HBA 驱动程序专用数据的标志位或 DMA 句柄可能会进行此分配。

使用 pkt 中的 PKT_DMA_PARTIAL 标志,目标驱动程序可以将数据传送按多个 SCSI 命令分类以适应整个请求。如果 HBA 硬件的分散/集中功能或系统 DMA 资源无法完成单个 SCSI 命令的请求,则此方法会非常有用。

使用 PKT_DMA_PARTIAL 标志,HBA 驱动程序可以设置 DDI_DMA_PARTIAL 标志。DDI_DMA_PARTIAL 标志有助于分配此 SCSI 命令的 DMA 资源。例如,ddi_dma_buf_bind_handle(9F) 命令可用于分配 DMA 资源。分配 DMA 资源时使用的 DMA 属性应准确说明针对 HBA 硬件执行 DMA 的能力设定的约束。如果系统只能为部分请求分配 DMA 资源,则 ddi_dma_buf_bind_handle(9F) 将返回 DDI_DMA_PARTIAL_MAP

tran_init_pkt(9E) 入口点必须在字段 pkt_resid 中返回未为此传送分配的 DMA 资源量。

目标驱动程序可以请求 tran_init_pkt(9E) 同时为该 pkt 分配 scsi_pkt(9S) 结构和 DMA 资源。在这种情况下,如果 HBA 驱动程序无法分配 DMA 资源,则该驱动程序必须在返回前释放已分配的 scsi_pkt(9S)scsi_pkt(9S) 必须通过调用 scsi_hba_pkt_free(9F) 进行释放。

目标驱动程序可能会首先分配 scsi_pkt(9S),随后再为此 pkt 分配 DMA 资源。在这种情况下,如果 HBA 驱动程序无法分配 DMA 资源,则该驱动程序决不能释放 pkt。在这种情况下,目标驱动程序负责释放 pkt


示例 18–3 HBA 驱动程序的 DMA 资源分配

static int
isp_i_dma_alloc(
    struct isp         *isp,
    struct scsi_pkt    *pkt,
    struct buf         *bp,
    int                flags,
    int                (*callback)())
{
    struct isp_cmd     *sp  = (struct isp_cmd *)pkt->pkt_ha_private;
    int                dma_flags;
    ddi_dma_attr_t     tmp_dma_attr;
    int                (*cb)(caddr_t);
    int                i;

    ASSERT(callback == NULL_FUNC || callback == SLEEP_FUNC);

    if (bp->b_flags & B_READ) {
        sp->cmd_flags &= ~CFLAG_DMASEND;
        dma_flags = DDI_DMA_READ;
    } else {
        sp->cmd_flags |= CFLAG_DMASEND;
        dma_flags = DDI_DMA_WRITE;
    }
    if (flags & PKT_CONSISTENT) {
        sp->cmd_flags |= CFLAG_CMDIOPB;
        dma_flags |= DDI_DMA_CONSISTENT;
    }
    if (flags & PKT_DMA_PARTIAL) {
        dma_flags |= DDI_DMA_PARTIAL;
    }

    tmp_dma_attr = isp_dma_attr;
    tmp_dma_attr.dma_attr_burstsizes = isp->isp_burst_size;

    cb = (callback == NULL_FUNC) ? DDI_DMA_DONTWAIT :
DDI_DMA_SLEEP;

    if ((i = ddi_dma_alloc_handle(isp->isp_dip, &tmp_dma_attr,
      cb, 0, &sp->cmd_dmahandle)) != DDI_SUCCESS) {
        switch (i) {
        case DDI_DMA_BADATTR:
            bioerror(bp, EFAULT);
            return (0);
        case DDI_DMA_NORESOURCES:
            bioerror(bp, 0);
            return (0);
        }
    }

    i = ddi_dma_buf_bind_handle(sp->cmd_dmahandle, bp, dma_flags,
    cb, 0, &sp->cmd_dmacookies[0], &sp->cmd_ncookies);

    switch (i) {
    case DDI_DMA_PARTIAL_MAP:
    if (ddi_dma_numwin(sp->cmd_dmahandle, &sp->cmd_nwin) ==
            DDI_FAILURE) {
        cmn_err(CE_PANIC, "ddi_dma_numwin() failed\n");
    }

    if (ddi_dma_getwin(sp->cmd_dmahandle, sp->cmd_curwin,
        &sp->cmd_dma_offset, &sp->cmd_dma_len,
        &sp->cmd_dmacookies[0], &sp->cmd_ncookies) ==
           DDI_FAILURE) {
        cmn_err(CE_PANIC, "ddi_dma_getwin() failed\n");
    }
    goto get_dma_cookies;

    case DDI_DMA_MAPPED:
    sp->cmd_nwin = 1;
    sp->cmd_dma_len = 0;
    sp->cmd_dma_offset = 0;

get_dma_cookies:
    i = 0;
    sp->cmd_dmacount = 0;
    for (;;) {
        sp->cmd_dmacount += sp->cmd_dmacookies[i++].dmac_size;

        if (i == ISP_NDATASEGS || i == sp->cmd_ncookies)
        break;
        ddi_dma_nextcookie(sp->cmd_dmahandle,
        &sp->cmd_dmacookies[i]);
    }
    sp->cmd_cookie = i;
    sp->cmd_cookiecnt = i;

    sp->cmd_flags |= CFLAG_DMAVALID;
    pkt->pkt_resid = bp->b_bcount - sp->cmd_dmacount;
    return (1);

    case DDI_DMA_NORESOURCES:
    bioerror(bp, 0);
    break;

    case DDI_DMA_NOMAPPING:
    bioerror(bp, EFAULT);
    break;

    case DDI_DMA_TOOBIG:
    bioerror(bp, EINVAL);
    break;

    case DDI_DMA_INUSE:
    cmn_err(CE_PANIC, "ddi_dma_buf_bind_handle:"
        " DDI_DMA_INUSE impossible\n");

    default:
    cmn_err(CE_PANIC, "ddi_dma_buf_bind_handle:"
        " 0x%x impossible\n", i);
    }

    ddi_dma_free_handle(&sp->cmd_dmahandle);
    sp->cmd_dmahandle = NULL;
    sp->cmd_flags &= ~CFLAG_DMAVALID;
    return (0);
}

重新分配用于数据传送的 DMA 资源

对于先前分配的包含待传送数据的包,tran_init_pkt(9E) 入口点在满足以下条件时必须重新分配 DMA 资源:

重新分配下一个传送部分的 DMA 资源时,tran_init_pkt(9E) 必须在字段 pkt_resid 中返回未为此传送分配的 DMA 资源量。

如果在尝试移动 DMA 资源时出现错误,则 tran_init_pkt(9E) 决不能释放 scsi_pkt(9S)。在这种情况下,目标驱动程序负责释放包。

如果回调参数为 NULL_FUNC,则 tran_init_pkt(9E) 入口点决不能休眠或调用可能会休眠的任何函数。如果回调参数为 SLEEP_FUNC 并且资源不会立即可用,则 tran_init_pkt(9E) 入口点会休眠。除非无法满足请求,否则 tran_init_pkt() 将休眠,直到资源可用为止。


示例 18–4 HBA 驱动程序的 DMA 资源重新分配

static int
isp_i_dma_move(
    struct isp         *isp,
    struct scsi_pkt    *pkt,
    struct buf         *bp)
{
    struct isp_cmd     *sp  = (struct isp_cmd *)pkt->pkt_ha_private;
    int                i;

    ASSERT(sp->cmd_flags & CFLAG_COMPLETED);
    sp->cmd_flags &= ~CFLAG_COMPLETED;
   /*
    * If there are no more cookies remaining in this window,
    * must move to the next window first.
    */
    if (sp->cmd_cookie == sp->cmd_ncookies) {
   /*
    * For small pkts, leave things where they are
    */
    if (sp->cmd_curwin == sp->cmd_nwin && sp->cmd_nwin == 1)
        return (1);
   /*
    * At last window, cannot move
    */
    if (++sp->cmd_curwin >= sp->cmd_nwin)
        return (0);
    if (ddi_dma_getwin(sp->cmd_dmahandle, sp->cmd_curwin,
        &sp->cmd_dma_offset, &sp->cmd_dma_len,
        &sp->cmd_dmacookies[0], &sp->cmd_ncookies) ==
        DDI_FAILURE)
        return (0);
        sp->cmd_cookie = 0;
    } else {
   /*
    * Still more cookies in this window - get the next one
    */
    ddi_dma_nextcookie(sp->cmd_dmahandle,
        &sp->cmd_dmacookies[0]);
    }
   /*
    * Get remaining cookies in this window, up to our maximum
    */
    i = 0;
    for (;;) {
    sp->cmd_dmacount += sp->cmd_dmacookies[i++].dmac_size;
    sp->cmd_cookie++;
    if (i == ISP_NDATASEGS ||
        sp->cmd_cookie == sp->cmd_ncookies)
            break;
    ddi_dma_nextcookie(sp->cmd_dmahandle,
        &sp->cmd_dmacookies[i]);
    }
    sp->cmd_cookiecnt = i;
    pkt->pkt_resid = bp->b_bcount - sp->cmd_dmacount;
    return (1);
}

tran_destroy_pkt() 入口点

tran_destroy_pkt(9E) 入口点是用于取消分配 scsi_pkt(9S) 结构的 HBA 驱动程序函数。目标驱动程序调用 scsi_destroy_pkt(9F) 时,将会调用 tran_destroy_pkt() 入口点。

tran_destroy_pkt() 入口点必须释放已为包分配的所有 DMA 资源。如果释放了 DMA 资源并且所有高速缓存的数据在完成传送后仍然保留,则会进行隐式 DMA 同步。tran_destroy_pkt() 入口点通过调用 scsi_hba_pkt_free(9F) 释放 SCSI 包。


示例 18–5 HBA 驱动程序 tran_destroy_pkt (9E) 入口点

static void
isp_scsi_destroy_pkt(
    struct scsi_address    *ap,
    struct scsi_pkt    *pkt)
{
    struct isp_cmd *sp = (struct isp_cmd *)pkt->pkt_ha_private;
   /*
    * Free the DMA, if any
    */
    if (sp->cmd_flags & CFLAG_DMAVALID) {
        sp->cmd_flags &= ~CFLAG_DMAVALID;
        (void) ddi_dma_unbind_handle(sp->cmd_dmahandle);
        ddi_dma_free_handle(&sp->cmd_dmahandle);
        sp->cmd_dmahandle = NULL;
    }
   /*
    * Free the pkt
    */
    scsi_hba_pkt_free(ap, pkt);
}

tran_sync_pkt() 入口点

tran_sync_pkt(9E) 入口点可在 DMA 传送前后同步为 scsi_pkt(9S) 结构分配的 DMA 对象。目标驱动程序调用 scsi_sync_pkt(9F) 时,将会调用 tran_sync_pkt() 入口点。

如果数据传送方向是从设备到内存的 DMA 读取,则 tran_sync_pkt() 必须同步 CPU 的数据视图。如果数据传送方向是从内存到设备的 DMA 写入,则 tran_sync_pkt() 必须同步设备的数据视图。


示例 18–6 HBA 驱动程序 tran_sync_pkt (9E) 入口点

static void
isp_scsi_sync_pkt(
    struct scsi_address    *ap,
    struct scsi_pkt        *pkt)
{
    struct isp_cmd *sp = (struct isp_cmd *)pkt->pkt_ha_private;

    if (sp->cmd_flags & CFLAG_DMAVALID) {
        (void)ddi_dma_sync(sp->cmd_dmahandle, sp->cmd_dma_offset,
        sp->cmd_dma_len,
        (sp->cmd_flags & CFLAG_DMASEND) ?
        DDI_DMA_SYNC_FORDEV : DDI_DMA_SYNC_FORCPU);
    }
}

tran_dmafree() 入口点

tran_dmafree(9E) 入口点可取消分配已为 scsi_pkt(9S) 结构分配的 DMA 资源。目标驱动程序调用 scsi_dmafree(9F) 时,将会调用 tran_dmafree() 入口点。

tran_dmafree() 必须仅释放为 scsi_pkt(9S) 结构分配的 DMA 资源,而不释放 scsi_pkt(9S) 本身。释放 DMA 资源时,将隐式执行 DMA 同步。


注 –

scsi_pkt(9S) 在单独请求 tran_destroy_pkt(9E) 时释放。由于 tran_destroy_pkt() 还必须释放 DMA 资源,因此 HBA 驱动程序必须准确记录 scsi_pkt() 结构是否分配了 DMA 资源。



示例 18–7 HBA 驱动程序 tran_dmafree (9E) 入口点

static void
isp_scsi_dmafree(
    struct scsi_address    *ap,
    struct scsi_pkt        *pkt)
{
    struct isp_cmd    *sp = (struct isp_cmd *)pkt->pkt_ha_private;

    if (sp->cmd_flags & CFLAG_DMAVALID) {
        sp->cmd_flags &= ~CFLAG_DMAVALID;
        (void)ddi_dma_unbind_handle(sp->cmd_dmahandle);
        ddi_dma_free_handle(&sp->cmd_dmahandle);
        sp->cmd_dmahandle = NULL;
    }
}

命令传输

在命令传输过程中,HBA 驱动程序将执行以下步骤:

  1. 接受来自目标驱动程序的命令。

  2. 向设备硬件发出命令。

  3. 传送出现的所有中断。

  4. 管理超时。

tran_start() 入口点

调用 SCSI HBA 驱动程序的 tran_start(9E) 入口点可将 SCSI 命令传输到指定地址的目标。在目标驱动程序通过 HBA 驱动程序的 tran_init_pkt(9E) 入口点分配的 scsi_pkt(9S) 结构中,对 SCSI 命令进行了完整描述。如果该命令涉及数据传送,则还必须为 scsi_pkt(9S) 结构分配 DMA 资源。

目标驱动程序调用 scsi_transport(9F) 时,将会调用 tran_start() 入口点。

tran_start() 应执行基本错误检查以及命令要求的任何初始化操作。scsi_pkt(9S) 结构的 pkt_flags 字段中的 FLAG_NOINTR 标志会影响 tran_start() 的行为。如果未设置 FLAG_NOINTR ,则 tran_start() 必须将命令排队以在硬件上执行并立即返回。完成命令后,HBA 驱动程序应调用 pkt 完成例程。

如果设置了 FLAG_NOINTR,则 HBA 驱动程序不会调用 pkt 完成例程。

以下示例说明如何处理 tran_start(9E) 入口点。ISP 硬件按目标设备提供了队列。对于只能管理一个活动的未完成命令的设备,驱动程序通常需要管理按目标的队列。然后,驱动程序会在完成当前命令后以循环方式启动新命令。


示例 18–8 HBA 驱动程序 tran_start (9E) 入口点

static int
isp_scsi_start(
    struct scsi_address    *ap,
    struct scsi_pkt        *pkt)
{
    struct isp_cmd         *sp;
    struct isp             *isp;
    struct isp_request     *req;
    u_long                 cur_lbolt;
    int                    xfercount;
    int                    rval = TRAN_ACCEPT;
    int                    i;

    sp = (struct isp_cmd *)pkt->pkt_ha_private;
    isp = (struct isp *)ap->a_hba_tran->tran_hba_private;

    sp->cmd_flags = (sp->cmd_flags & ~CFLAG_TRANFLAG) |
                CFLAG_IN_TRANSPORT;
    pkt->pkt_reason = CMD_CMPLT;
   /*
    * set up request in cmd_isp_request area so it is ready to
    * go once we have the request mutex
    */
    req = &sp->cmd_isp_request;

    req->req_header.cq_entry_type = CQ_TYPE_REQUEST;
    req->req_header.cq_entry_count = 1;
    req->req_header.cq_flags        = 0;
    req->req_header.cq_seqno = 0;
    req->req_reserved = 0;
    req->req_token = (opaque_t)sp;
    req->req_target = TGT(sp);
    req->req_lun_trn = LUN(sp);
    req->req_time = pkt->pkt_time;
    ISP_SET_PKT_FLAGS(pkt->pkt_flags, req->req_flags);
   /*
    * Set up data segments for dma transfers.
    */
    if (sp->cmd_flags & CFLAG_DMAVALID) {

        if (sp->cmd_flags & CFLAG_CMDIOPB) {
            (void) ddi_dma_sync(sp->cmd_dmahandle,
            sp->cmd_dma_offset, sp->cmd_dma_len,
            DDI_DMA_SYNC_FORDEV);
        }

        ASSERT(sp->cmd_cookiecnt > 0 &&
            sp->cmd_cookiecnt <= ISP_NDATASEGS);

        xfercount = 0;
        req->req_seg_count = sp->cmd_cookiecnt;
        for (i = 0; i < sp->cmd_cookiecnt; i++) {
            req->req_dataseg[i].d_count =
            sp->cmd_dmacookies[i].dmac_size;
            req->req_dataseg[i].d_base =
            sp->cmd_dmacookies[i].dmac_address;
            xfercount +=
            sp->cmd_dmacookies[i].dmac_size;
        }

        for (; i < ISP_NDATASEGS; i++) {
            req->req_dataseg[i].d_count = 0;
            req->req_dataseg[i].d_base = 0;
        }

        pkt->pkt_resid = xfercount;

        if (sp->cmd_flags & CFLAG_DMASEND) {
            req->req_flags |= ISP_REQ_FLAG_DATA_WRITE;
        } else {
            req->req_flags |= ISP_REQ_FLAG_DATA_READ;
        }
    } else {
        req->req_seg_count = 0;
        req->req_dataseg[0].d_count = 0;
    }
   /*
    * Set up cdb in the request
    */
    req->req_cdblen = sp->cmd_cdblen;
    bcopy((caddr_t)pkt->pkt_cdbp, (caddr_t)req->req_cdb,
    sp->cmd_cdblen);
   /*
    * Start the cmd.  If NO_INTR, must poll for cmd completion.
    */
    if ((pkt->pkt_flags & FLAG_NOINTR) == 0) {
        mutex_enter(ISP_REQ_MUTEX(isp));
        rval = isp_i_start_cmd(isp, sp);
        mutex_exit(ISP_REQ_MUTEX(isp));
    } else {
        rval = isp_i_polled_cmd_start(isp, sp);
    }
    return (rval);
}

中断处理程序和命令完成

中断处理程序必须检查设备状态,以确保设备正在生成相关中断。另外,中断处理程序还必须检查出现的全部错误,并传送设备生成的所有中断。

如果传送了数据,则应检查硬件以确定实际传送的数据量。scsi_pkt(9S) 结构中的 pkt_resid 字段应该设置为剩余未传送的数据量。

通过 tran_init_pkt(9E) 分配 DMA 资源时,使用 PKT_CONSISTENT 标志标记的命令需要特殊处理。HBA 驱动程序必须确保在执行目标驱动程序的命令完成回调之前,正确同步针对该命令的数据传送。

完成命令后,需要按照以下两个要求执行操作:

如有可能,在调用 PKT_COMP 命令完成回调之前,请在硬件上启动新命令。该命令完成处理可能需要大量时间。通常,目标驱动程序会调用函数(如 biodone(9F) 和可能会调用的 scsi_transport(9F))来启动新命令。

如果此中断是由该驱动程序请求的,则中断处理程序必须返回 DDI_INTR_CLAIMED。否则,处理程序会返回 DDI_INTR_UNCLAIMED

以下示例说明了 SCSI HBA isp 驱动程序的中断处理程序。如果 attach(9E) 中添加了中断处理程序,则应设置 caddr_t 参数。此参数通常是一个指向按实例分配的状态结构的指针。


示例 18–9 HBA 驱动程序中断处理程序

static u_int
isp_intr(caddr_t arg)
{
    struct isp_cmd         *sp;
    struct isp_cmd         *head, *tail;
    u_short                response_in;
    struct isp_response    *resp;
    struct isp             *isp = (struct isp *)arg;
    struct isp_slot        *isp_slot;
    int                    n;

    if (ISP_INT_PENDING(isp) == 0) {
        return (DDI_INTR_UNCLAIMED);
    }

    do {
again:
       /*
        * head list collects completed packets for callback later
        */
        head = tail = NULL;
       /*
        * Assume no mailbox events (e.g., mailbox cmds, asynch
        * events, and isp dma errors) as common case.
        */
        if (ISP_CHECK_SEMAPHORE_LOCK(isp) == 0) {
            mutex_enter(ISP_RESP_MUTEX(isp));
           /*
            * Loop through completion response queue and post
            * completed pkts.  Check response queue again
            * afterwards in case there are more.
            */
            isp->isp_response_in =
            response_in = ISP_GET_RESPONSE_IN(isp);
           /*
            * Calculate the number of requests in the queue
            */
            n = response_in - isp->isp_response_out;
            if (n < 0) {
                n = ISP_MAX_REQUESTS -
                isp->isp_response_out + response_in;
            }
            while (n-- > 0) {
                ISP_GET_NEXT_RESPONSE_OUT(isp, resp);
                sp = (struct isp_cmd *)resp->resp_token;
               /*
                * Copy over response packet in sp
                */
                isp_i_get_response(isp, resp, sp);
            }
            if (head) {
                tail->cmd_forw = sp;
                tail = sp;
                tail->cmd_forw = NULL;
            } else {
                tail = head = sp;
                sp->cmd_forw = NULL;
            }
            ISP_SET_RESPONSE_OUT(isp);
            ISP_CLEAR_RISC_INT(isp);
            mutex_exit(ISP_RESP_MUTEX(isp));

            if (head) {
                isp_i_call_pkt_comp(isp, head);
            }
        } else {
            if (isp_i_handle_mbox_cmd(isp) != ISP_AEN_SUCCESS) {
                return (DDI_INTR_CLAIMED);
            }
           /*
            * if there was a reset then check the response
            * queue again
            */
            goto again;    
        }

    } while (ISP_INT_PENDING(isp));

    return (DDI_INTR_CLAIMED);
}

static void
isp_i_call_pkt_comp(
    struct isp             *isp,
    struct isp_cmd         *head)
{
    struct isp             *isp;
    struct isp_cmd         *sp;
    struct scsi_pkt        *pkt;
    struct isp_response    *resp;
    u_char                 status;

    while (head) {
        sp = head;
        pkt = sp->cmd_pkt;
        head = sp->cmd_forw;

        ASSERT(sp->cmd_flags & CFLAG_FINISHED);

        resp = &sp->cmd_isp_response;

        pkt->pkt_scbp[0] = (u_char)resp->resp_scb;
        pkt->pkt_state = ISP_GET_PKT_STATE(resp->resp_state);
        pkt->pkt_statistics = (u_long)
            ISP_GET_PKT_STATS(resp->resp_status_flags);
        pkt->pkt_resid = (long)resp->resp_resid;
       /*
        * If data was xferred and this is a consistent pkt,
        * do a dma sync
        */
        if ((sp->cmd_flags & CFLAG_CMDIOPB) &&
            (pkt->pkt_state & STATE_XFERRED_DATA)) {
                (void) ddi_dma_sync(sp->cmd_dmahandle,
                sp->cmd_dma_offset, sp->cmd_dma_len,
                DDI_DMA_SYNC_FORCPU);
        }

        sp->cmd_flags = (sp->cmd_flags & ~CFLAG_IN_TRANSPORT) |
            CFLAG_COMPLETED;
       /*
        * Call packet completion routine if FLAG_NOINTR is not set.
        */
        if (((pkt->pkt_flags & FLAG_NOINTR) == 0) &&
            pkt->pkt_comp) {
                (*pkt->pkt_comp)(pkt);
        }
    }
}

超时处理程序

HBA 驱动程序负责强制执行超时设置。除非在 scsi_pkt(9S) 结构中指定了零超时,否则必须在指定时间内完成命令。

如果命令超时,则 HBA 驱动程序应将 scsi_pkt(9S) 标记为 pkt_reason=CMD_TIMEOUT,而且将 pkt_statistics 的值设置为与 STAT_TIMEOUT 进行或运算所得的值。另外,HBA 驱动程序还应尝试恢复目标和总线。如果恢复能够成功执行,则驱动程序应使用 pkt_statisticsSTAT_BUS_RESETSTAT_DEV_RESET 进行或运算所得的值标记 scsi_pkt(9S)

完成恢复尝试后,HBA 驱动程序应调用命令完成回调。


注 –

如果恢复不成功或未尝试恢复,则目标驱动程序可能会通过调用 scsi_reset(9F) 尝试从超时中恢复。


ISP 硬件直接管理命令超时,并会返回超时命令的必需状态。isp 样例驱动程序的超时处理程序每 60 秒才检查一次活动命令的超时状态。

isp 样例驱动程序使用 timeout(9F) 功能来安排内核每 60 秒调用一次超时处理程序。caddr_t 参数是在执行 attach(9E) 期间初始化超时值时设置的参数。在这种情况下,caddr_t 参数是一个指向按驱动程序实例分配的状态结构的指针。

如果 ISP 硬件未将超时命令作为超时项返回,则表明出现了问题。该硬件将无法正常工作并需要重置。

功能管理

以下各节讨论了功能管理。

tran_getcap() 入口点

SCSI HBA 驱动程序的 tran_getcap(9E) 入口点通过 scsi_ifgetcap(9F) 进行调用。目标驱动程序调用 scsi_ifgetcap() 可确定 SCSA 定义的一组功能中其中一个的当前值。

目标驱动程序可以通过将 whom 参数设置为非零值来请求特定目标的功能的当前设置。whom 值为零表明请求 SCSI 总线或适配器硬件的一般功能的当前设置。

对于未定义的功能或所请求的功能的当前值,tran_getcap() 入口点应返回 -1

HBA 驱动程序可以使用函数 scsi_hba_lookup_capstr(9F) 来比较功能字符串和已定义功能的标准集。


示例 18–10 HBA 驱动程序 tran_getcap (9E) 入口点

static int
isp_scsi_getcap(
    struct scsi_address    *ap,
    char                   *cap,
    int                    whom)
{
    struct isp             *isp;
    int                    rval = 0;
    u_char                 tgt = ap->a_target;
   /*
    * We don't allow getting capabilities for other targets
    */
    if (cap == NULL || whom  == 0) {
        return (-1);
    }
    isp = (struct isp *)ap->a_hba_tran->tran_hba_private;
    ISP_MUTEX_ENTER(isp);

    switch (scsi_hba_lookup_capstr(cap)) {
    case SCSI_CAP_DMA_MAX:
        rval = 1 << 24; /* Limit to 16MB max transfer */
        break;
    case SCSI_CAP_MSG_OUT:
        rval = 1;
        break;
    case SCSI_CAP_DISCONNECT:
        if ((isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_DR) == 0) {
            break;
        } else if (
            (isp->isp_cap[tgt] & ISP_CAP_DISCONNECT) == 0) {
            break;
        }
        rval = 1;
        break;
    case SCSI_CAP_SYNCHRONOUS:
        if ((isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_SYNC) == 0) {
            break;
        } else if (
            (isp->isp_cap[tgt] & ISP_CAP_SYNC) == 0) {
            break;
        }
        rval = 1;
        break;
    case SCSI_CAP_WIDE_XFER:
        if ((isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_WIDE) == 0) {
            break;
        } else if (
            (isp->isp_cap[tgt] & ISP_CAP_WIDE) == 0) {
            break;
        }
        rval = 1;
        break;
    case SCSI_CAP_TAGGED_QING:
        if ((isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_DR) == 0 ||
            (isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_TAG) == 0) {
            break;
        } else if (
            (isp->isp_cap[tgt] & ISP_CAP_TAG) == 0) {
            break;
        }
        rval = 1;
        break;
    case SCSI_CAP_UNTAGGED_QING:
        rval = 1;
        break;
    case SCSI_CAP_PARITY:
        if (isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_PARITY) {
            rval = 1;
        }
        break;
    case SCSI_CAP_INITIATOR_ID:
        rval = isp->isp_initiator_id;
        break;
    case SCSI_CAP_ARQ:
        if (isp->isp_cap[tgt] & ISP_CAP_AUTOSENSE) {
            rval = 1;
        }
        break;
    case SCSI_CAP_LINKED_CMDS:
        break;
    case SCSI_CAP_RESET_NOTIFICATION:
        rval = 1;
        break;
    case SCSI_CAP_GEOMETRY:
        rval = (64 << 16) | 32;
        break;
    default:
        rval = -1;
        break;
    }

    ISP_MUTEX_EXIT(isp);
    return (rval);
}

tran_setcap() 入口点

SCSI HBA 驱动程序的 tran_setcap(9E) 入口点通过 scsi_ifsetcap(9F) 进行调用。目标驱动程序调用 scsi_ifsetcap() 可更改 SCSA 定义的一组功能中其中一个的当前值。

目标驱动程序可能会通过将 whom 参数设置为非零值来请求为特定目标设置新值。whom 值为零通常表明请求为 SCSI 总线或适配器硬件设置新值。

tran_setcap() 应相应地返回以下值:

HBA 驱动程序可以使用函数 scsi_hba_lookup_capstr(9F) 来比较功能字符串和已定义功能的标准集。


示例 18–11 HBA 驱动程序 tran_setcap (9E) 入口点

static int
isp_scsi_setcap(
    struct scsi_address    *ap,
    char                   *cap,
    int                    value,
    int                    whom)
{
    struct isp             *isp;
    int                    rval = 0;
    u_char                 tgt = ap->a_target;
    int                    update_isp = 0;
   /*
    * We don't allow setting capabilities for other targets
    */
    if (cap == NULL || whom == 0) {
        return (-1);
    }

    isp = (struct isp *)ap->a_hba_tran->tran_hba_private;
    ISP_MUTEX_ENTER(isp);

    switch (scsi_hba_lookup_capstr(cap)) {
    case SCSI_CAP_DMA_MAX:
    case SCSI_CAP_MSG_OUT:
    case SCSI_CAP_PARITY:
    case SCSI_CAP_UNTAGGED_QING:
    case SCSI_CAP_LINKED_CMDS:
    case SCSI_CAP_RESET_NOTIFICATION:
   /*
    * None of these are settable through
    * the capability interface.
    */
        break;
    case SCSI_CAP_DISCONNECT:
        if ((isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_DR) == 0) {
                break;
        } else {
            if (value) {
                isp->isp_cap[tgt] |= ISP_CAP_DISCONNECT;
            } else {
                isp->isp_cap[tgt] &= ~ISP_CAP_DISCONNECT;
            }
        }
        rval = 1;
        break;
    case SCSI_CAP_SYNCHRONOUS:
        if ((isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_SYNC) == 0) {
                break;
        } else {
            if (value) {
                isp->isp_cap[tgt] |= ISP_CAP_SYNC;
            } else {
                isp->isp_cap[tgt] &= ~ISP_CAP_SYNC;
            }
        }
        rval = 1;
        break;
    case SCSI_CAP_TAGGED_QING:
        if ((isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_DR) == 0 ||
            (isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_TAG) == 0) {
                break;
        } else {
            if (value) {
                isp->isp_cap[tgt] |= ISP_CAP_TAG;
            } else {
            isp->isp_cap[tgt] &= ~ISP_CAP_TAG;
            }
        }
        rval = 1;
        break;
    case SCSI_CAP_WIDE_XFER:
        if ((isp->isp_target_scsi_options[tgt] &
            SCSI_OPTIONS_WIDE) == 0) {
                break;
        } else {
            if (value) {
                isp->isp_cap[tgt] |= ISP_CAP_WIDE;
            } else {
                isp->isp_cap[tgt] &= ~ISP_CAP_WIDE;
            }
        }
        rval = 1;
        break;
    case SCSI_CAP_INITIATOR_ID:
        if (value < N_ISP_TARGETS_WIDE) {
            struct isp_mbox_cmd mbox_cmd;
            isp->isp_initiator_id = (u_short) value;
           /*
            * set Initiator SCSI ID
            */
            isp_i_mbox_cmd_init(isp, &mbox_cmd, 2, 2,
            ISP_MBOX_CMD_SET_SCSI_ID,
            isp->isp_initiator_id,
            0, 0, 0, 0);
            if (isp_i_mbox_cmd_start(isp, &mbox_cmd) == 0) {
                rval = 1;
            }
        }
        break;
    case SCSI_CAP_ARQ:
        if (value) {
            isp->isp_cap[tgt] |= ISP_CAP_AUTOSENSE;
        } else {
            isp->isp_cap[tgt] &= ~ISP_CAP_AUTOSENSE;
        }
        rval = 1;
        break;
    default:
        rval = -1;
        break;
    }
    ISP_MUTEX_EXIT(isp);
    return (rval);
}

中止和重置管理

以下各节讨论了 SCSI HBA 的中止入口点和重置入口点。

tran_abort() 入口点

调用 SCSI HBA 驱动程序的 tran_abort(9E) 入口点可中止当前正在传输给特定目标的所有命令。目标驱动程序调用 scsi_abort(9F) 时,将会调用此入口点。

tran_abort() 入口点会尝试中止 pkt 参数表示的命令。如果 pkt 参数为 NULL,则 tran_abort() 会尝试中止传输层中针对特定目标或逻辑单元的所有未完成命令。

每个已成功中止的命令都必须标记为 pkt_reason CMD_ABORTED 以及 pkt_statisticsSTAT_ABORTED 进行或运算所得的值。

tran_reset() 入口点

调用 SCSI HBA 驱动程序的 tran_reset(9E) 入口点可重置 SCSI 总线或特定的 SCSI 目标设备。目标驱动程序调用 scsi_reset(9F) 时,将会调用此入口点。

如果级别为 RESET_ALL(),则 tran_reset 入口点必须重置 SCSI 总线。如果级别为 RESET_TARGET,则仅有特定目标或逻辑单元必须重置。

受重置影响的活动命令必须带有 pkt_reason CMD_RESET 标记。重置类型可确定应使用 STAT_BUS_RESET 还是使用 STAT_DEV_RESETpkt_statistics 进行或运算。

在目标上尚未处于活动状态的传输层中的命令必须标记为 pkt_reason CMD_RESET 以及 pkt_statisticsSTAT_ABORTED 进行或运算所得的值。

tran_bus_reset() 入口点

tran_bus_reset(9E) 必须重置 SCSI 总线而不重置目标。

#include <sys/scsi/scsi.h>

int tran_bus_reset(dev_info_t *hba-dip, int level);

其中:

*hba-dip

与 SCSI HBA 关联的指针

level

必须设置为 RESET_BUS,以便仅重置 SCSI 总线而不重置目标。

执行 HBA 驱动程序的 attach(9E) 的过程中,应初始化 scsi_hba_tran(9S) 结构中的 tran_bus_reset() 向量。该向量应指向用户启动总线重置时将调用的 HBA 入口点。

实现特定于硬件。如果 HBA 驱动程序无法在不影响目标的情况下重置 SCSI 总线,则驱动程序将无法执行 RESET_BUS 或不会初始化此向量。

tran_reset_notify() 入口点

重置 SCSI 总线时,请使用 tran_reset_notify(9E) 入口点。此函数将请求 SCSI HBA 驱动程序通过回调来通知目标驱动程序。


示例 18–12 HBA 驱动程序 tran_reset_notify (9E) 入口点

isp_scsi_reset_notify(
    struct scsi_address    *ap,
    int                    flag,
    void                   (*callback)(caddr_t),
    caddr_t                arg)
{
    struct isp                       *isp;
    struct isp_reset_notify_entry    *p, *beforep;
    int                              rval = DDI_FAILURE;

    isp = (struct isp *)ap->a_hba_tran->tran_hba_private;
    mutex_enter(ISP_REQ_MUTEX(isp));
   /*
    * Try to find an existing entry for this target
    */
    p = isp->isp_reset_notify_listf;
    beforep = NULL;

    while (p) {
        if (p->ap == ap)
            break;
        beforep = p;
        p = p->next;
    }

    if ((flag & SCSI_RESET_CANCEL) && (p != NULL)) {
        if (beforep == NULL) {
            isp->isp_reset_notify_listf = p->next;
        } else {
            beforep->next = p->next;
        }
        kmem_free((caddr_t)p, sizeof (struct
            isp_reset_notify_entry));
        rval = DDI_SUCCESS;
    } else if ((flag & SCSI_RESET_NOTIFY) && (p == NULL)) {
        p = kmem_zalloc(sizeof (struct isp_reset_notify_entry),
            KM_SLEEP);
        p->ap = ap;
        p->callback = callback;
        p->arg = arg;
        p->next = isp->isp_reset_notify_listf;
        isp->isp_reset_notify_listf = p;
        rval = DDI_SUCCESS;
    }

    mutex_exit(ISP_REQ_MUTEX(isp));
    return (rval);
}

动态重新配置

要支持最少的一组热插拔操作,驱动程序可能需要实现对总线静止、总线取消静止和总线重置的支持。scsi_hba_tran(9S) 结构支持这些操作。如果硬件不要求静止、取消静止或重置等操作,则无需对驱动程序进行任何更改。

scsi_hba_tran 结构包含以下字段:

int (*tran_quiesce)(dev_info_t *hba-dip);
int (*tran_unquiesce)(dev_info_t *hba-dip);
int (*tran_bus_reset)(dev_info_t *hba-dip, int level);

这些接口将使 SCSI 总线处于静止状态和取消其静止状态。

#include <sys/scsi/scsi.h>

int prefixtran_quiesce(dev_info_t *hba-dip);
int prefixtran_unquiesce(dev_info_t *hba-dip);

tran_quiesce(9E)tran_unquiesce(9E) 用于不适于热插拔的 SCSI 设备。HBA 驱动程序必须实现这些函数才能支持动态重新配置 (dynamic reconfiguration, DR)。

在执行 attach(9E) 的过程中,应将 scsi_hba_tran(9S) 结构中的 tran_quiesce()tran_unquiesce() 向量初始化为指向 HBA 入口点。用户启动静止和取消静止操作时,将会调用这些函数。

tran_quiesce() 入口点用于在重新配置连接到 SCSI 总线的设备之前和期间停止 SCSI 总线上的所有活动。完成重新配置操作后,SCSA 框架可调用 tran_unquiesce() 入口点来恢复 SCSI 总线上的活动。

返回成功信息之前,HBA 驱动程序需要处理 tran_quiesce(),方法是等待所有未完成的命令完成。 驱动程序使总线处于静止状态后,必须将任何新的 I/O 请求排入队列,直到 SCSA 框架调用对应的 tran_unquiesce() 入口点为止。

HBA 驱动程序通过启动队列中的所有目标驱动程序 I/O 请求来处理对 tran_unquiesce() 的调用。

SCSI HBA 驱动程序特定问题

本节介绍特定于 SCSI HBA 驱动程序的问题。

安装 HBA 驱动程序

SCSI HBA 驱动程序的安装方式与叶驱动程序类似。请参见第 21 章。差别在于 add_drv(1M) 命令必须将驱动程序类指定为 SCSI,如:

# add_drv -m" * 0666 root root" -i'"pci1077,1020"' -c scsi isp

HBA 配置属性

连接 HBA 设备实例时,scsi_hba_attach_setup(9F) 会为该 HBA 实例创建许多 SCSI 配置属性。仅当没有现有同名属性附加到 HBA 实例时,才会创建特定属性。此限制可避免覆盖 HBA 配置文件中的任何缺省属性值。

HBA 驱动程序必须使用 ddi_prop_get_int(9F) 检索每个属性。然后,HBA 驱动程序会修改或接受这些属性的缺省值来配置其特定操作。

scsi-reset-delay 属性

scsi-reset-delay 属性是一个整数,用于指定由 SCSI 总线或 SCSI 设备造成的重置延迟的恢复时间(以毫秒为单位)。

scsi-options 属性

scsi-options 属性是一个整数,用于通过单独定义的位来指定多个选项:

按目标的 scsi-options

HBA 驱动程序可能支持以下格式的按目标的 scsi-options 功能:

target<n>-scsi-options=<hex value>

在此示例中,< n> 是指目标 ID。如果定义了按目标的 scsi-options 属性,则 HBA 驱动程序将使用该值,而不使用按 HBA 驱动程序实例的 scsi-options 属性。例如,如果仅需针对某个特定目标设备禁用同步数据传送,则该方法可提供更准确的控制。按目标的 scsi-options 属性可以在 driver.conf(4) 文件中定义。

以下示例说明了按目标的 scsi-options 属性定义,用于禁用目标设备 3 的同步数据传送:

target3-scsi-options=0x2d8

x86 目标驱动程序配置属性

某些 x86 SCSI 目标驱动程序(如 cmdk 磁盘驱动程序)使用以下配置属性:

如果使用 cmdk 样例驱动程序编写适用于 x86 平台的 HBA 驱动程序,则必须在 driver.conf(4) 文件中定义所有相应属性。


注 –

这些属性定义应仅显示在 HBA 驱动程序的 driver.conf(4) 文件中。HBA 驱动程序本身不应以任何方式检查或尝试解释这些属性。这些属性仅是建议性的,并且用作 cmdk 驱动程序的附件。不应以任何方式依赖这些属性。在将来的发行版中可能不会使用这些属性定义。


disk 属性可用于定义 cmdk 支持的磁盘类型。对于 SCSI HBA,disk 属性的唯一可能值是:

queue 属性用于定义磁盘驱动程序如何在执行 strategy(9E) 的过程中对传入请求的队列进行排序。以下是两个可能的值:

flow_control 属性用于定义如何将命令传输到 HBA 驱动程序。以下是三个可能的值:

以下示例是一个供 x86 HBA PCI 设备使用的 driver.conf(4) 文件,该设备设计用于 cmdk 样例驱动程序:

#
# config file for ISP 1020 SCSI HBA driver     
#
       flow_control="dsngl" queue="qsort" disk="scdk"
       scsi-initiator-id=7;

排队支持

有关标记排队的定义,请参阅 SCSI-2 规范。要支持标记排队,请首先检查 scsi_options 标志 SCSI_OPTIONS_TAG,查看是否在全局范围内启用了标记排队。接下来,检查目标是否为 SCSI-2 设备以及目标是否启用了标记排队。如果这些条件全部符合,请通过 scsi_ifsetcap(9F) 尝试启用标记排队。

如果标记排队失败,则可尝试设置无标记排队。在此模式下,可提交主机适配器驱动程序所需的或对其最适用的尽可能多的命令。与标记排队不同,主机适配器随后将按照一次一个命令的方式对目标命令进行排队。在标记排队中,主机适配器将提交尽可能多的命令,直到目标指示队列已满为止。

第 19 章 网络设备驱动程序

要为 Solaris OS 编写网络驱动程序,请使用 Solaris 通用 LAN 驱动程序 (Generic LAN Driver, GLD) 框架。

GLDv3 网络设备驱动程序框架

GLDv3 框架是 MAC 插件和 MAC 驱动程序服务例程与结构的基于函数调用的接口。GLDv3 框架代表符合 GLDv3 的驱动程序实现必要的 STREAMS 入口点,并处理 DLPI 兼容性。

本节讨论以下主题:

GLDv3 MAC 注册

GLDv3 为使用 MAC_PLUGIN_IDENT_ETHER 插件类型注册的驱动程序定义驱动程序 API。

GLDv3 MAC 注册过程

GLDv3 设备驱动程序必须执行以下步骤来注册 MAC 层:

GLDv3 MAC 注册函数

GLDv3 接口包括在使用 MAC 层注册过程中通告的驱动程序入口点和驱动程序调用的 MAC 入口点。

mac_init_ops() 和·mac_fini_ops() 函数

void mac_init_ops(struct dev_ops *ops, const char *name);

GLDv3 设备驱动程序必须在调用 mod_install(9F) 之前在其 _init(9E) 入口点中调用 mac_init_ops(9F) 函数。

void mac_fini_ops(struct dev_ops *ops);

GLDv3 设备驱动程序必须在调用 mod_remove(9F) 之后在其 _fini(9E) 入口点中调用 mac_fini_ops(9F) 函数。


示例 19–1 mac_init_ops() 和·mac_fini_ops() 函数

int
_init(void)
{
        int     rv;
        mac_init_ops(&xx_devops, "xx");
        if ((rv = mod_install(&xx_modlinkage)) != DDI_SUCCESS) {
                mac_fini_ops(&xx_devops);
        }
        return (rv);
}

int
_fini(void)
{
        int     rv;
        if ((rv = mod_remove(&xx_modlinkage)) == DDI_SUCCESS) {
                mac_fini_ops(&xx_devops);
        }
        return (rv);
}

mac_alloc()mac_free() 函数

mac_register_t *mac_alloc(uint_t version);

mac_alloc(9F) 函数分配一个新的 mac_register 结构,并返回此结构的指针。在将新结构传递给 mac_register() 之前初始化结构成员。在 mac_alloc() 返回之前,MAC 层会初始化 MAC 专用元素。 version 的值必须是 MAC_VERSION_V1

void mac_free(mac_register_t *mregp);

mac_free(9F) 函数释放此前由 mac_alloc() 分配的 mac_register 结构。

mac_register()mac_unregister () 函数

int mac_register(mac_register_t *mregp, mac_handle_t *mhp);

为了使用 MAC 层注册新实例,GLDv3 驱动程序必须在 attach(9E) 入口点中调用 mac_register(9F) 函数。mregp 参数是指向 mac_register 注册信息结构的指针。 在成功时,mhp 参数是指向新 MAC 实例的 MAC 句柄指针。mac_tx_update()mac_link_update ()mac_rx() 等其他例程需要此句柄。


示例 19–2 mac_alloc()mac_register ()mac_free() 函数及 mac_register 结构

int
xx_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
        mac_register_t        *macp;

/* ... */

        if ((macp = mac_alloc(MAC_VERSION)) == NULL) {
                xx_error(dip, "mac_alloc failed");
                goto failed;
        }

        macp->m_type_ident = MAC_PLUGIN_IDENT_ETHER;
        macp->m_driver = xxp;
        macp->m_dip = dip;
        macp->m_src_addr = xxp->xx_curraddr;
        macp->m_callbacks = &xx_m_callbacks;
        macp->m_min_sdu = 0;
        macp->m_max_sdu = ETHERMTU;
        macp->m_margin = VLAN_TAGSZ;

        if (mac_register(macp, &xxp->xx_mh) == DDI_SUCCESS) {
                mac_free(macp);
                return (DDI_SUCCESS);
        }

/* failed to register with MAC */
        mac_free(macp);
failed:
        /* ... */
}

int mac_unregister(mac_handle_t mh);

mac_unregister(9F) 函数取消注册此前通过 mac_register() 注册的 MAC 实例。 mh 参数是 mac_register() 分配的 MAC 句柄。 从 detach(9E) 入口点调用 mac_unregister()


示例 19–3 mac_unregister() 函数

int
xx_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
        xx_t        *xxp; /* driver soft state */

        /* ... */

        switch (cmd) {
        case DDI_DETACH:

                if (mac_unregister(xxp->xx_mh) != 0) {
                        return (DDI_FAILURE);
                }
        /* ... */
}

GLDv3 MAC 注册数据结构

本节介绍的结构是在 sys/mac_provider.h 头文件中定义的。 在 GLDv3 驱动程序中包含以下三个 MAC 头文件:sys/mac.hsys/mac_ether.hsys/mac_provider.h. 切勿包含其他任何于 MAC 相关的头文件。

mac_register(9S) 数据结构是 MAC 注册信息结构,由 mac_alloc() 分配,传递至 mac_register()。 ·在将新结构传递给 mac_register() 之前初始化结构成员。在 mac_alloc() 返回之前,MAC 层会初始化 MAC 专用元素。 m_version 结构成员是 MAC 版本。 切勿修改 MAC 版本。m_type_ident 结构成员是 MAC 类型标识符。 将 MAC 类型标识符设置为 MAC_PLUGIN_IDENT_ETHERmac_register 结构的 m_callbacks 成员是指向 mac_callbacks 结构的一个实例的指针。

mac_callbacks(9S) 数据结构是设备驱动程序用于向 MAC 层公开其入口点的结构。 MAC 层使用这些入口点来控制驱动程序。这些入口点用于完成启动和停止适配器、管理多点传送地址、设置混杂模式、查询适配器功能、获取和设置属性等任务。 有关必需和可选 GLDv3 入口点的完整列表,请参见表 19–1。在 mac_register 结构的 m_callbacks 字段中提供指向 mac_callbacks 结构的指针。

mac_callbacks 结构的 mc_callbacks 成员是一个位掩码,结合了指定驱动程序将实现哪些可选入口点的以下标志。mac_callbacks 结构的其他成员是驱动程序各入口点的指针。

MC_IOCTL

显示 mc_ioctl() 入口点。

MC_GETCAPAB

显示 mc_getcapab() 入口点。

MC_SETPROP

显示·mc_setprop() 入口点。

MC_GETPROP

显示 mc_getprop() 入口点。

MC_PROPINFO

显示 mc_propinfo() 入口点。

MC_PROPERTIES

显示所有属性入口点。设置·MC_PROPERTIES 等同于设置全部三个标志:MC_SETPROP MC_GETPROPMC_PROPINFO


示例 19–4 mac_callbacks 结构

#define XX_M_CALLBACK_FLAGS \
    (MC_IOCTL | MC_GETCAPAB | MC_PROPERTIES)

static mac_callbacks_t xx_m_callbacks = {
        XX_M_CALLBACK_FLAGS,
        xx_m_getstat,     /* mc_getstat() */
        xx_m_start,       /* mc_start() */
        xx_m_stop,        /* mc_stop() */
        xx_m_promisc,     /* mc_setpromisc() */
        xx_m_multicst,    /* mc_multicst() */
        xx_m_unicst,      /* mc_unicst() */
        xx_m_tx,          /* mc_tx() */
        NULL,             /* Reserved, do not use */
        xx_m_ioctl,       /* mc_ioctl() */
        xx_m_getcapab,    /* mc_getcapab() */
        NULL,             /* Reserved, do not use */
        NULL,             /* Reserved, do not use */
        xx_m_setprop,     /* mc_setprop() */
        xx_m_getprop,     /* mc_getprop() */
        xx_m_propinfo     /* mc_propinfo() */
};

GLDv3 功能

GLDv3 实现一种功能机制,允许框架查询和启用 GLDv3 驱动程序支持的功能。使用 mc_getcapab(9E) 入口点报告功能。如果驱动程序支持某种功能,请通过 mc_getcapab() 传递关于该功能的信息,例如特定于功能的入口点或标志。传递一个指向 mac_callback 结构中 mc_getcapab() 入口点的指针。有关 mac_callbacks 结构的更多信息,请参见GLDv3 MAC 注册数据结构

boolean_t mc_getcapab(void *driver_handle, mac_capab_t cap, void *cap_data);

cap 参数指定所查询的功能类型。 cap 的值可以是 MAC_CAPAB_HCKSUM(硬件校验和负载转移)或 MAC_CAPAB_LSO(大段负载转移)。使用 cap_data 参数将功能数据返回框架。

如果驱动程序支持 cap 功能,则 mc_getcapab() 入口点必须返回 B_TRUE。如果驱动程序不支持 cap 功能,则 mc_getcapab() 必须返回 B_FALSE


示例 19–5 mc_getcapab() 入口点

static boolean_t
xx_m_getcapab(void *arg, mac_capab_t cap, void *cap_data)
{
        switch (cap) {
        case MAC_CAPAB_HCKSUM: {
                uint32_t *txflags = cap_data;
                *txflags = HCKSUM_INET_FULL_V4 | HCKSUM_IPHDRCKSUM;
                break;
        }
        case MAC_CAPAB_LSO: {
                /* ... */
                break;
        }
        default:
                return (B_FALSE);
        }
        return (B_TRUE);
}

下面各节将介绍所支持的功能和需要返回的相应功能数据。

硬件校验和负载转移

为了获得关于硬件校验和负载转移支持的数据,框架将在 cap 参数中发送 MAC_CAPA _HCKSUM。 请参见硬件校验和负载转移功能信息

要在启用硬件校验和的情况下查询校验和负载转移元数据以及检索每个包的硬件校验和元数据,请使用 mac_hcksum_get(9F)。 请参见mac_hcksum_get()() 函数标志

要设置校验和负载转移元数据,请使用 mac_hcksum_set(9F)。请参见mac_hcksum_set()() 函数标志

有关更多信息,请参见硬件校验和:硬件硬件校验和:MAC 层

硬件校验和负载转移功能信息

要将关于 MAC_CAPAB_HCKSUM 功能的信息传递给框架,驱动程序必须在指向 uint32_tcap_data 中设置以下标志的组合。 这些标志指明驱动程序能够为外发包执行的硬件校验和负载转移的级别。

HCKSUM_INET_PARTIAL

1 的补码的部分校验和功能

HCKSUM_INET_FULL_V4

针对 IPv4 包的 1 的补码的完全校验和能力

HCKSUM_INET_FULL_V6

针对 IPv6 包的 1 的补码的完全校验和能力

HCKSUM_IPHDRCKSUM

IPv4 头校验和负载转移功能

mac_hcksum_get()() 函数标志

mac_hcksum_get()flags 参数是以下值的组合:

HCK_FULLCKSUM

计算此包的完整校验和。

HCK_FULLCKSUM_OK

完整校验和已在硬件中通过验证,证实是正确的。

HCK_PARTIALCKSUM

根据传递给 mac_hcksum_get() 的其他参数计算 1 的补码的部分校验和。HCK_PARTIALCKSUM HCK_FULLCKSUM 互斥。

HCK_IPV4_HDRCKSUM

计算 IP 报头校验和。

HCK_IPV4_HDRCKSUM_OK

IP 头校验和已在硬件中通过验证,证实是正确的。

mac_hcksum_set()() 函数标志

mac_hcksum_setflags() 参数是以下值的组合:

HCK_FULLCKSUM

通过 value 参数计算和传递完整校验和。

HCK_FULLCKSUM_OK

完整校验和已在硬件中通过验证,证实是正确的。

HCK_PARTIALCKSUM

通过 value 参数计算和传递部分校验和。HCK_PARTIALCKSUM HCK_FULLCKSUM 互斥。

HCK_IPV4_HDRCKSUM

通过 value 参数计算和传递 IP 头校验和。··

HCK_IPV4_HDRCKSUM_OK

IP 头校验和已在硬件中通过验证,证实是正确的。

大段(或发送)负载转移

为了查询大段(或发送)负载转移支持,框架将在 cap 参数中发送 MAC_CAPA _LSO,并等待接收在指向 mac_capab_lso(9S) 结构的 cap_data 中返回的信息。框架将分配 mac_capab_lso 结构,并在 cap_data 中传递指向此结构的指针。 mac_capab_lso 结构包含一个 lso_basic_tcp_ipv4(9S) 结构和一个 lso_flags 成员。 如果驱动程序实例为 IPv4 上的 TCP 支持 LSO,请设置 lso_flags 中的 LSO_TX_BASIC_TCP_IPV4 标志,并将 lso_basic_tcp_ipv4 结构的 lso_max 成员设置为驱动程序实例支持的最大有效载荷大小。

使用 mac_lso_get(9F) 获得每个包的 LSO 元数据。 如果为此包启用了 LSO,则将在 mac_lso_get () flags 参数中设置 HW_LSO 标志。在大段的分段过程中使用的最大段大小 (maximum segment size, MSS) 将通过 mss 参数指向的位置返回。 有关更多信息,请参见大段负载转移

GLDv3 数据路径

数据路径入口点包括以下组件:

传输数据路径

GLDv3 框架使用传输入口点 mc_tx(9E) 将消息块链传递至驱动程序。在您的 mac_callbacks 结构中提供指向 mc_tx() 入口点的指针。有关 GLDv3 MAC 注册数据结构 结构的更多信息,请参阅 GLDv3 MAC 注册数据结构


示例 19–6 mc_tx() 入口点

mblk_t *
xx_m_tx(void *arg, mblk_t *mp)
{
        xx_t    *xxp = arg;
        mblk_t   *nmp;

        mutex_enter(&xxp->xx_xmtlock);

        if (xxp->xx_flags & XX_SUSPENDED) {
                while ((nmp = mp) != NULL) {
                        xxp->xx_carrier_errors++;
                        mp = mp->b_next;
                        freemsg(nmp);
                }
                mutex_exit(&xxp->xx_xmtlock);
                return (NULL);
        }

        while (mp != NULL) {
                nmp = mp->b_next;
                mp->b_next = NULL;

                if (!xx_send(xxp, mp)) {
                        mp->b_next = nmp;
                        break;
                }
                mp = nmp;
        }
        mutex_exit(&xxp->xx_xmtlock);

        return (mp);
}

以下各节将讨论与将数据传输至硬件相关的主题。

流量控制

如果驱动程序因硬件资源不足而无法发送包,则驱动程序将返回无法发送的包的子链。此后,在更多描述符可用时,驱动程序必须调用 mac_tx_update(9F) 通知框架。

硬件校验和:硬件

如果驱动程序指定了硬件校验和支持(请参见硬件校验和负载转移),则驱动程序必须执行以下任务:

大段负载转移

如果驱动程序指定了 LSO 功能(请参见大段(或发送)负载转移),则驱动程序必须使用 mac_lso_get(9F) 来查询是否必须在包上执行 LSO。·

虚拟 LAN:硬件

管理员配置 VLAN 时,MAC 层将通过 mc_tx() 入口点,在外发包传递至驱动程序之前将所需的 VLAN 头添加到外发包中。

接收数据路径

在驱动程序的中断处理程序中调用 mac_rx(9F) 函数,将一个或多个包构成的链沿堆栈向上传递至 MAC 层。避免在调用 mac_rx() 的过程中保留互斥锁或其他锁。具体来说,不要保留可能会在 mac_rx() 调用过程中被传输获取的锁。有关必须向上发送至 MAC 层的包的信息,请参见mc_unicst(9E)

以下各节将讨论与将数据发送至 MAC 层相关的主题。

硬件校验和:MAC 层

如果驱动程序指定了硬件校验和支持(请参见硬件校验和负载转移),则驱动程序必须使用 mac_hcksum_set(9F) 函数将硬件校验和元数据与包关联。

虚拟 LAN:MAC 层

VLAN 包必须连同标签一起传递至 MAC 层。切勿分流包中的 VLAN 头。

GLDv3 状态更改通知

驱动程序可以调用以下函数来通知网络栈驱动程序的状态已更改。

void mac_tx_update(mac_handle_t mh);

mac_tx_update(9F) 函数通知框架有更多 TX 描述符可用。 如果 mc_tx() 返回非空包链,则驱动程序必须在资源可用时立即调用 mac_tx_update(),通知 MAC 层重试之前作为未发送的包返回的包。 有关 mc_tx() 入口点的更多信息,请参见传输数据路径

void mac_link_update(mac_handle_t mh, link_state_t new_state);

mac_link_update(9F) 函数通知 MAC 层介质链接的状态已更改。new_state 参数必须为以下值之一:

LINK_STATE_UP

介质链路为启动状态。

LINK_STATE_DOWN

介质链路为关闭状态。

LINK_STATE_UNKNOWN

介质链路为未知状态。

GLDv3 网络统计信息

设备驱动程序为其管理的设备实例维护一组统计信息。MAC 层通过驱动程序的 mc_getstat(9E) 入口点查询这些统计信息。

int mc_getstat(void *driver_handle, uint_t stat, uint64_t *stat_value);

GLDv3 框架使用 stat 指定所查询的统计信息。 驱动程序使用 stat_value 返回 stat 指定的统计信息的值。 如果返回了统计信息的值,则 mc_getstat() 必须返回 0。如果驱动程序不支持 stat 统计信息,mc_getstat() 必须返回 ENOTSUP

所支持的 GLDv3 统计信息是通用 MAC 统计信息和特定于以太网的统计信息的联合。有关所支持的统计信息的完整列表,请参见 mc_getstat(9E) 手册页。


示例 19–7 mc_getstat() 入口点

int
xx_m_getstat(void *arg, uint_t stat, uint64_t *val)
{
        xx_t    *xxp = arg;

        mutex_enter(&xxp->xx_xmtlock);
        if ((xxp->xx_flags & (XX_RUNNING|XX_SUSPENDED)) == XX_RUNNING)
                xx_reclaim(xxp);
        mutex_exit(&xxp->xx_xmtlock);

        switch (stat) {
        case MAC_STAT_MULTIRCV:
                *val = xxp->xx_multircv;
                break;
        /* ... */
        case ETHER_STAT_MACRCV_ERRORS:
                *val = xxp->xx_macrcv_errors;
                break;
        /* ... */
        default:
                return (ENOTSUP);
        }
        return (0);
}

GLDv3 属性

使用 mc_propinfo(9E) 入口点返回属性的不可变属性。此信息包括权限、缺省值和允许的值范围。使用 mc_setprop(9E) 为此特定驱动程序实例设置属性值。使用 mc_getprop(9E) 返回属性的当前值。

有关属性及其类型的完整列表,请参见 mc_propinfo(9E) 手册页。

mc_propinfo() 入口点应调用 mac_prop_info_set_perm()mac_prop_info_set_default()mac_prop_info_set_range() 函数来关联所查询属性的特定属性,例如缺省值、权限或允许的值范围等。

mac_prop_info_set_default_uint8(9F)mac_prop_info_set_default_str(9F)mac_prop_info_set_default_link_flowctrl(9F) 函数将缺省值与特定属性相关联。mac_prop_info_set_range_uint32(9F) 函数为特定属性关联值的允许范围。

mac_prop_info_set_perm(9F) 函数指定属性的权限。权限可以是以下值之一:

MAC_PROP_PERM_READ

属性为只读

MAC_PROP_PERM_WRITE

属性为只写

MAC_PROP_PERM_RW

属性可以是读取和写入

如果 mc_propinfo() 入口点未为特定属性调用 mac_prop_info_set_perm(),GLDv3 框架将假设该属性拥有对应于 MAC_PROP_PERM_RW 的读写权限。

除了 mc_propinfo(9E) 手册页中列出的属性之外,驱动程序还可公开驱动程序专用属性。使用 mac_register 结构的 m_priv_props 字段指定驱动程序支持的驱动程序专用属性。框架在 mc_setprop() mc_getprop()mc_propinfo() 中传递 MAC_PROP_PRIVATE 属性·ID。 有关更多信息,请参见 mc_propinfo (9E) 手册页。

GLDv3 接口汇总

下表列出了属于 GLDv3 网络设备驱动程序框架的入口点、其他 DDI 函数和数据结构。

表 19–1 GLDv3 接口

接口名称 

说明 

必需的入口点

mc_getstat(9E)

从驱动程序检索网络统计信息。请参见GLDv3 网络统计信息

mc_start(9E)

启动一个驱动程序实例。GLDv3 框架在尝试任何操作之前都会调用 start 入口点。 

mc_stop(9E)

停止一个驱动程序实例。MAC 层会在设备分离之前调用停止入口点。 

mc_setpromisc(9E)

更改设备驱动程序实例的混杂模式。 

mc_multicst(9E)

添加或删除多点传送地址。 

mc_unicst(9E)

设置主单点传送地址。设备必须通过 mac_rx() 开始回传带有与新单点传送地址匹配的目标 MAC 地址的包。 有关 mac_rx() 的信息,请参见接收数据路径

mc_tx(9E)

发送一个或多个包。请参见传输数据路径

可选入口点

mc_ioctl(9E)

可选 ioctl 驱动程序接口。此工具仅供调试之用。 

mc_getcapab(9E)

检索功能。请参见GLDv3 功能

mc_setprop(9E)

设置属性值。请参见GLDv3 属性

mc_getprop(9E)

获得属性值。请参见GLDv3 属性

mc_propinfo(9E)

获得关于属性的信息。请参见GLDv3 属性

数据结构

mac_register(9S)

注册信息。请参见GLDv3 MAC 注册数据结构

mac_callbacks(9S)

驱动程序回调。请参见GLDv3 MAC 注册数据结构

mac_capab_lso(9S)

LSO 元数据。请参见大段(或发送)负载转移

lso_basic_tcp_ipv4(9S)

TCP/IPv4 的 LSO 元数据。请参见大段(或发送)负载转移

MAC 注册函数

mac_alloc(9F)

分配新 mac_register 结构。 请参见GLDv3 MAC 注册

mac_free(9F)

释放 mac_register 结构。

mac_register(9F)

使用 MAC 层注册。 

mac_unregister(9F)

从 MAC 层取消注册。 

mac_init_ops(9F)

初始化驱动程序的 dev_ops(9S) 结构。

mac_fini_ops(9F)

发布驱动程序的 dev_ops 结构。

数据传输函数

mac_rx(9F)

向上传递接收到的包。请参见接收数据路径

mac_tx_update(9F)

TX 资源可用。请参见GLDv3 状态更改通知

mac_link_update(9F)

链接状态已更改。 

mac_hcksum_get(9F)

检索硬件校验和信息。请参见硬件校验和负载转移传输数据路径

mac_hcksum_set(9F)

附加硬件校验和信息。请参见硬件校验和负载转移接收数据路径

mac_lso_get(9F)

检索 LSO 信息。请参见大段(或发送)负载转移

属性函数

mac_prop_info_set_perm(9F)

设置属性权限。请参见GLDv3 属性

mac_prop_info_set_default_uint8(9F), mac_prop_info_set_default_str(9F), mac_prop_info_set_default_link_flowctrl(9F)

设置属性值。 

mac_prop_info_set_range_uint32(9F)

设置属性值范围。 

GLDv2 网络设备驱动程序框架

GLDv2 是多线程、可克隆、可装入的核心模块,为局域网的设备驱动程序提供支持。Solaris OS 中的局域网 (Local area network, LAN) 设备驱动程序是基于 STREAMS 的驱动程序,使用数据链路提供者接口 (Data Link Provider Interface, DLPI) 与网络协议栈进行通信。这些协议栈使用网络驱动程序在 LAN 中发送和接收包。GLDv2 为 Solaris LAN 驱动程序实现 STREAMS 和 DLPI 的很多功能。GLDv2 提供可供许多网络驱动程序共享的通用代码。使用 GLDv2 可减少重复的代码,简化您的网络驱动程序。

有关 GLDv2 的更多信息,请参见gld(7D)手册页。

《STREAMS Programming Guide》中的第 II 部分, “Kernel Interface”中介绍了 STREAMS 驱动程序。具体请参见 STREAMS 指南中的第 9 章,“STREAMS 驱动程序”。STREAMS 框架是基于消息的框架。STREAMS 驱动程序特有的接口包括 STREAMS 消息队列处理入口点。

DLPI 指定 OSI 参考模型数据链路层的数据链路服务 (Data Link Services, DLS) 的接口。 DLPI 使 DLS 用户能够访问和使用多种符合规定的 DLS 提供者,而无需专门确认提供者的协议。DLPI 以 M_PROTO 和 M_PCPROTO 类型的·STREAMS 消息的形式指定对 DLS 提供者的访问。DLPI 模块使用 STREAMS ioctl 调用链接至 MAC 子层。有关 DLPI 协议的更多信息,包括特定于 Solaris 的 DPLI 扩展,请参见dlpi(7P)手册页。有关 DLPI 的一般信息,请参见 DLPI 标准:http://www.opengroup.org/pubs/catalog/c811.htm

使用 GLDv2 实现的 Solaris 网络驱动程序有两个独立的部分:

GLDv2 驱动程序必须处理格式完全的 MAC 层包,不能执行逻辑链路控制 (logical link control, LLC) 处理。

本节讨论以下主题:

GLDv2 设备支持

GLDv2 框架支持以下类型的设备:

Ethernet V2 和 ISO 8802-3 (IEEE 802.3)

对于声明为 DL_ETHER 类型的设备,GLDv2 提供 Ethernet V2 和 ISO 8802-3 (IEEE 802.3) 包处理支持。通过 Ethernet V2,用户可以访问遵循该标准的数据链路服务提供方,而无需了解有关此提供者协议的专门知识。服务访问点 (service access point, SAP) 是用户用于与服务提供者进行通信的点。

绑定到范围为 [0-255] 的 SAP 值的流均视为相同的流,并表示用户要使用 8802-3 模式。如果 DL_BIND_REQ 的 SAP 值在此范围内,则 GLDv2 将计算该流上的每条后续 DL_UNITDATA_REQ 消息的长度。长度不包括 14 字节的介质访问控制 (media access control, MAC) 头。然后,GLDv2 将传输 MAC 帧标题 type 字段中具有这些长度的 8802-3 帧。这样的长度不超过 1500。

拥有范围为 [0-1500] 的 type 字段的帧被视为 8802-3 帧。 这些帧以 8802-3 模式沿所有打开的流进行路由。那些具有范围为 [0-255] 的 SAP 值的流均被视为处于 8802-3 模式下。如果多个流处于 8802-3 模式,则传入帧会被复制并沿这些流进行路由。

那些绑定到大于 1500 的 SAP 值的流均被假定为处于 Ethernet V2 模式下。这些流将接收以太网 MAC 头 type 值与流绑定到的 SAP 值完全匹配的传入包。

TPR 和 FDDI:SNAP 处理

对于 DL_TPR 和·DL_FDDI 介质类型,GLDv2 会实现最低限度的 SNAP(Sub-Net Access Protocol,子网访问协议)处理。此处理用于任何绑定到大于 255 的 SAP 值的流。范围为 [0-255] 的 SAP 值是 LLC SAP 值。此类值自然地通过介质包格式传送。大于 255 的 SAP 值需要 SNAP 头(从属于 LLC 头)来传送 16 位 Ethernet V2 样式的 SAP 值。

SNAP 头位于 LLC 头后面,目标是 SAP 0xAA。SAP 值大于 255 的外发包要求 LLC+SNAP 头采用以下形式:

AA AA 03 00 00 00 XX XX

XX XX 表示对应于 Ethernet V2 样式 type 的 16 位 SAP。在支持非零的组织唯一标识符字段方面,此头是独一无二的。03 之外的 LLC 控制字段被视为具有 SAP 0xAA 的 LLC 包。要使用此格式之外的 SNAP 格式的客户机必须使用 LLC 并绑定到 SAP 0xAA

将检查传入包是否符合上述格式。将符合的包与任何已绑定到包的 16 位 SNAP 类型的流进行匹配。此外,还将这些包视为与 LLC SNAP SAP 0xAA 相匹配。

针对任何 LLC SAP 接收的包将沿所有绑定到 LLC SAP 的流传递。相关内容,请参见介质类型 DL_ETHER 的介绍。

TPR:源路由

对于类型 DL_TPR 设备,GLDv2 实现最低限度的源路由支持。

源路由支持包括以下任务:

源路由会将路由信息字段添加到传出包的 MAC 头中。此外,此支持还可在传入包中识别此类字段。

GLDv2 的源路由支持并未实现 ISO 8802-2 (IEEE 802.2) 的第 9 节中指定的完整路由确定实体 (route determination entity, RDE)。但是,此支持可以与任何可能存在于同一网络或桥接网络中的 RDE 实现进行交互操作。

GLDv2 DLPI 提供者

GLDv2 实现样式 1 和样式 2 的 DLPI 提供者。物理连接点 (physical point of attachment, PPA) 是系统将自身附加到物理通信介质的点。在此物理介质上进行的所有通信都通过 PPA。样式 1 提供者根据已打开的主设备或次要设备,将流附加到特定 PPA。样式 2 提供者需要 DLS 用户使用 DL_ATTACH_REQ 显式指定所需的 PPA。在这种情况下,open(9E) 会在用户与 GLDv2 之间创建流,并且 DL_ATTACH_REQ 随后会将特定的 PPA 与该流相关联。样式 2 由次要设备号 0 表示。如果打开了次要设备号不为 0 的设备节点,则会指示样式 1,并且关联的 PPA 为次要设备号减 1。在样式 1 和样式 2 启动中,都会克隆设备。

GLDv2 DLPI 原语

GLDv2 实现某些 DLPI 原语。DL_INFO_REQ 原语请求有关 DLPI 流的信息。消息包含一个 M_PROTO 消息块。GLDv2 将在对此请求的 DL_INFO_ACK 响应中返回与设备有关的值。这些值以基于 GLDv2 的驱动程序在传递给 gld_register(9F) 函数的 gld_mac_info(9S) 结构中指定的信息为基础。

GLDv2 代表所有基于 GLDv2 驱动程序返回以下值:


注 –

与 DLPI 规范相反,即使在将流附加到 PPA 之前,GLDv2 也会在 DL_INFO_ACK 中返回设备的正确地址长度和广播地址。


DL_ATTACH_REQ 原语用于将 PPA 与流相关联。样式 2 DLS 提供者标识用于发送通信的物理介质时需要此请求。完成时,状态会从 DL_UNATTACHED 更改为 DL_UNBOUND。消息包含一个 M_PROTO 消息块。使用样式 1 模式时,不允许此请求。使用样式 1 打开的流已在打开完成时附加到 PPA。

DL_DETACH_REQ 原语请求将 PPA 与流相分离。仅当流使用样式 2 打开时,才允许此分离。

DL_BIND_REQDL_UNBIND_REQ 原语用于将 DLSAP(data link service access point,数据链路服务访问点)与流绑定和解除绑定。 与流关联的 PPA 在完成处理该流上的 DL_BIND_REQ 之前完成初始化。可以将多个流绑定到同一 SAP。在这种情况下,每个流都接收针对此 SAP 接收的所有包的副本。

DL_ENABMULTI_REQDL_DISABMULTI_REQ 原语启用和禁用各多点传送组地址的接收。通过重复使用这些原语,应用程序或其他 DLS 用户可以创建或修改一组多点传送地址。必须将流附加到 PPA 才能接受这些原语。

DL_PROMISCON_REQ 和·DL_PROMISCOFF_REQ 原语以每个流为基础启动和关闭混杂模式。这些控制项既可在物理级别运行,也可在 SAP 级别运行。DL 提供者通过介质将所有已接收的消息路由到 DLS 用户。路由将继续进行,直到收到 DL_DETACH_REQ、收到 DL_PROMISCOFF_REQ 或关闭流为止。可以针对介质上的所有包或仅针对多点传送包指定物理级别混杂接收。


注 –

必须将流附加到 PPA 才能接受这些混杂模式原语。


DL_UNITDATA_REQ 原语用于在无连接传输中发送数据。由于此服务未得到确认,因此无法保证传送。消息包含一个 M_PROTO 消息块,以及一个或多个至少包含一个字节数据的 M_DATA 块。

在包向上游传递时,会使用 DL_UNITDATA_IND 类型。将包放入 M_PROTO 消息中,并将原语设置为 DL_UNITDATA_IND

DL_PHYS_ADDR_REQ 原语请求当前与附加到流的 PPA 相关联的 MAC 地址。此地址将通过 DL_PHYS_ADDR_ACK 原语返回。使用样式 2 时,仅当成功执行 DL_ATTACH_REQ 之后此原语才有效。

DL_SET_PHYS_ADDR_REQ 原语更改当前与附加到流的 PPA 相关联的 MAC 地址。 此原语会影响附加到此设备的所有其他当前流和将来流。更改之后,当前或随后打开并附加到此设备的所有流都将获取这一新的物理地址。在此原语再次更改物理地址或重新装入驱动程序之前,新的物理地址将一直有效。


注 –

允许超级用户在其他流绑定到 PPA 时更改同一 PPA 的物理地址。


DL_GET_STATISTICS_REQ 原语请求包含与附加到流的 PPA 相关的统计信息的 DL_GET_STATISTICS_ACK 响应。必须使用 DL_ATTACH_REQ 将样式 2 流附加到特定 PPA,此原语才能成功执行。

GLDv2 I/O 控制函数

GLDv2 可实现以下将介绍的 ioctl ioc_cmd 函数。如果 GLDv2 收到无法识别的 ioctl 命令,则 GLDv2 会将此命令传递给特定于设备的驱动程序的 gldm_ioctl() 例程,如 gld(9E) 中所述。

DLIOCRAW ioctl 函数可供某些 DLPI 应用程序(尤其是 snoop(1M) 命令)使用。DLIOCRAW 命令可将流置于原始模式。在原始模式下,驱动程序将在 M_DATA 消息中传递 MAC 级别的完整传入包,而不是将包转换为 DL_UNITDATA_IND 形式。DL_UNITDATA_IND 形式通常用于报告传入包。包 SAP 过滤仍将在处于原始模式下的流上执行。如果流用户要接收所有传入包,则此用户还必须选择相应的混杂模式。成功选择原始模式之后,还允许应用程序将完全格式化的包作为 M_DATA 消息发送到驱动程序以便传输。DLIOCRAW 不使用任何参数。启用之后,流将保持此模式直到关闭。

GLDv2 驱动程序需求

基于 GLDv2 的驱动程序必须包括头文件 <sys/gld.h>

基于 GLDv2 的驱动程序必须与 -N"misc/gld" 选项链接:

%ld -r -N"misc/gld" xx.o -o xx

GLDv2 代表特定于设备的驱动程序实现以下函数:

module_info(9S) 结构的 mi_idname 元素是用于指定驱动程序名称的字符串。此字符串必须与文件系统中定义的驱动程序模块的名称完全匹配。

读端 qinit(9S) 结构应该指定以下元素:

qi_putp

NULL

qi_srvp

gld_rsrv

qi_qopen

gld_open

qi_qclose

gld_close

写端 qinit(9S) 结构应该指定以下元素:

qi_putp

gld_wput

qi_srvp

gld_wsrv

qi_qopen

NULL

qi_qclose

NULL

dev_ops(9S) 结构的 devo_getinfo 元素应该将 gld_getinfo 指定为 getinfo(9E) 例程。

驱动程序的 attach(9E) 函数会将特定于硬件的设备驱动程序与 GLDv2 功能相关联。然后,attach() 将准备设备和驱动程序以供使用。

attach(9E) 函数使用 gld_mac_alloc() 分配 gld_mac_info(9S) 结构。通常,驱动程序需要针对每台设备保存的信息比在 macinfo 结构中定义的信息多。驱动程序应该分配其他必需的数据结构,并在 gld_mac_info(9S) 结构的 gldm_private 成员中保存指向该结构的指针。

attach(9E) 例程必须初始化 macinfo 结构,如 gld_mac_info(9S) 手册页中所述。然后,attach() 例程应该调用 gld_register() 以将驱动程序与 GLDv2 模块相链接。驱动程序应该在必要时映射寄存器,并在调用 gld_register() 之前完全初始化以准备接受中断。attach(9E) 函数应该添加中断,而不应该使设备生成这些中断。驱动程序应该在调用 gld_register() 之前重置硬件,以确保硬件处于停顿状态。不得将设备置于其可能会在调用 gld_register() 之前生成中断的状态。稍后会在 GLDv2 调用驱动程序的 gldm_start() 入口点时启动设备,相关内容在 gld(9E) 手册页中介绍。gld_register() 成功执行之后,GLDv2 可能会随时调用 gld(9E) 入口点。

如果 gld_register() 成功执行,则 attach(9E) 例程应该返回 DDI_SUCCESS。如果 gld_register() 失败,则会返回 DDI_FAILURE。如果出现故障,则 attach(9E) 例程应该解除分配在调用 gld_register() 之前分配的所有资源。然后,连接例程还应该返回 DDI_FAILURE。绝不能重新使用出现故障的 macinfo 结构。应该使用 gld_mac_free() 解除分配此类结构。

detach(9E) 函数应尝试通过调用 gld_unregister() 来尝试从 GLDv2 中取消注册驱动程序。有关 gld_unregister() 的更多信息,请参见 gld(9F) 手册页。detach(9E) 例程可以使用 ddi_get_driver_private(9F) 从设备的专用数据中获取指向所需 gld_mac_info(9S) 结构的指针。gld_unregister() 将检查可能要求不分离驱动程序的特定条件。如果检查失败,则 gld_unregister() 会返回 DDI_FAILURE,在这种情况下,驱动程序的 detach(9E) 例程必须保持设备处于运行状态并返回 DDI_FAILURE

如果检查成功,则 gld_unregister() 会确保停止设备中断。如有必要,则会调用驱动程序的 gldm_stop() 例程。驱动程序将与 GLDv2 框架解除链接。然后,gld_unregister() 会返回 DDI_SUCCESS。在这种情况下,detach(9E) 例程应该删除中断,并使用 gld_mac_free() 解除分配在 attach(9E) 例程中分配的所有 macinfo 数据结构。然后,detach() 例程应该返回 DDI_SUCCESS。此例程必须在调用 gld_mac_free() 之前删除中断。

GLDv2 网络统计信息

Solaris 网络驱动程序必须实现统计变量。GLDv2 可记录一些网络统计信息,但是其他统计信息必须由基于 GLDv2 的每个驱动程序进行计数。GLDv2 为基于 GLDv2 的驱动程序提供支持,以报告一组标准的网络驱动程序统计信息。GLDv2 使用 kstat(7D)kstat(9S) 机制报告统计信息。DL_GET_STATISTICS_REQ DLPI 命令还可用于检索当前统计计数器。所有统计信息均以无符号数据进行维护。除非另有说明,否则统计信息为 32 位。

GLDv2 维护并报告以下统计信息。

rbytes64

已在接口上成功接收的总字节数。将存储 64 位统计信息。

rbytes

已在接口上成功接收的总字节数。

obytes64

已请求在接口上传输的总字节数。将存储 64 位统计信息。

obytes

已请求在接口上传输的总字节数。

ipackets64

已在接口上成功接收的总包数。将存储 64 位统计信息。

ipackets

已在接口上成功接收的总包数。

opackets64

已请求在接口上传输的总包数。将存储 64 位统计信息。

opackets

已请求在接口上传输的总包数。

multircv

已成功接收的多点传送包,包括组和功能地址 (functional address) (long)。

multixmt

已请求传输的多点传送包,包括组和功能地址 (functional address) (long)。

brdcstrcv

已成功接收的广播包 (long)。

brdcstxmt

已请求传输的广播包 (long)。

unknowns

未由任何流接受的有效已接收包 (long)。

noxmtbuf

由于传输缓冲区繁忙或无法分配传输缓冲区而在输出中放弃的包 (long)。

blocked

由于队列受控于流而使已接收的包无法沿流放置的次数 (long)。

xmtretry

在由于资源不足而延迟之后重试传输的次数 (long)。

promisc

接口的当前“混杂”状态(字符串)。

与设备有关的驱动程序将在每个实例的专用结构中跟踪以下统计信息。为了报告统计信息,GLDv2 将调用驱动程序的 gldm_get_stats() 入口点。然后,gldm_get_stats() 会在 gld_stats(9S)有关更多信息,请参见 gldm_get_stats(9E) 手册页。然后,GLDv2 将使用如下所示的命名统计变量报告已更新的统计信息。

ifspeed

接口的当前估算带宽(以 bps 为单位)。将存储 64 位统计信息。

media

设备正在使用的当前介质类型(字符串)。

intr

调用中断处理程序从而导致中断的次数 (long)。

norcvbuf

由于无法分配接收缓冲区而放弃某个有效传入包的次数 (long)。

ierrors

已接收但由于错误而无法处理的总包数 (long)。

oerrors

由于错误而无法成功传输的总包数 (long)。

missed

硬件在接收时已丢弃的包数 (long)。

uflo

传输时 FIFO 下溢的次数 (long)。

oflo

接收期间接收器下溢的次数 (long)。

以下统计信息组适用于类型为 DL_ETHER 的网络。这些统计信息由此类型的特定于设备的驱动程序维护,如上所示。

align_errors

在出现帧错误时接收的包数,即这些包不包含整数个数的八位字节 (long)。

fcs_errors

在出现 CRC 错误时接收的包数 (long)。

duplex

接口的当前双工模式(字符串)。

carrier_errors

在尝试传输时丢失载体或从未检测到载体的次数 (long)。

collisions

传输期间的以太网冲突数 (long)。

ex_collisions

传输时出现过多的冲突而导致传输失败的帧数 (long)。

tx_late_collisions

在过了一段时间后(即过了 512 位时后)发生传输冲突的次数 (long)。

defer_xmts

没有发生由于介质繁忙而延迟首次传输尝试冲突的包数 (long)。

first_collisions

在仅发生一个冲突后成功传输的包数。

multi_collisions

在发生多个冲突后成功传输的包数。

sqe_errors

已报告 SQE 测试错误的次数。

macxmt_errors

遇到传输 MAC 故障(载体和冲突故障除外)的包数。

macrcv_errors

在出现 MAC 错误(align_errorsfcs_errorstoolong_errors 除外)时接收的包数。

toolong_errors

接收的大于最大允许长度的包数。

runt_errors

接收的小于最小允许长度的包数 (long)。

以下统计信息组适用于类型为 DL_TPR 的网络。这些统计信息由此类型的特定于设备的驱动程序维护,如上所示。

line_errors

在出现非数据位或 FCS 错误时接收的包数。

burst_errors

已针对五个半位计时器检测到不存在转换的次数。

signal_losses

在环上检测到信号丢失情况的次数。

ace_errors

AMP 或 SMP 帧(其中 A 等于 C 等于 0)后跟其他 SMP 帧而没有中间 AMP 帧的次数。

internal_errors

站识别到内部错误的次数。

lost_frame_errors

传输期间 TRR 计时器到期的次数。

frame_copied_errors

在 FS 字段 'A' 位设置为 1 时接收发往此站的帧的次数。

token_errors

用作活动监视器的站识别到需要已传输标记的错误情况的次数。

freq_errors

传入信号的频率不同于预期频率的次数。

以下统计信息组适用于类型为 DL_FDDI 的网络。这些统计信息由此类型的特定于设备的驱动程序维护,如上所示。

mac_errors

由该 MAC 检测到出现错误但是尚未由其他 MAC 检测到出现错误的帧数。

mac_lost_errors

在出现格式错误从而删除帧时接收的帧数。

mac_tokens

已接收的标记数,即不受限制和受限制的总标记数。

mac_tvx_expired

TVX 已到期的次数。

mac_late

自重置此 MAC 或接收标记以来的 TRT 到期次数。

mac_ring_ops

环从“环未运行”状态进入“环运行”状态的次数。

GLDv2 声明和数据结构

本节介绍 gld_mac_info(9S) 和 gld_stats 结构。

gld_mac_info 结构

GLDv2 MAC 信息 (gld_mac_info) 结构是用于链接特定于设备的驱动程序与 GLDv2 的主数据接口。此结构包含 GLDv2 所需的数据,以及指向可选的其他特定于驱动程序的信息结构的指针。

可使用 gld_mac_alloc 分配 gld_mac_info() 结构,可使用 gld_mac_free() 解除分配此结构。驱动程序不能做出有关此结构长度的任何假设,因为此长度在不同发行版的 Solaris OS 和/或 GLDv2 中可能会有所不同。专用于 GLDv2 的结构成员(未在此处介绍)既不应该由特定于设备的驱动程序设置,也不应该由其读取。

gld_mac_info(9S) 结构包含以下字段。

caddr_t              gldm_private;              /* Driver private data */
int                  (*gldm_reset)();           /* Reset device */
int                  (*gldm_start)();           /* Start device */
int                  (*gldm_stop)();            /* Stop device */
int                  (*gldm_set_mac_addr)();    /* Set device phys addr */
int                  (*gldm_set_multicast)();   /* Set/delete multicast addr */
int                  (*gldm_set_promiscuous)(); /* Set/reset promiscuous mode */
int                  (*gldm_send)();            /* Transmit routine */
uint_t               (*gldm_intr)();            /* Interrupt handler */
int                  (*gldm_get_stats)();       /* Get device statistics */
int                  (*gldm_ioctl)();           /* Driver-specific ioctls */
char                 *gldm_ident;               /* Driver identity string */
uint32_t             gldm_type;                 /* Device type */
uint32_t             gldm_minpkt;               /* Minimum packet size */
                                                /* accepted by driver */
uint32_t             gldm_maxpkt;               /* Maximum packet size */
                                                /* accepted by driver */
uint32_t             gldm_addrlen;              /* Physical address length */
int32_t              gldm_saplen;               /* SAP length for DL_INFO_ACK */
unsigned char        *gldm_broadcast_addr;      /* Physical broadcast addr */
unsigned char        *gldm_vendor_addr;         /* Factory MAC address */
t_uscalar_t          gldm_ppa;                  /* Physical Point of */
                                                /* Attachment (PPA) number */
dev_info_t           *gldm_devinfo;             /* Pointer to device's */
                                                /* dev_info node */
ddi_iblock_cookie_t  gldm_cookie;               /* Device's interrupt */
                                                /* block cookie */

gldm_private 结构成员对设备驱动程序可见。gldm_private 还专用于特定于设备的驱动程序。GLDv2 无法使用或修改 gldm_private。通常,gldm_private 用作指向专用数据的指针,指向同时由驱动程序定义和分配的每个实例的数据结构。

以下结构成员组必须由驱动程序在调用 gld_register() 之前设置,并且此后不应该由驱动程序进行修改。由于 gld_register() 可能会使用或高速缓存结构成员的值,因此,驱动程序在调用 gld_register() 之后进行的更改可能会导致不可预测的结果。有关这些结构的更多信息,请参见 gld(9E) 手册页。

gldm_reset

指向驱动程序入口点的指针。

gldm_start

指向驱动程序入口点的指针。

gldm_stop

指向驱动程序入口点的指针。

gldm_set_mac_addr

指向驱动程序入口点的指针。

gldm_set_multicast

指向驱动程序入口点的指针。

gldm_set_promiscuous

指向驱动程序入口点的指针。

gldm_send

指向驱动程序入口点的指针。

gldm_intr

指向驱动程序入口点的指针。

gldm_get_stats

指向驱动程序入口点的指针。

gldm_ioctl

指向驱动程序入口点的指针。允许此指针为 null。

gldm_ident

指向包含设备简短说明的字符串的指针。此指针用于在系统消息中标识设备。

gldm_type

驱动程序处理的设备的类型。GLDv2 目前支持以下值:

  • DL_ETHER(ISO 8802-3 (IEEE 802.3) 和以太网总线)

  • DL_TPR(IEEE 802.5 令牌传递环)

  • DL_FDDI(ISO 9314-2 光纤分布式数据接口)

必须正确设置此结构成员,GLDv2 才能正常运行。

gldm_minpkt

最小服务数据单元大小:设备可以传输的最小包大小(不包括 MAC 头)。如果特定于设备的驱动程序处理任何所需的填充,则允许此大小为 0。

gldm_maxpkt

最大服务数据单元大小:设备可以传输的最大包大小(不包括 MAC 头)。对于以太网,此数值为 1500。

gldm_addrlen

设备所处理的物理地址的长度(以字节为单位)。对于以太网、令牌环和 FDDI,此结构成员的值应该为 6。

gldm_saplen

驱动程序所使用的 SAP 地址的长度(以字节为单位)。对于基于 GLDv2 的驱动程序,应该始终将长度设置为 -2。长度为 -2 表示支持 2 个字节的 SAP 值,并且 SAP 出现在 DLSAP 地址中的物理地址之后。有关更多详细信息,请参见 DLPI 规范中的附录 A.2“消息 DL_INFO_ACK”。

gldm_broadcast_addr

指向长度 gldm_addrlen 字节数组的指针,该数组包含要用于传输的广播地址。驱动程序必须提供保存广播地址的空间,使用相应的值填充此空间,并将 gldm_broadcast_addr 设置为指向此地址。对于以太网、令牌环和 FDDI,广播地址通常为 0xFF-FF-FF-FF-FF-FF

gldm_vendor_addr

指向长度为 gldm_addrlen 字节的数组的指针,该数组包含供应商提供的设备网络物理地址。驱动程序必须提供保存地址的空间,使用来自设备的信息填充此空间,并将 gldm_vendor_addr 设置为指向此地址。

gldm_ppa

此设备实例的 PPA 编号。应该始终将 PPA 编号设置为从 ddi_get_instance(9F) 返回的实例编号。

gldm_devinfo

指向此设备的 dev_info 节点的指针。

gldm_cookie

以下例程之一返回的中断块 cookie:

此 cookie 必须对应于设备的接收中断,可从该中断中调用 gld_recv()

gld_stats 结构

调用 gldm_get_stats() 之后,基于 GLDv2 的驱动程序会使用 (gld_stats) 结构将统计信息和状态信息传递给 GLDv2。请参见 gld(9E) 和 gld(7D) 手册页。当 GLDv2 报告统计信息时,将使用已由基于 GLDv2 的驱动程序填充的此结构的成员。在下面各表的注释中说明了 GLDv2 所报告的统计变量的名称。有关每条统计信息含义的更详细说明,请参见 gld(7D) 手册页。

驱动程序不得做出有关此结构长度的任何假设。此结构长度在不同发行版的 Solaris OS 和/或 GLDv2 中可能会有所不同。专用于 GLDv2 的结构成员(未在此处介绍)既不应该由特定于设备的驱动程序设置,也不应该由其读取。

针对所有介质类型定义了以下结构成员:

uint64_t    glds_speed;                   /* ifspeed */
uint32_t    glds_media;                   /* media */
uint32_t    glds_intr;                    /* intr */
uint32_t    glds_norcvbuf;                /* norcvbuf */
uint32_t    glds_errrcv;                  /* ierrors */
uint32_t    glds_errxmt;                  /* oerrors */
uint32_t    glds_missed;                  /* missed */
uint32_t    glds_underflow;               /* uflo */
uint32_t    glds_overflow;                /* oflo */

针对介质类型 DL_ETHER 定义了以下结构成员:

uint32_t    glds_frame;                   /* align_errors */
uint32_t    glds_crc;                     /* fcs_errors */
uint32_t    glds_duplex;                  /* duplex */
uint32_t    glds_nocarrier;               /* carrier_errors */
uint32_t    glds_collisions;              /* collisions */
uint32_t    glds_excoll;                  /* ex_collisions */
uint32_t    glds_xmtlatecoll;             /* tx_late_collisions */
uint32_t    glds_defer;                   /* defer_xmts */
uint32_t    glds_dot3_first_coll;         /* first_collisions */
uint32_t    glds_dot3_multi_coll;         /* multi_collisions */
uint32_t    glds_dot3_sqe_error;          /* sqe_errors */
uint32_t    glds_dot3_mac_xmt_error;      /* macxmt_errors */
uint32_t    glds_dot3_mac_rcv_error;      /* macrcv_errors */
uint32_t    glds_dot3_frame_too_long;     /* toolong_errors */
uint32_t    glds_short;                   /* runt_errors */

针对介质类型 DL_TPR 定义了以下结构成员:

uint32_t    glds_dot5_line_error          /* line_errors */
uint32_t    glds_dot5_burst_error         /* burst_errors */
uint32_t    glds_dot5_signal_loss         /* signal_losses */
uint32_t    glds_dot5_ace_error           /* ace_errors */
uint32_t    glds_dot5_internal_error      /* internal_errors */
uint32_t    glds_dot5_lost_frame_error    /* lost_frame_errors */
uint32_t    glds_dot5_frame_copied_error  /* frame_copied_errors */
uint32_t    glds_dot5_token_error         /* token_errors */
uint32_t    glds_dot5_freq_error          /* freq_errors */

针对介质类型 DL_FDDI 定义了以下结构成员:

uint32_t    glds_fddi_mac_error;          /* mac_errors */
uint32_t    glds_fddi_mac_lost;           /* mac_lost_errors */
uint32_t    glds_fddi_mac_token;          /* mac_tokens */
uint32_t    glds_fddi_mac_tvx_expired;    /* mac_tvx_expired */
uint32_t    glds_fddi_mac_late;           /* mac_late */
uint32_t    glds_fddi_mac_ring_op;        /* mac_ring_ops */

上述大多数统计变量均为表示发现特定事件次数的计数器。以下统计信息不表示次数:

glds_speed

接口的当前带宽估算(以 bps 为单位)。此对象应该包含那些带宽不变或无法进行准确估算的接口的标称带宽。

glds_media

硬件所使用的介质(连线)或连接器的类型。支持以下介质名称:

  • GLDM_AUI

  • GLDM_BNC

  • GLDM_TP

  • GLDM_10BT

  • GLDM_100BT

  • GLDM_100BTX

  • GLDM_100BT4

  • GLDM_RING4

  • GLDM_RING16

  • GLDM_FIBER

  • GLDM_PHYMII

  • GLDM_UNKNOWN

glds_duplex

接口的当前双工状态。支持的值包括 GLD_DUPLEX_HALFGLD_DUPLEX_FULL。此外,还允许 GLD_DUPLEX_UNKNOWN

GLDv2 函数参数

GLDv2 例程使用以下参数。

macinfo

指向 gld_mac_info(9S) 结构的指针。

macaddr

指向包含有效 MAC 地址的字符数组的开头的指针。此数组的长度由驱动程序在 gld_mac_info(9S) 结构的 gldm_addrlen 元素中指定。

multicastaddr

指向包含多点传送、组或功能地址 (functional address) 的字符数组的起始地址的指针。此数组的长度由驱动程序在 gld_mac_info(9S) 结构的 gldm_addrlen 元素中指定。

multiflag

指示是启用还是禁用多点传送地址接收的标志。可将此参数指定为 GLD_MULTI_ENABLEGLD_MULTI_DISABLE

promiscflag

指示要启用何种类型的混杂模式(如果存在)的标志。可将此参数指定为 GLD_MAC_PROMISC_PHYSGLD_MAC_PROMISC_MULTIGLD_MAC_PROMISC_NONE

mp

gld_ioctl() 使用 mp 作为指向包含要执行的 ioctl 的 STREAMS 消息块的指针。gldm_send() 使用 mp 作为指向包含要传输的包的 STREAMS 消息块的指针。gld_recv() 使用 mp 作为指向包含已接收包的消息块的指针。

stats

指向要使用统计计数器的当前值填充的 gld_stats(9S) 结构的指针。

q

指向要用于 ioctl 回复的 queue(9S) 结构的指针。

dip

指向设备的 dev_info 结构的指针。

name

设备接口名称。

GLDv2 入口点

入口点必须通过针对使用 GLDv2 的接口设计的特定于设备的网络驱动程序实现。

gld_mac_info(9S) 结构是用于在特定于设备的驱动程序与 GLDv2 模块之间进行通信的主结构。请参见 gld(7D) 手册页。此结构中的某些元素是指向此处所述的入口点的函数指针。特定于设备的驱动程序必须在调用 gld_register() 之前在其 attach(9E) 例程中初始化这些函数指针。

gldm_reset() 入口点

int prefix_reset(gld_mac_info_t *macinfo);

gldm_reset() 可将硬件重置为初始状态。

gldm_start() 入口点

int prefix_start(gld_mac_info_t *macinfo);

gldm_start() 可使设备生成中断。gldm_start() 还可使驱动程序调用 gld_recv(),从而将已接收的数据包传送到 GLDv2。

gldm_stop() 入口点

int prefix_stop(gld_mac_info_t *macinfo);

gldm_stop() 禁止设备生成任何中断,并阻止驱动程序为将数据包传送到 GLDv2 而调用 gld_recv()。GLDv2 依赖 gldm_stop() 例程来确保设备不再中断。gldm_stop() 必须成功执行此操作。此函数应该始终返回 GLD_SUCCESS

gldm_set_mac_addr() 入口点

int prefix_set_mac_addr(gld_mac_info_t *macinfo, unsigned char *macaddr);

gldm_set_mac_addr() 可设置硬件用于接收数据的物理地址。利用此函数,可通过已传递的 MAC 地址 macaddr 对设备进行编程。如果当前没有足够的资源来执行请求,则 gldm_set_mac_add() 应该返回 GLD_NORESOURCES。如果不支持所请求的函数,则 gldm_set_mac_add() 应该返回 GLD_NOTSUPPORTED

gldm_set_multicast() 入口点

int prefix_set_multicast(gld_mac_info_t *macinfo, 
     unsigned char *multicastaddr, int multiflag);

gldm_set_multicast() 可启用和禁用设备级别的特定多点传送地址接收。如果将第三个参数 multiflag 设置为GLD_MULTI_ENABLE,则 gldm_set_multicast() 会将接口设置为使用多点传送地址接收包。gldm_set_multicast() 将使用第二个参数所指向的多点传送地址。如果将 multiflag 设置为 GLD_MULTI_DISABLE,则允许驱动程序禁用指定的多点传送地址接收。

当 GLDv2 要启用或禁用多点传送、组或功能地址 (functional address) 接收时,便会调用此函数。GLDv2 不会做出有关设备如何支持多点传送并调用此函数以启用或禁用特定多点传送地址的假设。某些设备可能会使用散列算法和位掩码来启用多点传送地址集合。将允许此过程,并且 GLDv2 会过滤出所有多余的包。如果在设备级别禁用一个地址会导致禁用多个地址,则设备驱动程序应该保留所有必要信息。此方法可避免禁用 GLDv2 已启用但未禁用的地址。

不能调用 gldm_set_multicast() 来启用已启用的特定多点传送地址。同样,也不能调用 gldm_set_multicast() 来禁用当前未启用的地址。GLDv2 将跟踪针对同一多点传送地址发出的多个请求。GLDv2 仅在第一次请求启用特定多点传送地址或最后一次请求禁用特定多点传送地址时才调用驱动程序的入口点。如果当前没有足够的资源来执行请求,则函数应该返回 GLD_NORESOURCES。如果不支持所请求的函数,则函数应该返回 GLD_NOTSUPPORTED

gldm_set_promiscuous() 入口点

int prefix_set_promiscuous(gld_mac_info_t *macinfo, int promiscflag);

gldm_set_promiscuous() 可启用和禁用混杂模式。当 GLDv2 要启用或禁用介质上的所有包接收时,便会调用此函数。还可以将此函数限制为针对介质上的多点传送包使用。如果将第二个参数 promiscflag 设置为 GLD_MAC_PROMISC_PHYS 的值,则函数会启用物理级别的混杂模式。物理级别的混杂模式会导致接收介质上的所有包。如果将 promiscflag 设置为 GLD_MAC_PROMISC_MULTI,则会启用所有多点传送包接收。如果将 promiscflag 设置为 GLD_MAC_PROMISC_NONE,则会禁用混杂模式。

在混杂多点传送模式下,无仅限多点传送混杂模式的设备的驱动程序必须将设备设置为物理混杂模式。此方法可确保接收所有多点传送包。在这种情况下,例程应该返回 GLD_SUCCESS。GLDv2 软件会过滤出所有多余的包。如果当前没有足够的资源来执行请求,则函数应该返回 GLD_NORESOURCES。如果不支持所请求的函数,gld_set_promiscuous() 应该返回 GLD_NOTSUPPORTED

为了向前兼容,gldm_set_promiscuous() 例程应该处理所有 promiscflag 无法识别的值,如同这些值为 GLD_MAC_PROMISC_PHYS 一样。

gldm_send() 入口点

int prefix_send(gld_mac_info_t *macinfo, mblk_t *mp);

gldm_send() 可将要发送到设备的包进行排队以进行传输。将向此例程传递包含要发送的包的 STREAMS 消息。此消息可能包括多个消息块。send() 例程必须遍历消息中的所有消息块,以访问要发送的整个包。应对驱动程序进行适当设置,以便处理并跳过链表中所有零长度的消息连续块。驱动程序还应该检查包是否未超过最大允许包大小。如有必要,驱动程序必须将包填充到最小允许包大小。如果发送例程成功传输包或对包排队,则应该返回 GLD_SUCCESS

如果无法立即接受要传输的包,则发送例程应该返回 GLD_NORESOURCES。在这种情况下,GLDv2 会稍后重试。如果 gldm_send() 曾经返回 GLD_NORESOURCES,则驱动程序必须在稍后资源变得可用时调用 gld_sched()。此 gld_sched() 调用会通知 GLDv2 重试驱动程序先前无法对其进行排队以进行传输的包。(如果调用驱动程序的 gldm_stop() 例程,则会为驱动程序免除这种职责,直到驱动程序从 gldm_send() 例程返回 GLD_NORESOURCES 为止。不过,再次调用 gld_sched() 也不会导致错误操作。)

如果驱动程序的发送例程返回 GLD_SUCCESS,则驱动程序会负责释放不再需要的消息。如果硬件使用 DMA 直接读取数据,则驱动程序在硬件完全读取数据之前不得释放消息。在这种情况下,驱动程序可以释放中断例程中的消息。或者,驱动程序可以在将来发送操作的开始回收缓冲区。如果发送例程返回 GLD_SUCCESS 之外的任何内容,则驱动程序不得释放消息。如果在不存在到网络或链路伙伴的物理连接时调用 gldm_send(),则会返回 GLD_NOLINK

gldm_intr() 入口点

int prefix_intr(gld_mac_info_t *macinfo);

当设备可能已中断时,便会调用 gldm_intr()。由于其他设备可以共享中断,因此,驱动程序必须检查设备状态以确定此设备是否实际导致了中断。如果驱动程序所控制的设备并未导致中断,则此例程必须返回 DDI_INTR_UNCLAIMED。否则,驱动程序必须修复中断并返回 DDI_INTR_CLAIMED。如果中断由成功接收包导致,则此例程应该将已接收的包放入类型为 M_DATA 的 STREAMS 消息中,并将此消息传递给 gld_recv()

gld_recv() 将传入包向上游传递到网络协议栈的相应下一层。在调用 gld_recv 之前,此例程必须正确设置 STREAMS 消息的 b_rptrb_wptr() 成员。

在调用 gld_recv() 期间,驱动程序应该避免持有互斥锁或其他锁。需要特别指出的是,在调用 gld_recv() 期间,不得持有传输线程可使用的锁。在某些情况下,调用 gld_recv() 的中断线程可发送传出包,这会导致调用驱动程序的 gldm_send() 例程。如果在调用 gld_recv()gldm_send() 尝试获取 gldm_intr() 持有的互斥锁,则会由于存在递归互斥锁入口操作而出现紧急情况。在调用 gld_recv() 时,如果其他驱动程序入口点尝试获取驱动程序所持有的互斥锁,则会导致死锁。

中断代码应该针对所有错误递增统计计数器。这些错误包括分配已接收数据所需的缓冲区时出现的故障,以及所有特定于硬件的错误(如 CRC 错误或帧错误)。

gldm_get_stats() 入口点

int prefix_get_stats(gld_mac_info_t *macinfo, struct gld_stats *stats);

gldm_get_stats() 可收集硬件和/或驱动程序专用计数器的统计信息,并更新 stats 所指向的 gld_stats(9S) 结构。GLDv2 会针对统计信息请求调用此例程。GLDv2 会使用 gldm_get_stats() 机制从驱动程序获取与设备有关的统计信息,然后再编写统计信息请求回复。有关已定义的统计计数器的更多信息,请参见 gld_stats(9S)gld(7D)qreply(9F) 手册页。

gldm_ioctl() 入口点

int prefix_ioctl(gld_mac_info_t *macinfo, queue_t *q, mblk_t *mp);

gldm_ioctl() 可实现所有特定于设备的 ioctl 命令。如果驱动程序未实现任何 ioctl 函数,则允许此元素为 null。驱动程序负责在返回 GLD_SUCCESS 之前将消息块转换为 ioctl 回复消息,并调用 qreply(9F) 函数。此函数应该始终返回 GLD_SUCCESS。驱动程序应该根据需要报告要传递给 qreply(9F) 的消息中的所有错误。如果将 gldm_ioctl 元素指定为 NULL,则 GLDv2 会返回类型为 M_IOCNAK 的消息以及 EINVAL 错误。

GLDv2 返回值

GLDv2 中的某些入口点函数可以返回以下值(具体视上述限制而定):

GLD_BADARG

函数检测到不适合的参数(如错误多点传送地址、错误 MAC 地址或错误包)时

GLD_FAILURE

出现硬件故障时

GLD_SUCCESS

成功时

GLDv2 服务例程

本节提供 GLDv2 服务例程的语法和说明。

gld_mac_alloc() 函数

gld_mac_info_t *gld_mac_alloc(dev_info_t *dip);

gld_mac_alloc() 可分配新的 gld_mac_info(9S) 结构并返回指向此结构的指针。可能会在 gld_mac_alloc() 返回之前初始化此结构的某些 GLDv2 专用元素。所有其他元素均初始化为 0。在将指向 gld_mac_info 结构的指针传递给 gld_register() 之前,设备驱动程序必须初始化某些结构成员,如 gld_mac_info(9S) 手册页中所述。

gld_mac_free() 函数

void gld_mac_free(gld_mac_info_t *macinfo);

gld_mac_free() 可释放先前由 gld_mac_alloc() 所分配的 gld_mac_info(9S) 结构。

gld_register() 函数

int gld_register(dev_info_t *dip, char *name, gld_mac_info_t *macinfo);

可通过设备驱动程序的 attach(9E) 例程调用 gld_register()gld_register() 将基于 GLDv2 的设备驱动程序与 GLDv2 框架相链接。在调用 gld_register() 之前,设备驱动程序的 attach(9E) 例程使用 gld_mac_alloc() 来分配 gld_mac_info(9S) 结构,然后初始化若干结构元素。有关更多信息,请参见 gld_mac_info(9S)。成功调用 gld_register() 可执行以下操作:

传递给 gld_register() 的设备接口名称必须与存在于文件系统中的驱动程序模块的名称完全匹配。

如果 gld_register() 成功执行,则驱动程序的 attach(9E) 例程应该返回 DDI_SUCCESS。如果 gld_register() 没有返回 DDI_SUCCESS,则 attach(9E) 例程应该在调用 gld_register() 之前解除分配所有已分配的资源,然后返回 DDI_FAILURE

gld_unregister() 函数

int gld_unregister(gld_mac_info_t *macinfo);

gld_unregister() 由设备驱动程序的 detach(9E) 函数进行调用,如果成功,会执行以下任务:

如果 gld_unregister() 返回 DDI_SUCCESS,则 detach(9E) 例程应该解除分配在 attach( 9E) 例程中分配的所有数据结构,使用 gld_mac_free() 解除分配 macinfo 结构,并返回 DDI_SUCCESS。如果 gld_unregister() 没有返回 DDI_SUCCESS,则驱动程序的 detach(9E) 例程必须保持设备处于运行状态并返回 DDI_FAILURE

gld_recv() 函数

void gld_recv(gld_mac_info_t *macinfo, mblk_t *mp);

驱动程序的中断处理程序可调用 gld_recv() 以向上游传递已接收的包。驱动程序必须构造并传递包含原始包的 STREAMS M_DATA 消息。gld_recv() 可确定哪些 STREAMS 队列应该接收包的副本,并在必要时复制包。然后,gld_recv() 会在需要时设置 DL_UNITDATA_IND 消息的格式,并沿所有相应的流向上传递数据。

在调用 gld_recv() 期间,驱动程序应该避免持有互斥锁或其他锁。需要特别指出的是,在调用 gld_recv() 期间,不得持有传输线程可使用的锁。在某些情况下调用 gld_recv() 的中断线程可执行包括发送传出包的处理。传输包会导致调用驱动程序的 gldm_send() 例程。如果在调用 gld_recv()gldm_send() 尝试获取 gldm_intr() 持有的互斥锁,则会由于存在递归互斥锁入口操作而出现紧急情况。在调用 gld_recv() 时,如果其他驱动程序入口点尝试获取驱动程序所持有的互斥锁,则会导致死锁。

gld_sched() 函数

void gld_sched(gld_mac_info_t *macinfo);

设备驱动程序可调用 gld_sched() 来重新安排已延迟的外发包。当驱动程序的 gldm_send() 例程返回 GLD_NORESOURCES 时,驱动程序必须调用 gld_sched() 以通知 GLDv2 框架重试先前无法发送的包。当资源变得可用之后,应该尽快调用 gld_sched(),以便 GLDv2 继续将外发包传递给驱动程序的 gldm_send() 例程。(如果调用了驱动程序的 gldm_stop() 例程,则在 gldm_send 返回 GLD_NORESOURCES() 之前,驱动程序不需要重试。不过,再次调用 gld_sched() 也不会导致错误操作。)

gld_intr() 函数

uint_t gld_intr(caddr_t);

gld_intr() 是 GLDv2 的主要中断处理程序。通常,将 gld_intr() 指定为设备驱动程序的 ddi_add_intr(9F) 调用中的中断例程。将中断处理程序的参数指定为 ddi_add_intr(9F) 调用中的 int_handler_arg。此参数必须是指向 gld_mac_info(9S) 结构的指针。gld_intr() 会在适当的情况下调用设备驱动程序的 gldm_intr() 函数,并将该指针传递给 gld_mac_info(9S) 结构。但是,要使用高级中断,驱动程序必须提供自身的高级中断处理程序,并在处理程序中触发软中断。在这种情况下,通常会将 gld_intr() 指定为 ddi_add_softintr() 调用中的软中断处理程序。gld_intr() 将返回适用于中断处理程序的值。

第 20 章 USB 驱动程序

本章介绍如何使用 Solaris 环境的 USBA 2.0 框架编写客户机 USB 设备驱动程序。本章讨论以下主题:

Solaris 环境中的 USB

Solaris USB 体系结构包括 USBA 2.0 框架和 USB 客户机驱动程序。

USBA 2.0 框架

USBA 2.0 框架是向符合 USBA 标准的客户机驱动程序呈现 USB 设备的抽象表示方式的服务层。利用该框架,符合 USBA 标准的客户机驱动程序可以管理其 USB 设备。USBA 2.0 框架支持除高速等时管道之外的 USB 2.0 规范。有关 USB 2.0 规范的信息,请参见 http://www.usb.org/home

USBA 2.0 框架与平台无关。下图显示了 Solaris USB 体系结构。在该图中,USBA 2.0 框架即是 USBA 层。此层通过与硬件无关的主机控制器驱动程序接口连接到特定于硬件的主机控制器驱动程序。主机控制器驱动程序通过其管理的主机控制器访问 USB 物理设备。

图 20–1 Solaris USB 体系结构

图中显示了从客户机和集线器驱动程序经由 USB 体系结构接口,然后到控制器和设备的控制流。

USB 客户机驱动程序

USBA 2.0 框架本身不是设备驱动程序。本章介绍图 20–1图 20–2 中所示的客户机驱动程序。客户机驱动程序与各种类型的 USB 设备(如海量存储设备、打印机和人机接口设备)交互。集线器驱动程序是同时充当结点驱动程序的客户机驱动程序,它枚举其端口上的设备,并为这些设备创建 devinfo 节点,然后连接客户机驱动程序。本章并未介绍如何编写集线器驱动程序。

USB 驱动程序的结构与其他任何 Solaris 驱动程序相同。USB 驱动程序可以是块驱动程序、字符驱动程序或 STREAMS 驱动程序。USB 驱动程序遵循调用约定,并使用 Solaris OS 手册页第 9 节中说明的数据结构和例程。请参见 Intro(9E)Intro(9F)Intro(9S)

USB 驱动程序与其他 Solaris 驱动程序的差别在于,USB 驱动程序通过调用 USBA 2.0 框架函数来访问设备,而不是直接访问设备。USBA 2.0 框架是对标准 Solaris DDI 例程的补充。请参见下图。

图 20–2 驱动程序和控制器接口

图中显示了 DDI 和 USBAI 函数、不同版本的 USBA 框架和不同类型的主机控制器。

图 20–2图 20–1 更为详细地显示了接口。图 20–2 显示 USBA 是客户机驱动程序可以调用的内核子系统,正如客户机驱动程序可以调用 DDI 函数一样。

并非所有系统都具有图 20–2 中所示的所有主机控制器接口。OHCI(Open Host Controller Interface,开放式主机控制器接口)硬件在 SPARC 系统和第三方 USB PCI 卡上最常见。UHCI(Universal Host Controller Interface,通用主机控制器接口)硬件在 x86 系统上最常见。但是,OHCI 和 UHCI 硬件都可以在任何系统上使用。当存在 EHCI(Enhanced Host Controller Interface,增强型主机控制器接口)硬件时,EHCI 硬件与 OHCI 或 UHCI 位于相同的卡上,并共享相同的端口。

主机控制器、主机控制器驱动程序和 HCDI 构成由 USBA 控制的传输层。您不能直接调用 OHCI、EHCI 或 UHCI。可通过与平台无关的 USBA 接口间接调用它们。

绑定客户机驱动程序

本节讨论如何将驱动程序绑定到设备,还讨论具有单个接口的设备和具有多个接口的设备的兼容设备名称。

USB 设备如何显示在系统中

一个 USB 设备可以支持多种配置。在任何给定时间,只有一种配置处于活动状态。活动配置称为当前配置

一种配置可以具有多个接口,其间可能具有针对一个函数分组两个或更多接口的介入式接口关联。一种配置的所有接口同时处于活动状态。不同的接口可由不同的设备驱动程序操作。

接口可以使用替代设置以不同的方式在主机系统中呈现自己。对于任何给定接口只能有一种替代设置处于活动状态。

每种替代设置通过端点提供设备访问。每个端点都有特定用途。主机系统通过建立到端点的通信通道来与设备通信。此通信通道称为管道

USB 设备和 Solaris 设备树

如果 USB 设备具有一种配置、一个接口,没有设备类,则可将该设备表示为单个设备节点。如果 USB 设备具有多个接口,则可将该设备表示为分层设备结构。在分层设备结构中,每个接口的设备节点是顶层设备节点的子节点。例如,音频设备即是具有多个接口的设备,该设备向主机同时呈现为音频控制和音频流两种接口。音频控制接口和音频流接口可以分别由各自的驱动程序控制。

兼容设备名称

Solaris 软件基于每个设备中存储的标识信息为 USB 绑定生成有序的兼容设备名称列表。此信息包括设备类、子类、供应商 ID、产品 ID、修订版和协议。有关 USB 类和子类的列表,请参见 http://www.usb.org/home

采用此名称分层结构,可以在没有特定于设备的驱动程序时,绑定到相对较常用的驱动程序。特定于类的驱动程序即是常规驱动程序。以 usbif 开头的设备名称指定单个接口的设备。有关示例,请参见示例 20–1。USBA 2.0 框架定义设备的所有兼容名称。使用 prtconf 命令可显示这些设备名称,如示例 20–2 中所示。

以下示例显示了 USB 鼠标设备的兼容设备名称。此鼠标设备表示完全由单个驱动程序操作的组合节点。USBA 2.0 框架为此设备节点指定了示例中所示的名称(按所示顺序)。


示例 20–1 USB 鼠标的兼容设备名称

  1. 'usb430,100.102'      Vendor 430, product 100, revision 102
  2. 'usb430,100'          Vendor 430, product 100
  3. 'usbif430,class3.1.2' Vendor 430, class 3, subclass 1, protocol 2
  4. 'usbif430,class3.1'   Vendor 430, class 3, subclass 1
  5. 'usbif430,class3'     Vendor 430, class 3
  6. 'usbif,class3.1.2'    Class 3, subclass 1, protocol 2
  7. 'usbif,class3.1'      Class 3, subclass 1
  8. 'usbif,class3'        Class 3

请注意,上面示例中的名称按从最具体到最常规的顺序进行排列。第 1 项仅绑定到特定供应商的特定产品的特定修订版。第 3、4 和 5 项用于由供应商 430 生产的类 3 设备。第 6、7 和 8 项用于任何供应商生产的类 3 设备。绑定过程将按从上到下的顺序查找名称匹配项。要进行绑定,必须将驱动程序添加到其别名与上述其中一个名称匹配的系统。要获取在添加驱动程序时要绑定到的兼容设备名称的列表,请在 prtconf -vp 命令的输出中检查设备的 compatible 属性。

以下示例显示了键盘和鼠标的兼容属性列表。使用 prtconf -D 命令可显示绑定的驱动程序。


示例 20–2 列显配置命令显示的兼容设备名称


# prtconf -vD | grep compatible
            compatible: 'usb430,5.200' + 'usb430,5' + 'usbif430,class3.1.1'
+ 'usbif430,class3.1' + 'usbif430,class3' + 'usbif,class3.1.1' +
'usbif,class3.1' + 'usbif,class3'
            compatible: 'usb2222,2071.200' + 'usb2222,2071' +
'usbif2222,class3.1.2' + 'usbif2222,class3.1' + 'usbif2222,class3' +
'usbif,class3.1.2' + 'usbif,class3.1' + 'usbif,class3'

使用最具体的名称可以更准确地确定一个设备或一组设备的驱动程序。要绑定为特定产品的特定修订版编写的驱动程序,请尽可能使用最具体的名称匹配项。例如,如果您有由供应商 430 为其产品 100 的修订版 102 编写的 USB 鼠标驱动程序,则可以使用以下命令将该驱动程序添加到系统中:

add_drv -n -i '"usb430,100.102"' specific_mouse_driver

要添加为供应商 430 的任何 USB 鼠标(类 3、子类 1、协议 2)编写的驱动程序,请使用以下命令:

add_drv -n -i '"usbif430,class3.1.2"' more_generic_mouse_driver

如果安装这两个驱动程序并连接兼容设备,则系统会将正确的驱动程序绑定到所连接的设备。例如,如果安装这两个驱动程序,并连接供应商 430、型号 100、修订版 102 的设备,则此设备将绑定到 specific_mouse_driver。如果连接供应商 430、型号 98 的设备,则此设备将绑定到 more_generic_mouse_driver 。如果连接其他供应商的鼠标,则此设备也将绑定到 more_generic_mouse_driver。如果有多个驱动程序可供特定设备使用,则驱动程序绑定框架将选择与兼容名称列表中第一个兼容名称匹配的驱动程序。

具有多个接口的设备

复合设备是支持多个接口的设备。复合设备的每个接口都有一个兼容名称列表。此兼容名称列表可确保将最有效的驱动程序绑定到该接口。最常规的多接口项是 usb,device

对于 USB 音频复合设备,兼容名称如下:

1. 'usb471,101.100'     Vendor 471, product 101, revision 100
2. 'usb471,101'         Vendor 471, product 101
3. 'usb,device'         Generic USB device

名称 usb,device 是可表示任何整个 USB 设备的兼容名称。如果没有其他驱动程序请求该整个设备,则 usb_mid(7D) 驱动程序(USB 多接口驱动程序)将绑定到 usb,device 设备节点。usb_mid 驱动程序为物理设备的每个接口创建一个子设备节点。usb_mid 驱动程序还为每个接口生成一组兼容名称。生成的所有这些兼容名称都以 usbif 开头。系统将使用生成的这些兼容名称为每一个接口查找最佳的驱动程序。通过这种方法,可以将一个物理设备的不同接口绑定到不同的驱动程序。

例如,usb_mid 驱动程序通过多接口音频设备的 usb,device 节点名称绑定到该音频设备。然后 usb_mid 驱动程序创建特定于接口的设备节点。这些特定于接口的设备节点中的每个节点都有各自的兼容名称列表。对于音频控制接口节点,兼容名称列表可能类似于下例中所示的列表。


示例 20–3 USB 音频兼容设备名称

1. 'usbif471,101.100.config1.0' Vend 471, prod 101, rev 100, cnfg 1, iface 0
2. 'usbif471,101.config1.0'     Vend 471, product 101, config 1, interface 0
3. 'usbif471,class1.1.0'        Vend 471, class 1, subclass 1, protocol 0
4. 'usbif471,class1.1'          Vend 471, class 1, subclass 1
5. 'usbif471,class1'            Vend 471, class 1
6. 'usbif,class1.1.0'           Class 1, subclass 1, protocol 0
7. 'usbif,class1.1'             Class 1, subclass 1
8. 'usbif,class1'               Class 1

使用以下命令可将特定于供应商、特定于设备的客户机驱动程序(名为 vendor_model_audio_usb)绑定到特定于供应商、特定于设备的配置 1、接口 0 的接口兼容名称,如示例 20–3 中所示。

add_drv -n -i '"usbif471,101.config1.0"' vendor_model_audio_usb

使用以下命令可将名为 audio_class_usb_if_driver 的类驱动程序绑定到较常规的类 1、子类 1 的接口兼容名称,如示例 20–3 中所示:

add_drv -n -i '"usbif,class1.1"' audio_class_usb_if_driver

使用 prtconf -D 命令可显示设备及其驱动程序的列表。在以下示例中,prtconf -D 命令显示 usb_mid 驱动程序管理 audio 设备。usb_mid 驱动程序将 audio 设备拆分为多个接口。每个接口在 audio 设备名称下以缩进方式列出。对于缩进列表中所示的每个接口,prtconf -D 命令显示了哪个驱动程序管理该接口。

audio, instance #0 (driver name: usb_mid)
    sound-control, instance #2 (driver name: usb_ac)
    sound, instance #2 (driver name: usb_as)
    input, instance #8 (driver name: hid)

检查设备驱动程序绑定

文件 /etc/driver_aliases 包含对应于系统中已存在的绑定的项。/etc/driver_aliases 文件的每一行都有一个驱动程序名称,后面依次跟随一个空格和一个设备名称。使用此文件可检查现有的设备驱动程序绑定。


注 –

请不要手动编辑 /etc/driver_aliases 文件。使用 add_drv(1M) 命令可建立绑定。使用 update_drv(1M) 命令可更改绑定。


基本设备访问

本节介绍如何访问 USB 设备以及如何注册客户机驱动程序。本节还将讨论描述符树。

连接客户机驱动程序之前

在连接客户机驱动程序之前发生下列事件:

  1. PROM (OBP/BIOS) 和 USBA 框架在连接任何客户机驱动程序之前获取访问设备的权限。

  2. 集线器驱动程序将在其集线器的每个端口上探测设备的标识和配置。

  3. 将打开每个设备的缺省控制管道,并探测每个设备的设备描述符。

  4. 使用设备和接口描述符为每个设备构建兼容名称属性。

兼容名称属性定义可单独绑定到客户机驱动程序的设备的不同部分。可以将客户机驱动程序绑定到整个设备或仅绑定到一个接口。请参见绑定客户机驱动程序

描述符树

解析描述符涉及在自然边界对齐结构成员,以及将结构成员转换为主机 CPU 的字节序。解析后的标准 USB 配置描述符、接口描述符和端点描述符可用于每种配置的分层树格式的客户机驱动程序。任何特定于原始类或特定于供应商的描述符信息也可用于同一分层树中的客户机驱动程序。

调用 usb_get_dev_data(9F) 函数可检索分层描述符树。usb_get_dev_data (9F) 手册页的“另请参见”部分列出了每个标准 USB 描述符的手册页。使用 usb_parse_data(9F) 函数可解析原始描述符信息。

具有两种配置的设备的描述符树可能与下图中所示的树类似。

图 20–3 分层 USB 描述符树

图中显示了每个接口(具有两种配置的设备的每个接口)的描述符对树。

上图中所示的 dev_cfg 数组包含对应于相应配置的节点。每个节点包含以下信息:

表示第二个索引配置的第二个接口的节点位于图中的 dev_cfg[1].cfg_if[1] 位置。该节点包含表示该接口的替代设置的节点数组。USB 描述符的分层结构通过该树传播。字符串描述符数据中的 ASCII 字符串连接到 USB 规范说明的存在这些字符串的位置。

配置数组是非稀疏数组,按配置索引进行索引。第一个有效配置(配置 1)是 dev_cfg[0]。接口和替代设置具有与其编号对齐的索引。对于每个替代设置的端点,都以连续方式进行索引。每个替代设置的第一个端点位于索引 0 位置。

此编号方案使得很容易对树进行遍历。例如,端点索引为 0、替代项为 0、接口为 1、配置索引为 1 的原始描述符数据位于以下路径定义的节点:

dev_cfg[1].cfg_if[1].if_alt[0].altif_ep[0].ep_descr

另一种直接使用描述符树的方法是使用 usb_lookup_ep_data(9F) 函数。usb_lookup_ep_data(9F) 函数采用接口、替代项、端点、端点类型和指令作为参数。您可以使用 usb_lookup_ep_data(9F) 函数遍历描述符树以获取特定端点。有关更多信息,请参见 usb_get_dev_data(9F) 手册页。

注册驱动程序以获取设备访问权限

在 USBA 2.0 框架中,客户机驱动程序执行的前两个调用是对 usb_client_attach(9F) 函数和 usb_get_dev_data(9F) 函数的调用。这两个调用来自客户机驱动程序的 attach(9E) 入口点。在调用 usb_get_dev_data(9F) 函数之前,必须先调用 usb_client_attach(9F) 函数。

usb_client_attach(9F) 函数用于向 USBA 2.0 框架注册客户机驱动程序。usb_client_attach(9F) 函数用于强制进行版本控制。所有客户机驱动程序源文件必须使用下列行开头:


#define USBDRV_MAJOR_VER        2
#define USBDRV_MINOR_VER        minor-version
#include <sys/usb/usba.h>

minor-version 的值必须小于或等于 USBA_MINOR_VER。符号 USBA_MINOR_VER<sys/usb/usbai.h> 头文件中定义。<sys/usb/usbai.h> 头文件通过 <sys/usb/usba.h> 头文件包含进来。

USBDRV_VERSION 是根据 USBDRV_MAJOR_VERSIONUSBDRV_MINOR_VERSION 生成版本号的宏。usb_client_attach() 的第二个参数必须是 USBDRV_VERSION。如果第二个参数不是 USBDRV_VERSION,或者如果 USBDRV_VERSION 反映的是无效的版本,则 usb_client_attach() 函数将失败。此限制可确保编程接口兼容性。

usb_get_dev_data() 函数可返回正确管理 USB 设备所需的信息。例如,usb_get_dev_data() 函数返回以下信息:

必须调用 usb_get_dev_data() 函数。调用 usb_get_dev_data() 是检索缺省控制管道以及检索互斥锁初始化所需的 iblock_cookie 的唯一方法。

调用 usb_get_dev_data() 后,客户机驱动程序的 attach(9E) 例程通常会将所需的描述符和 数据从描述符树复制到驱动程序的软状态。复制到软状态的端点描述符在以后打开到这些端点的管道时会用到。attach(9E) 例程通常在复制描述符后调用 usb_free_descr_tree(9F) 以释放描述符树。或者,可以选择保留描述符树,且不复制描述符。

可以为 usb_get_dev_data(9F) 函数指定以下三个解析级别之一,以请求想要返回的描述符树的广度。如果驱动程序需要绑定到的对象不仅仅是设备,则需要更大的树广度。

客户机驱动程序的 detach(9E) 例程必须调用 usb_free_dev_data(9F) 函数来释放由 usb_get_dev_data() 函数分配的所有资源。usb_free_dev_data() 函数接受已使用 usb_free_descr_tree() 函数释放了描述符树的句柄。客户机驱动程序的 detach() 例程还必须调用 usb_client_detach(9F) 函数以释放由 usb_client_attach(9F) 函数分配的所有资源。

设备通信

USB 设备的工作方式是通过称为管道的通信通道来传递请求。只有管道处于打开状态时您才能提交请求。也可以刷新、查询和关闭管道。本节讨论管道、数据传输和回调以及数据请求。

USB 端点

与四种类型的 USB 端点通信的四种类型的管道包括:

有关对应于这些端点的传输类型的更多信息,请参见 USB 2.0 规范的第 5 章或参见请求

缺省管道

每个 USB 设备都有称为缺省端点的特殊控制端点。其通信通道称为缺省管道。大多数(可能并非所有)设备的设置都通过此管道进行。许多 USB 设备使用此管道作为其唯一的控制管道。

usb_get_dev_data(9F) 函数为客户机驱动程序提供缺省控制管道。此管道将会被预先打开以适应在打开其他管道之前需要的任何特殊设置。此缺省控制管道的特殊性表现在以下方面:

其他管道(包括其他控制管道)必须明确打开且仅限独占打开。

管道状态

管道处于以下状态之一:

调用 usb_pipe_get_state(9F) 函数可检索管道的状态。

打开管道

要打开管道,请将对应于要打开的管道的端点描述符传递给 usb_pipe_open(9F) 函数。使用 usb_get_dev_data(9F)usb_lookup_ep_data(9F) 函数可检索描述符树中的端点描述符。usb_pipe_open(9F) 函数将句柄返回给管道。

打开管道时必须指定管道策略。管道策略包含并发异步操作的估计数量,这些并发异步操作要求使用此管道将需要的独立线程。线程的估计数量是回调期间可能发生的并行操作的数量。此估计值必须至少为 2。有关管道策略的更多信息,请参见 usb_pipe_open(9F) 手册页。

关闭管道

驱动程序必须使用 usb_pipe_close(9F) 函数关闭缺省管道之外的管道。usb_pipe_close(9F) 函数使管道中的所有剩余请求得以完成。然后该函数还留出一秒钟的时间让这些请求的所有回调得以完成。

数据传输

对于所有管道类型,编程模型如下:

  1. 分配请求。

  2. 使用管道传输函数之一提交该请求。请参见 usb_pipe_bulk_xfer(9F)usb_pipe_ctrl_xfer(9F)usb_pipe_intr_xfer(9F)usb_pipe_isoc_xfer(9F) 手册页。

  3. 等待完成通知。

  4. 释放请求。

有关请求的更多信息,请参见请求。以下各节介绍各种请求类型的特性。

同步传输、异步传输和回调

传输分为同步传输和异步传输。同步传输在完成之前将会一直阻塞。异步传输完成时,将向客户机驱动程序发送回调。在 flags 参数中设置了 USB_FLAGS_SLEEP 标志时调用的传输函数大多数是同步的。

连续传输(如轮询)和等时传输不能是同步的。为了进行连续传输而对传输函数进行的调用(设置了 USB_FLAGS_SLEEP 标志)将会阻塞,目的只是等待资源,然后开始传输。

同步传输是要设置的最简单的传输,因为同步传输不要求任何回调函数。同步传输函数将返回传输开始状态,即使同步传输函数在完成传输前一直阻塞也是如此。完成时,可以在请求的完成原因字段和回调标志字段中查找有关传输状态的其他信息。下面将讨论完成原因字段和回调标志字段。

如果未在 flags 参数中指定 USB_FLAGS_SLEEP 标志,则该传输操作是异步的。此规则的例外是等时传输。异步传输操作将设置并启动传输,然后在传输完成前返回。异步传输操作将返回传输开始状态。客户机驱动程序通过回调处理程序接收传输完成状态。

回调处理程序是在异步传输完成时调用的函数。不要设置不进行回调的异步传输。两种类型的回调处理程序是正常完成处理程序和异常处理程序。您可以指定一个在这两种情况下要调用的处理程序。

完成处理程序和异常处理程序将传输请求作为参数接收。异常处理程序在请求中使用完成原因和回调状态来了解所发生的情况。完成原因 (usb_cr_t) 指示原始事务是如何完成的。例如,完成原因 USB_CR_TIMEOUT 指示传输超时。又如,如果 USB 设备在使用时被移除,则客户机驱动程序可能接收 USB_CR_DEV_NOT_RESP 作为其未完成请求的完成原因。回调状态 (usb_cb_flags_t) 指示 USBA 框架为修正这种情况所执行的操作。例如,回调状态 USB_CB_STALL_CLEARED 指示 USBA 框架清除了运行延迟条件。有关完成原因的更多信息,请参见 usb_completion_reason(9S) 手册页。有关回调状态标志的更多信息,请参见 usb_callback_flags(9S) 手册页。

运行请求的回调上下文和管道策略会对在回调中可以执行的操作实施一些限制。

请求

本节讨论请求结构,以及分配和取消分配不同类型的请求。

分配和取消分配请求

请求以初始化的请求结构的形式实现。每种不同的端点类型接受不同类型的请求。每种类型的请求有不同的请求结构类型。下表显示了每种类型请求的结构类型。此表还列出了可用于分配和释放每种类型结构的函数。

表 20–1 请求初始化

管道或端点类型 

请求结构 

请求结构分配函数 

请求结构释放函数 

控制 

usb_ctrl_req_t(请参见 usb_ctrl_request(9S) 手册页)

usb_alloc_ctrl_req(9F)

usb_free_ctrl_req(9F)

批量传输 

usb_bulk_req_t(请参见 usb_bulk_request(9S) 手册页)

usb_alloc_bulk_req(9F)

usb_free_bulk_req(9F)

中断 

usb_intr_req_t(请参见 usb_intr_request(9S) 手册页)

usb_alloc_intr_req(9F)

usb_free_intr_req(9F)

等时 

usb_isoc_req_t(请参见 usb_isoc_request(9S) 手册页)

usb_alloc_isoc_req(9F)

usb_free_isoc_req(9F)

下表列出了可用于每种类型请求的传输函数。

表 20–2 请求传输设置

管道或端点类型 

传输函数 

控制 

usb_pipe_ctrl_xfer(9F)usb_pipe_ctrl_xfer_wait(9F)

批量传输 

usb_pipe_bulk_xfer(9F)

中断 

usb_pipe_intr_xfer(9F)usb_pipe_stop_intr_polling(9F)

等时 

usb_pipe_isoc_xfer(9F)usb_pipe_stop_isoc_polling(9F)

分配和取消分配请求的过程如下:

  1. 使用相应的分配函数为所需的请求类型分配请求结构。表 20–1 中列出了请求结构分配函数的手册页。

  2. 初始化结构中所需的任何字段。有关更多信息,请参见请求特性和字段或相应的请求结构手册页。表 20–1 中列出了请求结构的手册页。

  3. 完成数据传输时,使用相应的释放函数释放请求结构。表 20–1 中列出了请求结构释放函数的手册页。

请求特性和字段

所有请求的数据都以消息块的形式传递,这样,无论驱动程序是 STREAMS 驱动程序、字符驱动程序还是块驱动程序,都将统一地对数据进行处理。消息块类型 mblk_tmblk(9S) 手册页中进行了介绍。DDI 提供了多个用于处理消息块的例程。示例包括 allocb(9F)freemsg(9F)。要了解用于处理消息块的其他例程,请参见 allocb(9F) 和 freemsg(9F) 手册页的“另请参见”部分。此外,还可以参见《STREAMS Programming Guide》

所有传输类型中都包括以下请求字段。在每个字段名称中,xxxx 的可能值包括: ctrlbulkintrisoc

xxxx_client_private

此字段值是一个指针,适用于将与请求一起在客户机驱动程序中传递的内部数据。此指针不用于将数据传输到设备。 

xxxx_attributes

此字段值是一组传输属性。虽然此字段对所有请求结构通用,但对于每种传输类型,此字段的初始化稍有不同。有关更多信息,请参见相应的请求结构手册页。表 20–1 中列出了这些手册页。另请参见 usb_request_attributes(9S) 手册页。

xxxx_cb

此字段值是正常传输完成的回调函数。如果异步传输在没有错误的情况下完成,则会调用此函数。 

xxxx_exc_cb

此字段值是错误处理的回调函数。仅当异步传输完成且出现错误时,才会调用此函数。 

xxxx_completion_reason

此字段存放传输本身的完成状态。如果出现错误,此字段将显示出现错误的内容。有关更多信息,请参见 usb_completion_reason(9S) 手册页。此字段由 USBA 2.0 框架更新。

xxxx_cb_flags

此字段列出在调用回调处理程序之前 USBA 2.0 框架所采取的恢复操作。USB_CB_INTR_CONTEXT 标志指示回调是否在中断上下文中运行。有关更多信息,请参见 usb_callback_flags(9S) 手册页。此字段由 USBA 2.0 框架更新。

以下各节介绍针对四种不同传输类型的不同请求字段。其中介绍如何初始化这些结构字段,还介绍有关属性和参数的各种组合的限制。

控制请求

使用控制请求可沿控制管道向下启动消息传输。您可以手动设置传输,如下所示。也可以使用 usb_pipe_ctrl_xfer_wait(9F) 包装函数设置并发送同步传输。

客户机驱动程序必须初始化 ctrl_bmRequestType ctrl_bRequestctrl_wValue ctrl_wIndexctrl_wLength 字段,如 USB 2.0 规范中所述。

必须将请求的 ctrl_data 字段初始化为指向数据缓冲区。将一个正值作为缓冲区 len 传递时,usb_alloc_ctrl_req(9F) 函数将初始化此字段。当然,还必须初始化缓冲区以便进行任何外发传输。在所有情况下,完成传输时,客户机驱动程序必须释放请求。

可以对多个控制请求进行排队。排队的请求中可以包括同步请求和异步请求。

ctrl_timeout 字段定义等待要被处理的请求的最长时间,但不包括在队列中的等待时间。此字段适用于同步和异步请求。ctrl_timeout 字段中指定的值以秒为单位。

如果出现异常,ctrl_exc_cb 字段接受要调用的函数的地址。usb_ctrl_request(9S) 手册页中指定了此异常处理程序的参数。异常处理程序的第二个参数是 usb_ctrl_req_t 结构。通过将请求结构作为参数传递,异常处理程序可以检查该请求的 ctrl_completion_reasonctrl_cb_flags 字段,以确定最佳恢复操作。

USB_ATTRS_ONE_XFERUSB_ATTRS_ISOC_* 标志对所有控制请求而言都是无效属性。USB_ATTRS_SHORT_XFER_OK 标志仅对主机绑定的请求有效。

批量传输请求

使用批量传输请求可发送非时间关键数据。批量传输请求可以接纳多个要完成的 USB 帧,具体取决于总体总线负载。

所有请求必须接收已初始化的消息块。有关 mblk_t 消息块类型的说明,请参见 mblk(9S) 手册页。此消息块将提供数据或存储数据,具体取决于传输方向。有关更多详细信息,请参阅 usb_bulk_request(9S) 手册页。

USB_ATTRS_ONE_XFERUSB_ATTRS_ISOC_* 标志对所有批量传输请求而言都是无效属性。USB_ATTRS_SHORT_XFER_OK 标志仅对主机绑定的请求有效。

usb_pipe_get_max_bulk_transfer_size(9F) 函数指定每个请求的最大字节数。检索到的值可以是客户机驱动程序的 minphys(9F) 例程中使用的最大值。

可以对多个批量传输请求进行排队。

中断请求

中断请求通常用于定期传入数据。中断请求定期在设备中轮询数据。但是,USBA 2.0 框架支持一次性的传入中断数据请求以及外发中断数据请求。所有中断请求可以利用 USB 中断传输的及时与重试特性。

USB_ATTRS_ISOC_* 标志对所有中断请求而言都是无效属性。USB_ATTRS_SHORT_XFER_OKUSB_ATTRS_ONE_XFER 标志仅对主机绑定的请求有效。

只有一次性轮询可以作为同步中断传输执行。如果在请求中指定 USB_ATTRS_ONE_XFER 属性,将进行一次性轮询。

定期轮询是作为异步中断传输启动。原始中断请求将被传递到 usb_pipe_intr_xfer(9F)。当轮询操作找到要返回的新数据时,将从原始请求克隆一个新的 usb_intr_req_t 结构,并使用已初始化的数据块填充该结构。分配请求时,请为 usb_alloc_intr_req(9F) 函数的 len 参数指定零。len 参数为零是因为 USBA 2.0 框架将为每个回调分配新请求,并填充该请求。分配请求结构后,请填充 intr_len 字段,以指定希望框架为每个轮询分配的字节数。将不会返回超出 intr_len 字节的数据。

客户机驱动程序必须释放它接收的每个请求。如果逆向发送消息块,请在逆向发送消息块之前从请求中分离该消息块。要从请求中分离消息块,请将该请求的数据指针设置为 NULL。将请求的数据指针设置为 NULL,可在取消分配请求时阻塞释放消息块。

调用 usb_pipe_stop_intr_polling(9F) 函数可取消定期轮询。停止轮询或关闭管道时,将通过异常回调返回原始请求结构。此返回的请求结构的完成原因将被设置为 USB_CR_STOPPED_POLLING

请不要在轮询进行过程中启动轮询。也不要在调用 usb_pipe_stop_intr_polling(9F) 的过程中启动轮询。

等时请求

等时请求用于速率恒定、与时间相关的流数据。出现错误时不会进行重试。等时请求具有以下特定于请求的字段:

isoc_frame_no

当必须从特定的帧编号启动总体传输时,请指定此字段。此字段的值必须大于当前帧编号。使用 usb_get_current_frame_number(9F) 可查找当前帧编号。请注意,当前帧编号为活动目标。对于低速和全速总线,将每 1 毫秒更新一次当前帧。对于高速总线,将每 0.125 毫秒更新一次当前帧。应设置 USB_ATTR_ISOC_START_FRAME 属性以便可以识别 isoc_frame_no 字段。

要忽略此帧编号字段并尽快启动,请设置 USB_ATTR_ISOC_XFER_ASAP 标志。

isoc_pkts_count

此字段是请求中的包数。此值受由 usb_get_max_pkts_per_isoc_request(9F) 函数返回的值和 isoc_pkt_descr 数组(参见下面的内容)的大小限制。该请求中可传输的字节数等于此 isoc_pkts_count 值与端点的 wMaxPacketSize 值的乘积。

isoc_pkts_length

此字段是请求的所有包的长度之和。此值由启动器设置。应将此值设置为零,以便自动使用 isoc_pkt_descr 列表中 isoc_pkts_length 的和,而不对此元素应用任何检查。

isoc_error_count

此字段是完成时出现错误的包数。此值由 USBA 2.0 框架设置。 

isoc_pkt_descr

此字段指向定义每个包传输数据量的包描述符的数组。对于传出请求,此值定义要处理的子请求的专用队列。对于传入请求,此值描述数据块的到达方式。客户机驱动程序将为传出请求分配这些描述符。框架将为传入请求分配和初始化这些描述符。此数组中的描述符包含框架初始化的字段,这些字段存储实际传输的字节数和传输的状态。有关更多详细信息,请参见 usb_isoc_request(9S) 手册页。

所有请求都必须接收已初始化的消息块。此消息块提供数据或存储数据。有关 mblk_t 消息块类型的说明,请参见 mblk(9S) 手册页。

USB_ATTR_ONE_XFER 标志是非法属性,因为系统将决定如何通过可用包数改变数据量。USB_ATTR_SHORT_XFER_OK 标志仅对主机绑定的数据有效。

无论是否设置 USB_FLAGS_SLEEP 标志,usb_pipe_isoc_xfer(9F) 函数都会使所有等时传输成为异步传输。所有等时输入请求都将启动轮询。

调用 usb_pipe_stop_isoc_polling(9F) 函数可取消定期轮询。停止轮询或关闭管道时,将通过异常回调返回原始请求结构。此返回的请求结构的完成原因将被设置为 USB_CR_STOPPED_POLLING

轮询会一直继续,直到发生以下某个事件:

刷新管道

您可能需要在出现错误后清理管道,或者可能想要等待管道清除。可使用下列方法之一刷新或清除管道:

设备状态管理

管理 USB 设备具体涉及到热插拔、系统电源管理(检查点和恢复)以及设备电源管理这几方面。所有客户机驱动程序应实现下图中所示的基本状态机。有关更多信息,请参见 /usr/include/sys/usb/usbai.h

图 20–4 USB 设备状态机

图中显示了在七个不同事件中的每个事件之后设备进入的状态。

可以使用特定于驱动程序的状态扩充此状态机及其四种状态。可以定义设备状态 0x800xff,且只有客户机驱动程序可以使用这些状态。

热插拔 USB 设备

USB 设备支持热插拔。可以随时插入或移除 USB 设备。客户机驱动程序必须处理打开的设备的移除和重新插入。使用热插拔回调可处理打开的设备。关闭的设备的插入和移除由 attach(9E)detach(9E) 入口点处理。

热插拔回调

USBA 2.0 框架支持以下事件通知:

客户机驱动程序必须在其 attach(9E) 例程中调用 usb_register_hotplug_cbs(9F),以便注册事件回调。在中断之前,驱动程序必须在其 detach(9E) 例程中调用 usb_unregister_hotplug_cbs(9F)

热插入

USB 设备的热插入的事件顺序如下:

  1. 集线器驱动程序 hubd(7D) 等待端口连接状态发生变化。

  2. hubd 驱动程序检测到端口连接。

  3. hubd 驱动程序枚举设备,创建子设备节点,然后连接客户机驱动程序。有关兼容名称的定义,请参阅绑定客户机驱动程序

  4. 客户机驱动程序管理设备。驱动程序处于 ONLINE 状态。

热移除

USB 设备的热移除的事件顺序如下:

  1. 集线器驱动程序 hubd(7D) 等待端口连接状态发生变化。

  2. hubd 驱动程序检测到端口断开连接。

  3. hubd 驱动程序将断开连接事件发送到子客户机驱动程序。如果子客户机驱动程序是 hubd 驱动程序或 usb_mid(7D) 多接口驱动程序,则子客户机驱动程序将该事件传播到其子级。

  4. 客户机驱动程序在内核线程上下文中接收断开连接事件通知。内核线程上下文使驱动程序的断开连接处理程序进入阻塞状态。

  5. 客户机驱动程序将转为 DISCONNECTED 状态。未完成的 I/O 传输将失败,完成原因为 device not responding。所有新 I/O 传输以及打开设备节点的尝试也将失败。要关闭管道,不需要客户机驱动程序。而要保存设备以及重新连接设备时需要恢复的驱动程序上下文,需要客户机驱动程序。

  6. hubd 驱动程序试图按照从下到上的顺序使 OS 设备节点及其子节点脱机。

如果在 hubd 驱动程序试图使设备节点脱机时,未打开该设备节点,则会发生以下事件:

  1. 将调用客户机驱动程序的 detach(9E) 入口点。

  2. 销毁设备节点。

  3. 新设备可以使用相应端口。

  4. 重新开始热插拔事件序列。hubd 驱动程序等待端口连接状态发生变化。

如果在 hubd 驱动程序试图使设备节点脱机时已打开该设备节点,则会发生以下事件:

  1. hubd 驱动程序将脱机请求放入定期脱机重试队列。

  2. 新设备仍然不可使用相应端口。

如果在 hubd 驱动程序试图使设备节点脱机时,已打开该设备节点,但用户稍后关闭了该设备节点,则 hubd 驱动程序定期使该设备节点脱机将成功,且会发生以下事件:

  1. 将调用客户机驱动程序的 detach(9E) 入口点。

  2. 销毁设备节点。

  3. 新设备可以使用相应端口。

  4. 重新开始热插拔事件序列。hubd 驱动程序等待端口连接状态发生变化。

如果用户关闭使用该设备的所有应用程序,则端口将重新变为可用。如果应用程序未终止或未关闭该设备,则端口仍然不可用。

热重新插入

如果将先前移除的设备重新插入同一端口,同时该设备的设备节点仍处于打开状态,则会发生以下事件:

  1. 集线器驱动程序 hubd(7D) 检测到端口连接。

  2. hubd 驱动程序恢复总线地址和设备配置。

  3. hubd 驱动程序取消脱机重试请求。

  4. hubd 驱动程序将连接事件发送到客户机驱动程序。

  5. 客户机驱动程序收到连接事件。

  6. 客户机驱动程序确定新设备是否与先前连接的设备相同。客户机驱动程序首先通过比较设备描述符来进行此项确定。客户机驱动程序也可以比较序列号和配置描述符群。

如果客户机驱动程序确定当前设备与先前连接的设备不同,则可能会发生以下事件:

  1. 客户机驱动程序可能向控制台发出警告消息。

  2. 用户可能再次移除该设备。如果用户再次移除该设备,则将重新开始热移除事件序列。hubd 驱动程序检测到端口断开连接。如果用户没有再次移除该设备,则会发生以下事件:

    1. 客户机驱动程序仍然保持 DISCONNECTED 状态,所有请求和打开操作将失败。

    2. 端口仍然不可用。用户必须关闭设备并断开其连接以释放端口。

    3. 释放端口时,将重新开始热插拔事件序列。hubd 驱动程序等待端口连接状态发生变化。

如果客户机驱动程序确定当前设备与先前连接的设备相同,则可能会发生以下事件:

  1. 客户机驱动程序可能恢复其状态,并继续正常操作。此策略由客户机驱动程序负责。音频扬声器就是客户机驱动程序可继续操作的典型示例。

  2. 如果使用重新连接的设备继续操作是安全的,则将重新开始热插拔事件序列。hubd 驱动程序等待端口连接状态发生变化。设备再次可用。

电源管理

本节讨论设备电源管理和系统电源管理。

设备电源管理根据各个 USB 设备的 I/O 是处于活动状态还是空闲状态来管理这些设备。

系统电源管理使用检查点和恢复机制在文件中设置系统状态的检查点,然后完全关闭系统。(检查点有时称为“系统暂停”。)再次打开系统电源时,系统将恢复为其暂停前的状态。

设备电源管理

下面简要列出了要对 USB 设备进行电源管理时驱动程序需要执行的操作。后面对电源管理进行了较详细的说明。

  1. 在执行 attach(9E) 期间创建电源管理组件。请参见 usb_create_pm_components(9F) 手册页。

  2. 实现 power(9E) 入口点。

  3. 在访问设备之前调用 pm_busy_component(9F)pm_raise_power(9F)

  4. 完成设备访问后调用 pm_idle_component(9F)

USBA 2.0 框架支持 USB 接口电源管理规范指定的四种电源级别。有关 USB 电源级别与操作系统电源级别对应关系的信息,请参见 /usr/include/sys/usb/usbai.h

当设备进入 USB_DEV_OS_PWR_OFF 状态时,hubd 驱动程序将暂停端口。当设备进入 USB_DEV_OS_PWR_1 及以上状态时,hubd 驱动程序将恢复端口。请注意,端口暂停不同于系统暂停。端口暂停时,将仅关闭 USB 端口。系统电源管理中定义了系统暂停。

客户机驱动程序可以选择在设备上启用远程唤醒。请参见 usb_handle_remote_wakeup(9F) 手册页。当 hubd 驱动程序在端口上发现远程唤醒时,hubd 驱动程序将完成唤醒操作,并调用 pm_raise_power(9F) 以通知子级。

下图显示了电源管理的不同部分之间的关系。

图 20–5 USB 电源管理

图中显示了使用两种不同电源管理方案的时机。

驱动程序可以实现图 20–5 底部说明的两种电源管理方案之一。被动方案比主动方案简单,这是因为被动方案在设备传输期间不进行电源管理。

主动电源管理

本节介绍实现主动电源管理方案需使用的函数。

在驱动程序的 attach(9E) 入口点执行以下工作:

  1. 调用 usb_create_pm_components(9F)

  2. 可选择调用 usb_handle_remote_wakeup(9F)(使用 USB_REMOTE_WAKEUP_ENABLE 作为第二个参数),以在设备上启用远程唤醒。

  3. 调用 pm_busy_component(9F)

  4. 调用 pm_raise_power(9F) 以使功耗达到 USB_DEV_OS_FULL_PWR 级别。

  5. 与设备通信以初始化该设备。

  6. 调用 pm_idle_component(9F)

在驱动程序的 detach(9E) 入口点执行以下工作:

  1. 调用 pm_busy_component(9F)

  2. 调用 pm_raise_power(9F) 以使功耗达到 USB_DEV_OS_FULL_PWR 级别。

  3. 如果在 attach(9E) 入口点中调用了 usb_handle_remote_wakeup (9F) 函数,请在此处调用 usb_handle_remote_wakeup(9F)(使用 USB_REMOTE_WAKEUP_DISABLE 作为第二个参数)。

  4. 与设备通信以干净地关闭该设备。

  5. 调用 pm_lower_power(9F) 以使功耗达到 USB_DEV_OS_PWR_OFF 级别。

    这是唯一一次客户机驱动程序调用 pm_lower_power(9F)。

  6. 调用 pm_idle_component(9F)

当驱动程序线程要启动在设备上执行 I/O 操作时,该线程将执行以下任务:

  1. 调用 pm_busy_component(9F)

  2. 调用 pm_raise_power(9F) 以使功耗达到 USB_DEV_OS_FULL_PWR 级别。

  3. 开始 I/O 传输。

当驱动程序收到 I/O 传输已完成的通知时,驱动程序将调用 pm_idle_component(9F)

在驱动程序的 power(9E) 入口点中,检查您要转换到的电源级别是否有效。此外,还可能需要考虑同时调用 power(9E) 的不同线程。

如果设备已空闲一段时间或者系统正在关闭,则可以调用 power(9E) 例程以使设备进入 USB_DEV_OS_PWR_OFF 状态。此状态对应于图 20–4 中所示的 PWRED_DWN 状态。如果设备将进入 USB_DEV_OS_PWR_OFF 状态,请在 power(9E) 例程中执行以下工作:

  1. 使所有打开的管道进入空闲状态。例如,停止对中断管道进行的轮询。

  2. 保存任何设备或需要保存的驱动程序上下文。

    在完成 power(9E) 调用后,将暂停设备所连接到的端口。

收到设备启动的远程唤醒或系统启动的唤醒时,可以调用 power(9E) 例程以打开设备电源。由于超出空闲时间或系统暂停而关闭设备电源后,将会发生唤醒通知。如果设备将进入 USB_DEV_OS_PWR_1 或以上状态,请在 power(9E) 例程中执行以下工作:

  1. 恢复任何所需的设备和驱动程序上下文。

  2. 在管道中重新启动适合指定电源级别的活动。例如,对中断管道启动轮询。

如果先前暂停了设备所连接到的端口,则在调用 power(9E) 之前将恢复该端口。

被动电源管理

被动电源管理方案比上面介绍的主动电源管理方案简单。在此被动方案中,在传输期间不执行任何电源管理。要实现此被动方案,请在打开设备时调用 pm_busy_component(9F)pm_raise_power(9F)。然后在关闭设备时调用 pm_idle_component(9F)

系统电源管理

系统电源管理包括:在保存整个系统的状态后关闭系统,以及在重新打开系统后恢复状态。此过程称为 CPR(checkpoint and resume,检查点和恢复)。在 CPR 相关方面,USB 客户机驱动程序的运行方式与其他客户机驱动程序相同。要暂停设备,请在 cmd 参数为 DDI_SUSPEND 的情况下调用驱动程序的 detach(9E) 入口点。要恢复设备,请在 cmd 参数为 DDI_RESUME 的情况下调用驱动程序的 attach(9E) 入口点。处理 detach(9E) 例程中的 DDI_SUSPEND 命令时,请尽可能地清理设备状态和驱动程序状态,以满足后面清理恢复操作的需要。(请注意,这对应于图 20–4 中的 SUSPENDED 状态。)处理 attach(9E) 例程中的 DDI_RESUME 命令时,务必使设备达到全功率状态,以使设备与系统同步。

对于 USB 设备,暂停和恢复的处理与热插拔断开连接和重新连接类似(请参见热插拔 USB 设备)。CPR 与热插拔之间的重要差别是,在 CPR 的情况下,如果设备处于不可暂停的状态,驱动程序的检查点过程可能会失败。例如,如果设备正在进行错误恢复,则无法暂停设备。如果设备正忙,无法安全将其停止,也无法暂停该设备。

序列化

通常,驱动程序在持有互斥锁时不应调用 USBA 函数。因此,客户机驱动程序中的竞态条件可能很难防止。

不允许在处理异步事件(如断开连接或 CPR)的同时运行正常操作代码。这些类型的异步事件通常会清理和中断管道,可能会破坏正常操作代码。

一种管理竞态条件和保护正常操作代码的方法是,编写可以获取和释放独占访问同步对象的序列化工具。您可以按以下方法编写序列化工具:通过调用 USBA 函数安全地持有同步对象。usbskel 驱动程序样例中就采用了这种方法。有关 usbskel 驱动程序的信息,请参见USB 设备驱动程序样例

实用程序函数

本节介绍几个常规用途的函数。

设备配置工具

本节介绍与设备配置相关的函数。

获取接口编号

如果您使用的是多接口设备,usb_mid(7D) 驱动程序只会使其接口之一可用于调用驱动程序,此时您可能需要知道调用驱动程序所绑定到的接口的编号。使用 usb_get_if_number(9F) 函数执行以下任一任务:

管理整个设备

如果驱动程序管理整个复合设备,则可通过使用包含供应商 ID、产品 ID 和修订版 ID 的兼容名称将该驱动程序绑定到整个设备。绑定到整个复合设备的驱动程序必须像结点驱动程序一样管理该设备的所有接口。通常,不应将驱动程序绑定到整个复合设备。应改为使用一般的多接口驱动程序 usb_mid(7D)

使用 usb_owns_device(9F) 函数可确定驱动程序是否拥有整个设备。设备可以是复合设备。如果驱动程序拥有整个设备,则 usb_owns_device(9F) 函数将返回 TRUE

多配置设备

在任何特定时间,主机上只能使用 USB 设备的一种配置。大多数设备仅支持一种配置。但是,少数 USB 设备支持多种配置。

对于具有多种配置的任何设备,都是采用可使用某驱动程序的第一种配置。查找匹配项时,设备配置以数字顺序处理。如果未找到任何匹配的驱动程序,则设备将被设置采用第一种配置。在这种情况下,usb_mid 驱动程序将接管该设备,并将设备拆分为多个接口节点。使用 usb_get_cfg(9F) 函数可返回设备的当前配置。

您可以使用以下两种方法中的任何一种来请求采用其他配置。使用其中任何一种方法修改设备配置,均可确保 USBA 模块保持与设备同步。


注意 – 注意 –

不要通过手动执行 SET_CONFIGURATION USB 请求来更改设备配置。不支持使用 SET_CONFIGURATION 请求更改配置。


修改或获取替代设置

客户机驱动程序可以调用 usb_set_alt_if(9F) 函数以更改当前选定接口的选定替代设置。请确保关闭已明确打开的所有管道。切换替代设置时,usb_set_alt_if(9F) 函数将验证是否仅打开了缺省管道。确保在调用 usb_set_alt_if(9F) 之前已正确设置了设备。

更改替代设置可能会影响对驱动程序可用的端点以及特定于类和特定于供应商的描述符。有关端点和描述符的更多信息,请参见描述符树

调用 usb_get_alt_if(9F) 函数可检索当前替代设置的编号。


注 –

请求新替代设置、新配置或新接口时,必须关闭设备的除缺省管道外的所有管道。这是因为更改替代设置、配置或接口会更改设备的运行模式。此外,更改替代设置、配置或接口还会更改设备在系统中的呈现方式。


其他实用程序函数

本节介绍在 USB 设备驱动程序中有用的其他函数。

检索字符串描述符

调用 usb_get_string_descr(9F) 函数可检索给定了索引的字符串描述符。一些配置、接口或设备描述符具有关联的字符串 ID。这样的描述符包含具有非零值的字符串索引字段。将字符串索引字段值传递给 usb_get_string_descr(9F) 可检索对应的字符串。

管道专用数据工具

每个管道都有一个空间指针,专供客户机驱动程序使用。使用 usb_pipe_set_private(9F) 函数可安装一个值。使用 usb_pipe_get_private(9F) 函数可检索该值。当管道可能需要将其自己的客户机定义状态传递到回调,以进行特定处理时,此工具在回调中很有用。

清除 USB 条件

使用 usb_clr_feature(9F) 函数可执行以下任务:

获取设备、接口或端点状态

使用 usb_get_status(9F) 函数可发出 USB GET_STATUS 请求,以检索设备、接口或端点的状态。

获取设备的总线地址

使用 usb_get_addr(9F) 函数可获取设备的 USB 总线地址以用于调试目的。此地址映射到特定的 USB 端口。

USB 设备驱动程序样例

本节介绍使用 Solaris 环境的 USBA 2.0 框架的 USB 设备驱动程序模板。此驱动程序演示了本章中讨论的许多功能。此模板或框架驱动程序的名称为 usbskel

usbskel 驱动程序是可用于启动您自己的 USB 设备驱动程序的模板。usbskel 驱动程序演示了以下功能:

usbskel 驱动程序可在 Sun Web 站点 http://www.sun.com/bigadmin/software/usbskel/ 上获取。

有关其他 USB 驱动程序的源代码,请参见 OpenSolaris Web 站点。请访问 http://hub.opensolaris.org/bin/view/Main/,然后单击页面左侧菜单中的 "Source Browser"(源代码浏览器)。