字符设备没有可物理寻址的存储介质(如磁带机或串行端口),在这些存储介质中 I/O 通常是以字节流的形式执行的。本章介绍字符设备驱动程序的结构,其中重点介绍字符驱动程序的入口点。此外,本章还介绍在同步和异步 I/O 传输上下文中 physio(9F) 和 aphysio(9F) 的用法。
本章介绍有关以下主题的信息:
图 15–1 显示了用来定义字符设备驱动程序结构的数据结构和例程。设备驱动程序通常包括以下元素:
可装入设备的驱动程序段
设备配置部分
字符驱动程序入口点
下图中涂有阴影的设备访问部分列出了字符驱动程序入口点。

对于每个设备驱动程序,都有一个 dev_ops(9S) 结构与之关联,该结构进而又指向 cb_ops(9S) 结构。这些结构包含指向驱动程序入口点的指针:
可以根据需要将其中一些入口点替换为 nodev(9F) 或 nulldev(9F)。
attach(9E) 例程应执行所有设备需要的常见初始化任务,例如:
分配每个实例的状态结构
注册设备中断
映射设备寄存器
初始化互斥变量和条件变量
创建可进行电源管理的组件
创建次要节点
有关这些任务的代码示例,请参见attach() 入口点。
字符设备驱动程序将创建类型为 S_IFCHR 的次要节点。类型为 S_IFCHR 的次要节点会使代表节点的字符特殊文件最终出现在 /devices 分层结构中。
以下示例显示了字符驱动程序的典型 attach(9E) 例程。与设备有关的属性通常在 attach() 例程中声明。该示例使用了预定义的 Size 属性。在获取块设备的分区大小时,Size 与 Nblocks 属性是等效的。举例来说,如果要在磁盘设备上执行字符 I/O,就可以使用 Size 来获取分区大小。因为 Size 是 64 位属性,所以必须使用 64 位属性接口。在本例中,使用 ddi_prop_update_int64(9F)。有关属性的更多信息,请参见设备属性。
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(9E) 的语法如下所示:
int xxopen(dev_t *devp, int flag, int otyp, cred_t *credp);
其中:
指向设备编号的指针。会向 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 的主设备号。驱动程序必须在内部跟踪可用的次要设备号。
使用位指示打开设备是供读取 (FREAD)、写入 (FWRITE) 还是供可同时读写的标志。发出 open(2) 系统调用的用户线程也可以请求对设备进行独占访问 (FEXCL),或指定不得以任何原因阻止打开操作 (FNDELAY),但驱动程序必须强制执行两者。只写设备(例如打印机)的驱动程序可能会将 open(9E) 视为对读操作无效。
表示如何调用 open() 的整数。驱动程序必须检查 otyp 的值是否适用于相应设备。对于字符驱动程序,otyp 应为 OTYP_CHR(请参见 open(9E) 手册页)。
指向包含有关调用方信息(例如用户 ID 和组 ID)的凭证结构的指针。驱动程序不会直接检查此结构,但会使用 drv_priv(9F) 来检查超级用户权限的一般情况。在本示例中,只允许 root 或具有 PRIV_SYS_DEVICES 权限的用户打开设备进行写入。
以下示例显示了字符驱动程序的 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(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 执行完毕的驱动程序会一直等待下去。有关如何避免此问题的信息,请参见线程无法接收信号。
当用户线程发出 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) 可以对驱动程序或设备内存的连续区域执行完整的数据传入或传出操作。
在字符驱动程序中,传输由 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_offset 或 uio_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_offset 或 uio_loffset 字段都不需要调整。以此方式对设备执行的 I/O 操作受 uio_offset 或 uio_loffset 的最大可能值约束。对磁盘的原始 I/O 操作即是此用法的一个示例。
如果设备没有位置概念,则驱动程序会采取下列步骤:
保存 uio_offset 或 uio_loffset。
执行 I/O 操作。
将 uio_offset 或 uio_loffset 恢复为字段的初始值。
以此方式对设备执行的 I/O 操作不受 uio_offset 或 uio_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 操作完成之后。
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 设备依赖 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 */
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) 结构描述的区域中。
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);
}
字符驱动程序通常在 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() 入口点。
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) 设置的错误字段确定。
支持 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)。
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() 入口点是指向要由 physio(9F) 或 aphysio(9F) 调用的函数的指针。xxminphys 的用途在于确保所请求的传输大小不会超过驱动程序强加的限制。如果用户请求较大的传输,则会重复调用 strategy(9E),这就要求每次都不能超过强加的限制。因为 DMA 资源有限,所以该方法非常重要。对于慢速设备(例如打印机)的驱动程序,应避免其长时间占用资源。
通常,驱动程序会传递内核函数 minphys(9F) 的地址,但驱动程序也可以定义自己的 xxminphys() 例程。xxminphys() 的作用是使 buf(9S) 结构的 b_bcount 字段保持在驱动程序限制内。驱动程序还应遵循其他系统限制。例如,驱动程序的 xxminphys() 例程应该在设置 b_bcount 字段之后而在返回之前调用系统 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(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 字段检索到的。
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() 接收指向可能产生中断的设备的状态结构的指针。
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) 例程。
一个线程有时需要处理多个文件描述符上的 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) 入口点中,驱动程序必须遵循下列规则:
在调用 chpoll(9E) 入口点时执行以下算法:
if ( /* events are satisfied now */ ) {
*reventsp = mask_of_satisfied_events
} else {
*reventsp = 0;
if (!anyyet)
*phpp = &local_pollhead_structure;
}
return (0);
有关要检查的事件的论述,请参见 chpoll(9E) 手册页。然后,chpoll(9E) 入口点应通过在 *reventsp 中设置返回事件,来返回满足要求的事件的掩码。
如果没有发生任何事件,则清除事件的返回字段。如果未设置 anyyet 字段,则驱动程序必须返回 pollhead 结构的实例。通常在状态结构中分配 pollhead 结构。驱动程序应该将 pollhead 结构视为不透明。不能引用任何 pollhead 字段。
只要出现示例 15–10 中列出的 events 类型的设备条件,就会调用 pollwakeup(9F)。一次只能对一个事件调用此函数。当出现这种条件时,可以在中断例程中调用 pollwakeup(9F)。
示例 15–10 和示例 15–11 说明了如何实现轮询规程以及如何使用 pollwakeup(9F)。
以下示例说明如何处理 POLLIN 和 POLLERR 事件。首先,驱动程序读取状态寄存器以确定设备的当前状态。参数 events 指定驱动程序应该检查哪些条件。如果出现适当的条件,驱动程序会在 *reventsp 中设置相应的位。如果未出现任何条件并且没有设置 anyyet,则会在 *phpp 中返回 pollhead 结构的地址。
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) 并获取相同的锁,则会导致死锁。
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);
}
当用户线程对与设备相关的文件描述符发出 ioctl(2) 系统调用时,就会调用 ioctl(9E) 例程。I/O 控制机制是获取和设置设备特定参数的统称。该机制经常用于设置设备特定模式(通过设置内部驱动程序软件标志或将命令写入设备)。也可以使用该控制机制向用户返回有关当前设备状态的信息。简而言之,控制机制可以做应用程序和驱动程序需要完成的任何事情。
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 语句。
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 控制的应用程序。
#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);
}
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) 例程中。可以将此标志设置为以下值之一:
DATAMODEL_ILP32
DATAMODEL_LP64
可以有条件地定义 FNATIVE,以匹配内核实现的数据模型。应使用 FMODELS 掩码来提取 mode 参数的标志。驱动程序随后会对数据模型进行明确检查,以确定如何复制应用程序的数据结构。
DDI 函数 ddi_model_convert_from(9F) 是一个公用例程,可帮助一些驱动程序完成它们的 ioctl() 调用。该函数将用户应用程序的数据类型模型用作参数,并返回下列值之一:
DDI_MODEL_ILP32-从 ILP32 应用程序进行转换
DDI_MODEL_NONE-无需转换
如果不必进行数据转换,则会返回 DDI_MODEL_NONE。当应用程序和驱动程序具有相同的数据模型时,便会发生这种情况。DDI_MODEL_ILP32 将返回到被编译为 LP64 模式而且能够与 32 位应用程序进行通信的驱动程序。
在以下示例中,驱动程序复制了包含用户地址的数据结构。数据结构的处理能力从 ILP32 更改为 LP64。相应地,此 64 位驱动程序在与 32 位应用程序进行通信时使用 32 位版本的结构。
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);
}
驱动程序有时需要将不再适于 32 位大小结构的本机数值复制出来。在这种情况下,驱动程序应向调用方返回 EOVERFLOW。EOVERFLOW 用于表明接口中的数据类型太小,无法保存要返回的值,如以下示例中所示。
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);
}
示例 15–15 中的方法适用于许多驱动程序。另一种方案是使用 <sys/model.h> 中提供的数据结构宏在应用程序和内核之间移动数据。从功能角度看,这些宏减少了代码混乱问题,并使代码的表现形式完全相同。
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" 附加到本机数据结构名称,就形成了阴影数据结构的名称。为方便起见,将阴影结构的定义放置到与本机结构相同的文件中,以降低将来的维护成本。
这些宏可以采用下列参数:
数据结构的本机形式的结构名称,即在 struct 关键字后输入的内容。
包含用户数据模型(例如 FILP32 或 FLP64)的标志字,从 ioctl(9E) 的模式参数中提取。
此名称用于引用这些宏所处理的结构的特定实例。
结构内部的字段的名称。
宏使您能够仅对数据项的字段进行适当地引用。宏不提供采用基于数据模型的单独代码路径的方法。如果数据结构中的字段数量很大,则应避免使用宏。如果对这些字段的引用非常频繁,也应避免使用宏。
在实现宏的过程中,宏隐藏了数据模型之间的很多差异。因此,使用此接口编写的代码通常比较容易理解。如果驱动程序以 32 位方式编译,则生成的代码较为简洁,并且无需冗长的 #ifdefs,但仍保留了类型检查。
可以使用 STRUCT_DECL(9F) 和 STRUCT_INIT(9F) 声明和初始化句柄及空间,以便在栈中对 ioctl 进行解码。STRUCT_HANDLE(9F) 和 STRUCT_SET_HANDLE(9F) 可以声明和初始化句柄,但不在栈中分配空间。如果结构非常大,或者包含在其他某个数据结构中,则后面的宏比较有用。
因为 STRUCT_DECL(9F) 和 STRUCT_HANDLE(9F) 宏扩展为数据结构声明,所以这些宏在 C 代码中应该使用这些声明进行分组。
用于声明和初始化结构的宏如下所示:
声明为 structname 数据结构调用了 handle 的结构句柄。STRUCT_DECL 按其本机形式在栈中分配空间。假定本机形式大于或等于结构的 ILP32 形式。
将 handle 的数据模型初始化为 umodel。在对使用 STRUCT_DECL(9F) 声明的结构句柄进行任何访问之前,必须调用此宏。
声明调用了 handle 的结构句柄。它与 STRUCT_DECL(9F) 相对。
将 handle 的数据模型初始化为 umodel,然后将 addr 设置为用于后续处理的缓冲区。在访问使用 STRUCT_DECL(9F) 声明的结构句柄之前,请调用此宏。
用于在结构上执行操作的宏如下所示:
返回 handle 所引用的结构的大小(取决于该结构的嵌入式数据模型)。
返回 handle 所引用的数据结构中的指定字段。此字段为非指针类型。
返回 handle 所引用的数据结构中的指定字段。此字段为指针类型。
将 handle 所引用的数据结构中的指定字段设置为值 val。val 的类型应与 fieldname 的类型相匹配。此字段为非指针类型。
将 handle 所引用的数据结构中的指定字段设置为值 val。此字段为指针类型。
返回 handle 所引用的数据结构中的指定字段的地址。
返回指向 handle 所描述的本机结构的指针。
其他一些结构宏如下所示:
返回 struct_name 的大小(取决于给定的数据模型)。
根据给定的数据模型,返回指针的大小。