编写设备驱动程序

第 1 章 Solaris 设备驱动程序概述

本章概述了 Solaris 设备驱动程序。本章提供有关以下主题的信息:

设备驱动程序基础知识

本节介绍 Solaris 平台上的设备驱动程序及其入口点。

什么是设备驱动程序?

设备驱动程序是一种内核模块,负责管理硬件设备的底层 I/O 操作。设备驱动程序是使用标准接口编写的,内核可通过调用该标准接口与设备进行交互。设备驱动程序也可以是仅针对软件的,即模拟仅存在于软件中的设备,如 RAM 磁盘、总线以及伪终端。

设备驱动程序包含与设备进行通信时所需的所有特定于设备的代码。此代码包括一组用于系统其余部分的标准接口。就像系统调用接口可使应用程序不受平台特定信息影响一样,此接口可保护内核不受设备特定信息的影响。应用程序和内核其余部分需要非常少的特定于设备的代码(如果有)对此设备进行寻址。这样,设备驱动程序使得系统的可移植性更强,并更易于维护。

初始化 Solaris 操作系统 (Solaris operating system, Solaris OS) 后,设备会进行自标识并组织为设备树,即设备分层结构。实际上,设备树是内核的硬件模型。单个设备驱动程序表示为树中的一个节点,并且不包含任何子节点。此类型的节点称为叶驱动程序。为其他驱动程序提供服务的驱动程序称为总线结点驱动程序,并显示为包含子节点的节点。在引导过程中,物理设备会映射到树中的驱动程序,以便可以在需要时找到这些驱动程序。有关 Solaris OS 如何使用设备的更多信息,请参见第 2 章

设备驱动程序按照处理 I/O 的方式可以分为以下三大类别:

什么是设备驱动程序入口点?

入口点是设备驱动程序内的一个函数,外部实体可调用此函数以访问某种驱动程序功能或运行某个设备。 每个设备驱动程序都提供一组标准函数作为入口点。有关所有驱动程序类型入口点的完整列表,请参见 Intro(9E) 手册页。Solaris 内核使用入口点执行以下常见任务:

根据设备执行的操作类型,不同类型设备的驱动程序具有不同的入口点集。例如,对于内存映射的面向字符的设备,其驱动程序支持 devmap(9E) 入口点,而块驱动程序不支持此入口点。

使用基于驱动程序名称的前缀可为驱动程序函数指定唯一的名称。通常,此前缀是驱动程序的名称,例如 xx_open() 代表驱动程序 xx 的 open(9E) 例程。有关更多信息,请参见使用唯一前缀来避免内核符号冲突。在本书后面的示例中,xx 用作驱动程序前缀。

设备驱动程序入口点

本节提供以下类别的入口点列表:

通用于所有驱动程序的入口点

有些操作可由任何类型的驱动程序执行,如装入模块所需的函数以及必需的自动配置入口点所需的函数。本节介绍通用于所有驱动程序的入口点类型。通用入口点汇总中列出了通用入口点,并包含指向手册页以及其他相关讨论的链接。

设备访问入口点

字符设备和块设备的驱动程序导出 cb_ops(9S) 结构,该结构定义用于块设备访问和字符设备访问的驱动程序入口点。这两种类型的驱动程序都需要支持 open(9E)close(9E) 入口点。块驱动程序需要支持 strategy(9E),而字符驱动程序可选择实现适用于设备类型的 read(9E)write(9E)ioctl(9E)mmap(9E)devmap(9E) 入口点的任意组合。字符驱动程序还可通过 chpoll(9E) 支持轮询接口。块驱动程序以及那些可使用块文件系统和字符文件系统的驱动程序可通过 aread(9E)awrite(9E) 支持异步 I/O。

可装入模块入口点

所有驱动程序都需要实现可装入模块入口点 _init(9E)_fini(9E)_info(9E),以便装入、卸载和报告有关驱动程序模块的信息。

驱动程序应在 _init(9E) 中分配并初始化所有全局资源,并应在 _fini(9E) 中释放其资源。


注 –

