编写设备驱动程序

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)