在 Solaris OS 中,只有可装入模块例程必须在驱动程序对象模块的外部可见。其他例程可具有存储类 static


自动配置入口点

对于设备自动配置,驱动程序需要实现 attach(9E)detach(9E)getinfo(9E) 入口点。设备(如 SCSI 目标设备)在引导过程中无法自标识时,驱动程序也可以实现可选入口点 probe(9E)。有关这些例程的更多信息,请参见第 6 章

内核统计信息入口点

Solaris 平台提供了一组丰富的接口来维护和导出内核级统计信息(也称为 kstat)。驱动程序可以自由使用这些接口导出驱动程序和设备的统计信息,用户应用程序可使用这些统计信息来查看驱动程序的内部状态。提供了两个入口点来处理内核统计信息:

有关详细信息,请参见 kstat_create(9F)kstat(9S) 手册页。另请参见内核统计信息

电源管理入口点

提供电源管理功能的硬件设备的驱动程序可支持可选的 power(9E) 入口点。有关此入口点的详细信息,请参见第 12 章

通用入口点汇总

下表列出了所有类型驱动程序都可使用的入口点。

表 1–1 用于所有驱动程序类型的入口点

类别/入口点 

使用情况 

说明 

cb_ops 入口点

   

open(9E)

必需 

获取访问设备的权限。有关其他信息,请参见: 

close(9E)

必需 

放弃访问设备的权限。STREAMS 驱动程序的 close() 版本具有不同于字符驱动程序和块驱动程序的签名。有关其他信息,请参见:

可装入模块入口点

   

_init(9E)

必需 

初始化可装入模块。有关其他信息,请参见: 可装入驱动程序接口

_fini(9E)

必需 

准备可装入模块以进行卸载。该入口点是所有驱动程序类型所必需的。有关其他信息,请参见: 可装入驱动程序接口

_info(9E)

必需 

返回有关可装入模块的信息。有关其他信息,请参见: 可装入驱动程序接口

自动配置入口点

   

attach(9E)

必需 

在初始化过程中向系统添加设备。此外,还用于恢复已暂停的系统。有关其他信息,请参见: attach() 入口点

detach(9E)

必需 

从系统中分离设备。此外,还用于临时暂停设备。有关其他信息,请参见: detach() 入口点

getinfo(9E)

必需 

获取特定于驱动程序的设备信息,如设备编号和相应实例之间的映射。有关其他信息,请参见: 

probe(9E)

请参见说明 

确定是否存在非自标识设备。该入口点是无法进行自标识的设备所必需的。有关其他信息,请参见: 

内核统计信息入口点

   

ks_snapshot(9E)

可选 

捕获 kstat(9S) 数据的快照。有关其他信息,请参见: 内核统计信息

ks_update(9E)

可选 

动态更新 kstat(9S) 数据。有关其他信息,请参见: 内核统计信息

电源管理入口点

   

power(9E)

必需 

设置设备的电源级别。如果不使用此入口点,则设置为 NULL。有关其他信息,请参见: power() 入口点

其他入口点

   

prop_op(9E)

请参见说明 

报告驱动程序属性信息。除非替换 ddi_prop_op(9F),否则此入口点是必需的。有关其他信息,请参见:

dump(9E)

请参见说明 

系统出现故障时将内存转储到设备。对于出现紧急情况时要用作转储设备的任何设备,该入口点是必需的。有关其他信息,请参见: 

identify(9E)

已过时 

请勿使用此入口点。请在 dev_ops 结构中将 nulldev(9F) 指定给此入口点。

用于块设备驱动程序的入口点

支持文件系统的设备称为块设备。为这些设备编写的驱动程序称为块设备驱动程序。块设备驱动程序接受 buf(9S) 结构形式的文件系统请求,并向磁盘发出 I/O 操作以传送指定的块。文件系统的主接口为 strategy(9E) 例程。有关更多信息,请参见第 16 章

块设备驱动程序还可以提供字符驱动程序接口,以使实用程序能够绕过文件系统并直接访问设备。这种设备访问通常称为块设备的原始接口。

下表列出了块设备驱动程序可使用的其他入口点。另请参见通用于所有驱动程序的入口点

表 1–2 用于块驱动程序的其他入口点

入口点 

使用情况 

说明 

aread(9E)

可选 

执行异步读取。不支持 aread() 入口点的驱动程序应使用 nodev(9F) 错误返回函数。有关其他信息,请参见:

awrite(9E)

可选 

执行异步写入。不支持 awrite() 入口点的驱动程序应使用 nodev(9F) 错误返回函数。有关其他信息,请参见:

print(9E)

必需 

在系统控制台上显示驱动程序消息。有关其他信息,请参见: print() 入口点(块驱动程序)

strategy(9E)

必需 

执行块 /O。其他信息: 

用于字符设备驱动程序的入口点

字符设备驱动程序通常以字节流的形式执行 I/O 操作。使用字符驱动程序的设备包括磁带机和串行端口。字符设备驱动程序还可以提供块驱动程序中不存在的其他接口,如 I/O 控制 (ioctl) 命令、内存映射以及设备轮询。有关更多信息,请参见第 15 章

任何设备驱动程序的主要任务都是执行 I/O 操作,许多其他字符设备驱动程序执行称为字节流字符 I/O 的操作。驱动程序可在设备上来回传送数据,而无需使用特定设备地址。此类型的传送与块设备驱动程序中的相反,后者部分文件系统请求会标识设备上的特定位置。

read(9E)write(9E) 入口点可处理标准字符驱动程序的字节流 I/O。有关更多信息,请参见I/O 请求处理

下表列出了字符设备驱动程序可使用的其他入口点。有关其他入口点的信息,请参见通用于所有驱动程序的入口点

表 1–3 用于字符驱动程序的其他入口点

入口点 

使用情况 

说明 

chpoll(9E)

可选 

针对非 STREAMS 字符驱动程序轮询事件。有关其他信息,请参见: 对文件描述符执行多路复用 I/O 操作

ioctl(9E)

可选 

针对字符驱动程序执行一系列 I/O 命令。ioctl() 例程必须确保根据需要显式使用 copyin(9F)copyout(9F)ddi_copyin(9F)ddi_copyout(9F) 在内核地址空间复制用户数据。有关其他信息,请参见:

read(9E)

必需 

从设备读取数据。有关其他信息,请参见: 

segmap(9E)

可选 

将设备内存映射到用户空间。有关其他信息,请参见: 

write(9E)

必需 

将数据写入设备。有关其他信息,请参见: 

用于 STREAMS 设备驱动程序的入口点

STREAMS 是一个独立的编程模型,用于编写字符驱动程序。异步接收数据的设备(如终端设备和网络设备)适合实现 STREAMS。STREAMS 设备驱动程序必须提供第 6 章中介绍的装入和自动配置支持。有关如何编写 STREAMS 驱动程序的其他信息,请参见《STREAMS Programming Guide》

下表列出了 STREAMS 设备驱动程序可使用的其他入口点。有关其他入口点的信息,请参见通用于所有驱动程序的入口点用于字符设备驱动程序的入口点

表 1–4 用于 STREAMS 驱动程序的入口点

入口点 

使用情况 

说明 

put(9E)

请参见说明 

协调以流的形式将消息从一个队列传递到下一个队列。此入口点是必需的,但是读取数据的驱动程序端除外。有关其他信息,请参见: 《STREAMS Programming Guide 》

srv(9E)

必需 

处理队列中的消息。有关其他信息,请参见: 《STREAMS Programming Guide 》

用于内存映射设备的入口点

对于某些设备,如帧缓存器,为应用程序提供对设备内存的直接访问比提供字节流 I/O 更有效。应用程序使用 mmap(2) 系统调用可将设备内存映射到其地址空间。要支持内存映射,设备驱动程序需要实现 segmap(9E)devmap(9E) 入口点。有关 devmap(9E) 的信息,请参见第 10 章。有关 segmap(9E) 的信息,请参见第 15 章

定义 devmap(9E) 入口点的驱动程序通常不会定义 read(9E)write(9E) 入口点,因为应用程序在调用 mmap(2) 之后会直接对设备执行 I/O 操作。

下表列出了使用 devmap 框架执行内存映射的字符设备驱动程序可以使用的其他入口点。有关其他入口点的信息,请参见通用于所有驱动程序的入口点用于字符设备驱动程序的入口点

表 1–5 使用 devmap 进行内存映射的字符驱动程序的入口点

入口点 

使用情况 

说明 

devmap(9E)

必需 

验证和转换内存映射设备的虚拟映射。有关其他信息,请参见: 导出映射

devmap_access(9E)

可选 

访问具有验证或保护问题的映射时通知驱动程序。有关其他信息,请参见: devmap_access() 入口点

devmap_contextmgt(9E)

必需 

对映射执行设备上下文切换。有关其他信息,请参见: devmap_contextmgt() 入口点

devmap_dup(9E)

可选 

复制设备映射。有关其他信息,请参见: devmap_dup() 入口点

devmap_map(9E)

可选 

创建设备映射。有关其他信息,请参见: devmap_map() 入口点

devmap_unmap(9E)

可选 

取消设备映射。有关其他信息,请参见: devmap_unmap() 入口点

网络设备驱动程序入口点

有关使用 Generic LAN Driver v3 (GLDv3) 框架的网络设备驱动程序的入口点列表,请参见表 19–1。有关更多信息,请参见第 19 章中的GLDv3 网络设备驱动程序框架GLDv3 MAC 注册函数

用于 SCSI HBA 驱动程序的入口点

下表列出了 SCSI HBA 设备驱动程序可使用的其他入口点。有关 SCSI HBA 传输结构的信息,请参见 scsi_hba_tran(9S)。有关其他入口点的信息,请参见通用于所有驱动程序的入口点用于字符设备驱动程序的入口点

表 1–6 用于 SCSI HBA 驱动程序的其他入口点

入口点 

使用情况 

说明 

tran_abort(9E)

必需 

异常中止已传输到 SCSI 主机总线适配器 (Host Bus Adapter, HBA) 驱动程序的指定 SCSI 命令。有关其他信息,请参见: tran_abort() 入口点

tran_bus_reset(9E)

可选 

重置 SCSI 总线。有关其他信息,请参见: tran_bus_reset() 入口点

tran_destroy_pkt(9E)

必需 

释放为 SCSI 包分配的资源。有关其他信息,请参见: tran_destroy_pkt() 入口点

tran_dmafree(9E)

必需 

释放已为 SCSI 包分配的 DMA 资源。有关其他信息,请参见: tran_dmafree() 入口点

tran_getcap(9E)

必需 

获取 HBA 驱动程序所提供的特定功能的当前值。有关其他信息,请参见: tran_getcap() 入口点

tran_init_pkt(9E)

必需 

分配和初始化 SCSI 包的资源。有关其他信息,请参见: 资源分配

tran_quiesce(9E)

可选 

停止 SCSI 总线上的所有活动,通常是为了进行动态重新配置。有关其他信息,请参见: 动态重新配置

tran_reset(9E)

必需 

重置 SCSI 总线或目标设备。有关其他信息,请参见: tran_reset() 入口点

tran_reset_notify(9E)

可选 

请求通知 SCSI 目标设备进行总线重置。有关其他信息,请参见: tran_reset_notify() 入口点

tran_setcap(9E)

必需 

设置 SCSI HBA 驱动程序所提供的特定功能的值。有关其他信息,请参见: tran_setcap() 入口点

tran_start(9E)

必需 

请求传输 SCSI 命令。有关其他信息,请参见: tran_start() 入口点

tran_sync_pkt(9E)

必需 

按 HBA 驱动程序或设备同步数据视图。有关其他信息,请参见: tran_sync_pkt() 入口点

tran_tgt_free(9E)

可选 

代表目标设备请求释放已分配的 SCSI HBA 资源。有关其他信息,请参见: 

tran_tgt_init(9E)

可选 

代表目标设备请求初始化 SCSI HBA 资源。有关其他信息,请参见: 

tran_tgt_probe(9E)

可选 

探测 SCSI 总线上的指定目标。有关其他信息,请参见: tran_tgt_probe() 入口点

tran_unquiesce(9E)

可选 

调用 tran_quiesce(9E)(通常是为了进行动态重新配置)之后恢复 SCSI 总线上的 I/O 活动。有关其他信息,请参见: 动态重新配置

用于 PC 卡驱动程序的入口点

下表列出了 PC 卡设备驱动程序可使用的其他入口点。有关其他入口点的信息,请参见通用于所有驱动程序的入口点用于字符设备驱动程序的入口点

表 1–7 仅适用于 PC 卡驱动程序的入口点

入口点 

使用情况 

说明 

csx_event_handler(9E)

必需 

处理 PC 卡驱动程序的事件。驱动程序必须显式调用 csx_RegisterClient(9F) 函数来设置入口点,而不是使用类似 cb_ops 的结构字段。

设备驱动程序设计注意事项

从服务的使用方和提供方角度来看,设备驱动程序都必须与 Solaris OS 兼容。本节所讨论的以下问题是设计设备驱动程序过程中需要考虑的问题:

DDI/DKI 功能

为了使驱动程序具有可移植性,提供了 Solaris DDI/DKI 接口。利用 DDI/DKI,开发者可采用标准方式编写驱动程序代码,而不必担心硬件或平台差异。本节介绍 DDI/DKI 接口的各个方面。

设备 ID

利用 DDI 接口,驱动程序可以为设备提供永久、唯一的标识符。可以使用设备 ID 标识或查找设备。此 ID 与设备的名称或编号 (dev_t) 无关。应用程序可以使用 libdevid(3LIB) 中定义的函数来读取和处理由驱动程序注册的设备 ID。

设备属性

设备或设备驱动程序的特性 (attribute) 通过属性 (property) 指定。属性是一个名称/值对。名称是标识具有关联值的属性的字符串。属性可以由自标识设备的 FCode、硬件配置文件(请参见 driver.conf(4) 手册页)或驱动程序自身使用 ddi_prop_update(9F) 系列例程进行定义。

中断处理

DDI/DKI 解决了设备中断处理的以下方面的问题:

设备中断源包含在称为 interrupt 的属性中,此属性既可由自标识设备的 PROM(位于硬件配置文件中)提供,也可由 x86 平台上的引导系统提供。

回调函数

某些 DDI 机制提供回调机制。DDI 函数提供一种在满足某个条件时调度回调的机制。在以下典型的情况下,可以使用回调函数:

回调函数在某种程度上与入口点(例如,中断处理程序)类似。允许回调的 DDI 函数期望回调函数执行特定任务。如果使用 DMA 例程,则回调函数必须返回一个值,指示是否需要在出现故障时重新调度此函数。

回调函数作为单独的中断线程执行。回调必须处理所有常见的多线程问题。


注 –

驱动程序在分离设备之前必须先取消所有已调度的回调函数。


软件状态管理

为了帮助设备驱动程序编写人员分配状态结构,DDI/DKI 提供了一组称为软件状态管理例程的内存管理例程,也称为软状态例程。这些例程可动态分配、检索以及销毁指定大小的内存项,并可隐藏列表管理的详细信息。可使用实例编号来标识所需内存项。此编号通常为系统指定的实例编号。

这些例程用于实现以下任务:

有关如何使用这些例程的示例,请参见可装入驱动程序接口

程控 I/O 设备访问

程控 I/O 设备访问是指通过主机 CPU 读/写设备寄存器或设备内存的行为。Solaris DDI 提供通过内核映射设备寄存器或内存的接口,以及从驱动程序读/写设备内存的接口。使用这些接口,通过自动管理设备和主机字节存储顺序中的任何差异,以及强制执行设备所强加的任何内存存储顺序要求,可以开发与平台和总线无关的驱动程序。

直接内存访问 (Direct Memory Access, DMA)

Solaris 平台可定义与体系结构无关的高级模型,以支持具备 DMA 功能的设备。Solaris DDI 可防止驱动程序使用特定于平台的详细信息。使用此概念,通用驱动程序可以在多个平台和体系结构上运行。

分层驱动程序接口

DDI/DKI 提供一组称为分层设备接口 (layered device interfaces, LDI) 的接口。利用这些接口,可以从 Solaris 内核中访问设备。该功能使开发者可以编写查看内核设备使用情况的应用程序。例如,prtconf(1M)fuser(1M) 命令都可以使用 LDI,以便使系统管理员可以跟踪设备使用情况的各方面。第 14 章对 LDI 进行了更详细的说明。

驱动程序上下文

驱动程序上下文是指驱动程序的当前运行环境。上下文会限制驱动程序可执行的操作。驱动程序上下文取决于调用的执行代码。驱动程序代码在以下四种上下文中执行:

手册页的第 9F 节介绍了每个函数所允许的上下文。例如,在内核上下文中,驱动程序不得调用 copyin(9F)

返回错误

除列显数据损坏之类的意外错误外,设备驱动程序通常不会列显消息。相反,驱动程序入口点应返回错误代码,以便应用程序可以确定如何处理错误。可以使用 cmn_err(9F) 函数将消息写入随后会在控制台上显示的系统日志中。

由 cmn_err(9F) 解释的格式字符串说明符与 printf(3C) 格式字符串说明符类似,前者还添加了可列显位字段的格式 %b。格式字符串的第一个字符可能具有特殊意义。在对 cmn_err(9F) 的调用中,还会指定消息 level,用于指示要列显的严重性标签。有关更多详细信息,请参见 cmn_err(9F) 手册页。

级别 CE_PANIC 具有使系统崩溃的负面影响。仅当系统处于不稳定状态以至继续运行将导致更多问题时,才应使用此级别。此外,调试时可以使用此级别来获取系统核心转储。不应使用 CE_PANIC 生成设备驱动程序。

动态内存分配

必须将设备驱动程序设计为可以同时处理驱动程序声明要驱动的所有连接设备。驱动程序处理的设备数不应受到限制。必须动态分配所有每设备信息。

void *kmem_alloc(size_t size, int flag);

标准内核内存分配例程为 kmem_alloc(9F)kmem_alloc() 与 C 库例程 malloc(3C) 类似,前者添加了 flag 参数。flag 参数可以是 KM_SLEEPKM_NOSLEEP,用于指示没有所需大小的内存空间时调用者是否要阻塞。如果设置了 KM_NOSLEEP 并且内存不可用,kmem_alloc(9F) 将返回 NULL

kmem_zalloc(9F)kmem_alloc(9F) 类似,但前者还可以清除已分配内存的内容。


注 –

内核内存是有限资源,并且不可分页,它还会与用户应用程序和内核其余部分争用物理内存。分配大量内核内存的驱动程序可导致系统性能降低。


void kmem_free(void *cp, size_t size);

可使用 kmem_free(9F) 将通过 kmem_alloc(9F)kmem_zalloc(9F) 分配的内存返回到系统。kmem_free() 与 C 库例程 free(3C) 类似,但前者添加了 size 参数。驱动程序必须跟踪每个已分配对象的大小,以便在以后调用 kmem_free(9F)

热插拔

本手册没有重点介绍热插拔信息。如果按照本书中介绍的规则和建议编写设备驱动程序,则您的应用程序应该能够处理热插拔。需要特别指出的是,请确保您的驱动程序中的自动配置(请参见第 6 章)和 detach(9E) 都能正常工作。此外,如果要设计使用电源管理的驱动程序,则应遵循第 12 章中介绍的信息。SCSI HBA 驱动程序可能需要向其 dev_ops 结构中添加 cb_ops 结构(请参见第 18 章),以利用热插拔功能。

早期版本的 Solaris OS 要求可热插拔的驱动程序包括 DT_HOTPLUG 属性,但现在已不再需要该属性。不过,驱动程序编写人员可视情况自由加入和使用 DT_HOTPLUG 属性。