本手册第一部分提供了针对 Solaris 平台开发设备驱动程序的一般信息。本部分包括以下各章:
第 1 章介绍了 Solaris 平台上的设备驱动程序和关联的入口点。每种类型的设备驱动程序的入口点都列在表中。
第 2 章对 Solaris 内核进行了概述,并介绍了设备如何在设备树中表示为节点。
第 3 章针对设备驱动程序开发者介绍了 Solaris 多线程内核的各个方面。
第 4 章介绍了一组用于使用设备属性的接口。
第 5 章介绍了设备驱动程序如何记录事件,以及如何使用任务队列在以后执行任务。
第 6 章介绍了驱动程序必须提供的用于自动配置的支持。
第 7 章介绍了驱动程序用来读取或写入设备内存的接口和方法。
第 8 章介绍了用来处理中断的机制。这些机制包括分配、注册、维护和删除中断。
第 9 章介绍了直接内存访问 (direct memory access, DMA) 和 DMA 接口。
第 10 章介绍了用于管理设备和内核内存的接口。
第 11 章介绍了一组设备驱动程序用来管理用户对设备的访问的接口。
第 12 章介绍了用于 Power Management 功能(这是一个用于管理能耗的框架)的接口。
第 13 章介绍了如何将故障管理功能集成到 I/O 设备驱动程序、如何并入防御性编程做法,以及如何使用驱动程序强化测试工具。
第 14 章介绍了 LDI,利用 LDI,内核模块可以访问系统中的其他设备。
本章概述了 Solaris 设备驱动程序。本章提供有关以下主题的信息:
本节介绍 Solaris 平台上的设备驱动程序及其入口点。
设备驱动程序是一种内核模块,负责管理硬件设备的底层 I/O 操作。设备驱动程序是使用标准接口编写的,内核可通过调用该标准接口与设备进行交互。设备驱动程序也可以是仅针对软件的,即模拟仅存在于软件中的设备,如 RAM 磁盘、总线以及伪终端。
设备驱动程序包含与设备进行通信时所需的所有特定于设备的代码。此代码包括一组用于系统其余部分的标准接口。就像系统调用接口可使应用程序不受平台特定信息影响一样,此接口可保护内核不受设备特定信息的影响。应用程序和内核其余部分需要非常少的特定于设备的代码(如果有)对此设备进行寻址。这样,设备驱动程序使得系统的可移植性更强,并更易于维护。
初始化 Solaris 操作系统 (Solaris operating system, Solaris OS) 后,设备会进行自标识并组织为设备树,即设备分层结构。实际上,设备树是内核的硬件模型。单个设备驱动程序表示为树中的一个节点,并且不包含任何子节点。此类型的节点称为叶驱动程序。为其他驱动程序提供服务的驱动程序称为总线结点驱动程序,并显示为包含子节点的节点。在引导过程中,物理设备会映射到树中的驱动程序,以便可以在需要时找到这些驱动程序。有关 Solaris OS 如何使用设备的更多信息,请参见第 2 章。
设备驱动程序按照处理 I/O 的方式可以分为以下三大类别:
块设备驱动程序-适用于可将 I/O 数据作为异步块进行处理的情况。通常,块驱动程序用于管理可物理寻址的存储介质的设备,如磁盘。
字符设备驱动程序-适用于针对连续的字节流执行 I/O 操作的设备。
如果为文件系统设置了两个不同的接口,则驱动程序可同时为块驱动程序和字符驱动程序。请参见作为特殊文件的设备。
使用 STREAMS 模型(参见下文)、程控 I/O、直接内存访问、SCSI 总线、USB 以及其他网络 I/O 的驱动程序都属于字符类别的驱动程序。
STREAMS 设备驱动程序-字符驱动程序的子集,将 streamio(7I) 例程集用于内核中的字符 I/O。
入口点是设备驱动程序内的一个函数,外部实体可调用此函数以访问某种驱动程序功能或运行某个设备。 每个设备驱动程序都提供一组标准函数作为入口点。有关所有驱动程序类型入口点的完整列表,请参见 Intro(9E) 手册页。Solaris 内核使用入口点执行以下常见任务:
装入和卸载驱动程序
自动配置设备-自动配置是将设备驱动程序的代码和静态数据装入内存以在系统内注册此驱动程序的过程。
为驱动程序提供 I/O 服务
根据设备执行的操作类型,不同类型设备的驱动程序具有不同的入口点集。例如,对于内存映射的面向字符的设备,其驱动程序支持 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)。驱动程序可以自由使用这些接口导出驱动程序和设备的统计信息,用户应用程序可使用这些统计信息来查看驱动程序的内部状态。提供了两个入口点来处理内核统计信息:
ks_snapshot(9E),可在特定时间捕获 kstat。
ks_update(9E),可用于根据需要更新 kstat 数据。在设置设备来跟踪内核数据但是提取该数据很耗时的情况下,ks_update() 非常有用。
有关详细信息,请参见 kstat_create(9F) 和 kstat(9S) 手册页。另请参见内核统计信息。
提供电源管理功能的硬件设备的驱动程序可支持可选的 power(9E) 入口点。有关此入口点的详细信息,请参见第 12 章。
下表列出了所有类型驱动程序都可使用的入口点。
表 1–1 用于所有驱动程序类型的入口点
类别/入口点 |
使用情况 |
说明 |
---|---|---|
cb_ops 入口点 | ||
必需 |
获取访问设备的权限。有关其他信息,请参见: |
|
必需 |
放弃访问设备的权限。STREAMS 驱动程序的 close() 版本具有不同于字符驱动程序和块驱动程序的签名。有关其他信息,请参见: |
|
可装入模块入口点 | ||
必需 |
初始化可装入模块。有关其他信息,请参见: 可装入驱动程序接口 |
|
必需 |
准备可装入模块以进行卸载。该入口点是所有驱动程序类型所必需的。有关其他信息,请参见: 可装入驱动程序接口 |
|
必需 |
返回有关可装入模块的信息。有关其他信息,请参见: 可装入驱动程序接口 |
|
自动配置入口点 | ||
必需 |
在初始化过程中向系统添加设备。此外,还用于恢复已暂停的系统。有关其他信息,请参见: attach() 入口点 |
|
必需 |
从系统中分离设备。此外,还用于临时暂停设备。有关其他信息,请参见: detach() 入口点 |
|
必需 |
获取特定于驱动程序的设备信息,如设备编号和相应实例之间的映射。有关其他信息,请参见: |
|
请参见说明 |
确定是否存在非自标识设备。该入口点是无法进行自标识的设备所必需的。有关其他信息,请参见: |
|
内核统计信息入口点 | ||
可选 | ||
可选 | ||
电源管理入口点 | ||
必需 |
设置设备的电源级别。如果不使用此入口点,则设置为 NULL。有关其他信息,请参见: power() 入口点 |
|
其他入口点 | ||
请参见说明 |
报告驱动程序属性信息。除非替换 ddi_prop_op(9F),否则此入口点是必需的。有关其他信息,请参见: |
|
请参见说明 |
系统出现故障时将内存转储到设备。对于出现紧急情况时要用作转储设备的任何设备,该入口点是必需的。有关其他信息,请参见: |
|
identify(9E) |
已过时 |
请勿使用此入口点。请在 dev_ops 结构中将 nulldev(9F) 指定给此入口点。 |
支持文件系统的设备称为块设备。为这些设备编写的驱动程序称为块设备驱动程序。块设备驱动程序接受 buf(9S) 结构形式的文件系统请求,并向磁盘发出 I/O 操作以传送指定的块。文件系统的主接口为 strategy(9E) 例程。有关更多信息,请参见第 16 章。
块设备驱动程序还可以提供字符驱动程序接口,以使实用程序能够绕过文件系统并直接访问设备。这种设备访问通常称为块设备的原始接口。
下表列出了块设备驱动程序可使用的其他入口点。另请参见通用于所有驱动程序的入口点。
表 1–2 用于块驱动程序的其他入口点
入口点 |
使用情况 |
说明 |
---|---|---|
可选 |
执行异步读取。不支持 aread() 入口点的驱动程序应使用 nodev(9F) 错误返回函数。有关其他信息,请参见: |
|
可选 |
执行异步写入。不支持 awrite() 入口点的驱动程序应使用 nodev(9F) 错误返回函数。有关其他信息,请参见: |
|
必需 |
在系统控制台上显示驱动程序消息。有关其他信息,请参见: print() 入口点(块驱动程序) |
|
必需 |
执行块 /O。其他信息: |
字符设备驱动程序通常以字节流的形式执行 I/O 操作。使用字符驱动程序的设备包括磁带机和串行端口。字符设备驱动程序还可以提供块驱动程序中不存在的其他接口,如 I/O 控制 (ioctl) 命令、内存映射以及设备轮询。有关更多信息,请参见第 15 章。
任何设备驱动程序的主要任务都是执行 I/O 操作,许多其他字符设备驱动程序执行称为字节流或字符 I/O 的操作。驱动程序可在设备上来回传送数据,而无需使用特定设备地址。此类型的传送与块设备驱动程序中的相反,后者部分文件系统请求会标识设备上的特定位置。
read(9E) 和 write(9E) 入口点可处理标准字符驱动程序的字节流 I/O。有关更多信息,请参见I/O 请求处理。
下表列出了字符设备驱动程序可使用的其他入口点。有关其他入口点的信息,请参见通用于所有驱动程序的入口点。
表 1–3 用于字符驱动程序的其他入口点
入口点 |
使用情况 |
说明 |
---|---|---|
可选 |
针对非 STREAMS 字符驱动程序轮询事件。有关其他信息,请参见: 对文件描述符执行多路复用 I/O 操作 |
|
可选 |
针对字符驱动程序执行一系列 I/O 命令。ioctl() 例程必须确保根据需要显式使用 copyin(9F)、copyout(9F)、ddi_copyin(9F) 和 ddi_copyout(9F) 在内核地址空间复制用户数据。有关其他信息,请参见: |
|
必需 |
从设备读取数据。有关其他信息,请参见: |
|
可选 |
将设备内存映射到用户空间。有关其他信息,请参见: |
|
必需 |
将数据写入设备。有关其他信息,请参见: |
STREAMS 是一个独立的编程模型,用于编写字符驱动程序。异步接收数据的设备(如终端设备和网络设备)适合实现 STREAMS。STREAMS 设备驱动程序必须提供第 6 章中介绍的装入和自动配置支持。有关如何编写 STREAMS 驱动程序的其他信息,请参见《STREAMS Programming Guide》。
下表列出了 STREAMS 设备驱动程序可使用的其他入口点。有关其他入口点的信息,请参见通用于所有驱动程序的入口点和用于字符设备驱动程序的入口点。
表 1–4 用于 STREAMS 驱动程序的入口点
入口点 |
使用情况 |
说明 |
---|---|---|
请参见说明 |
协调以流的形式将消息从一个队列传递到下一个队列。此入口点是必需的,但是读取数据的驱动程序端除外。有关其他信息,请参见: 《STREAMS Programming Guide 》 |
|
必需 |
处理队列中的消息。有关其他信息,请参见: 《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_access() 入口点 |
|
必需 |
对映射执行设备上下文切换。有关其他信息,请参见: devmap_contextmgt() 入口点 |
|
可选 |
复制设备映射。有关其他信息,请参见: devmap_dup() 入口点 |
|
可选 |
创建设备映射。有关其他信息,请参见: devmap_map() 入口点 |
|
可选 |
取消设备映射。有关其他信息,请参见: devmap_unmap() 入口点 |
有关使用 Generic LAN Driver v3 (GLDv3) 框架的网络设备驱动程序的入口点列表,请参见表 19–1。有关更多信息,请参见第 19 章中的GLDv3 网络设备驱动程序框架和GLDv3 MAC 注册函数。
下表列出了 SCSI HBA 设备驱动程序可使用的其他入口点。有关 SCSI HBA 传输结构的信息,请参见 scsi_hba_tran(9S)。有关其他入口点的信息,请参见通用于所有驱动程序的入口点和用于字符设备驱动程序的入口点。
表 1–6 用于 SCSI HBA 驱动程序的其他入口点
入口点 |
使用情况 |
说明 |
---|---|---|
必需 |
异常中止已传输到 SCSI 主机总线适配器 (Host Bus Adapter, HBA) 驱动程序的指定 SCSI 命令。有关其他信息,请参见: tran_abort() 入口点 |
|
可选 |
重置 SCSI 总线。有关其他信息,请参见: tran_bus_reset() 入口点 |
|
必需 |
释放为 SCSI 包分配的资源。有关其他信息,请参见: tran_destroy_pkt() 入口点 |
|
必需 |
释放已为 SCSI 包分配的 DMA 资源。有关其他信息,请参见: tran_dmafree() 入口点 |
|
必需 |
获取 HBA 驱动程序所提供的特定功能的当前值。有关其他信息,请参见: tran_getcap() 入口点 |
|
必需 |
分配和初始化 SCSI 包的资源。有关其他信息,请参见: 资源分配 |
|
可选 |
停止 SCSI 总线上的所有活动,通常是为了进行动态重新配置。有关其他信息,请参见: 动态重新配置 |
|
必需 |
重置 SCSI 总线或目标设备。有关其他信息,请参见: tran_reset() 入口点 |
|
可选 |
请求通知 SCSI 目标设备进行总线重置。有关其他信息,请参见: tran_reset_notify() 入口点 |
|
必需 |
设置 SCSI HBA 驱动程序所提供的特定功能的值。有关其他信息,请参见: tran_setcap() 入口点 |
|
必需 |
请求传输 SCSI 命令。有关其他信息,请参见: tran_start() 入口点 |
|
必需 |
按 HBA 驱动程序或设备同步数据视图。有关其他信息,请参见: tran_sync_pkt() 入口点 |
|
可选 |
代表目标设备请求释放已分配的 SCSI HBA 资源。有关其他信息,请参见: |
|
可选 |
代表目标设备请求初始化 SCSI HBA 资源。有关其他信息,请参见: |
|
可选 |
探测 SCSI 总线上的指定目标。有关其他信息,请参见: tran_tgt_probe() 入口点 |
|
可选 |
调用 tran_quiesce(9E)(通常是为了进行动态重新配置)之后恢复 SCSI 总线上的 I/O 活动。有关其他信息,请参见: 动态重新配置 |
下表列出了 PC 卡设备驱动程序可使用的其他入口点。有关其他入口点的信息,请参见通用于所有驱动程序的入口点和用于字符设备驱动程序的入口点。
表 1–7 仅适用于 PC 卡驱动程序的入口点
入口点 |
使用情况 |
说明 |
---|---|---|
必需 |
处理 PC 卡驱动程序的事件。驱动程序必须显式调用 csx_RegisterClient(9F) 函数来设置入口点,而不是使用类似 cb_ops 的结构字段。 |
从服务的使用方和提供方角度来看,设备驱动程序都必须与 Solaris OS 兼容。本节所讨论的以下问题是设计设备驱动程序过程中需要考虑的问题:
为了使驱动程序具有可移植性,提供了 Solaris DDI/DKI 接口。利用 DDI/DKI,开发者可采用标准方式编写驱动程序代码,而不必担心硬件或平台差异。本节介绍 DDI/DKI 接口的各个方面。
利用 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 设备访问是指通过主机 CPU 读/写设备寄存器或设备内存的行为。Solaris DDI 提供通过内核映射设备寄存器或内存的接口,以及从驱动程序读/写设备内存的接口。使用这些接口,通过自动管理设备和主机字节存储顺序中的任何差异,以及强制执行设备所强加的任何内存存储顺序要求,可以开发与平台和总线无关的驱动程序。
Solaris 平台可定义与体系结构无关的高级模型,以支持具备 DMA 功能的设备。Solaris DDI 可防止驱动程序使用特定于平台的详细信息。使用此概念,通用驱动程序可以在多个平台和体系结构上运行。
DDI/DKI 提供一组称为分层设备接口 (layered device interfaces, LDI) 的接口。利用这些接口,可以从 Solaris 内核中访问设备。该功能使开发者可以编写查看内核设备使用情况的应用程序。例如,prtconf(1M) 和 fuser(1M) 命令都可以使用 LDI,以便使系统管理员可以跟踪设备使用情况的各方面。第 14 章对 LDI 进行了更详细的说明。
驱动程序上下文是指驱动程序的当前运行环境。上下文会限制驱动程序可执行的操作。驱动程序上下文取决于调用的执行代码。驱动程序代码在以下四种上下文中执行:
用户上下文。用户线程以同步方式调用驱动程序入口点时,此入口点具有用户上下文。即,用户线程会等待系统从调用的入口点返回。例如,通过 read(2) 系统调用来调用驱动程序的 read(9E) 入口点时,此入口点具有用户上下文。在这种情况下,驱动程序可访问用户区域,以在用户线程中复制数据。
内核上下文。通过某部分内核调用驱动程序函数时,此函数具有内核上下文。在块设备驱动程序中,可以通过 pageout 守护进程来调用 strategy(9E) 入口点,以向设备中写入页面。由于页面守护进程与当前用户线程无关,因此在这种情况下 strategy(9E) 具有内核上下文。
中断上下文 中断上下文是一种限制性更强的内核上下文形式。中断上下文是在提供中断服务的情况下调用。驱动程序中断例程在中断上下文中以关联的中断级别运行。回调例程也在中断上下文中运行。有关更多信息,请参见第 8 章。
高级中断上下文高级中断上下文是一种限制性更强的中断上下文形式。如果 ddi_intr_hilevel(9F) 指示某中断为高级中断,则驱动程序中断处理程序将在高级中断上下文中运行。有关更多信息,请参见第 8 章。
手册页的第 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_SLEEP 或 KM_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 属性。
设备驱动程序需要作为操作系统的组成部分透明地工作。理解内核工作方式是了解设备驱动程序的前提条件。本章概述了 Solaris 内核和设备树。有关设备驱动程序工作方式的概述,请参见第 1 章。
本章介绍有关以下主题的信息:
Solaris 内核是用于管理系统资源的程序。内核将应用程序与系统硬件隔离,并为它们提供基本系统服务,如输入/输出 (input/output, I/O) 管理、虚拟内存和调度。内核由需要时动态装入内存的对象模块组成。
Solaris 内核在逻辑上可分为两个部分: 第一部分称为内核,用于管理文件系统、调度和虚拟内存。第二部分称为 I/O 子系统,用于管理物理组件。
内核提供了一组接口,供可通过系统调用访问的应用程序使用。Reference Manual Collection 的第 2 部分对系统调用进行了介绍(请参见 Intro(2))。某些系统调用用于调用设备驱动程序以执行 I/O 操作。设备驱动程序是可装入的内核模块,用于管理数据传输,同时将内核的其余部分与设备硬件隔离。为了与操作系统兼容,设备驱动程序需要能够提供多线程、虚拟内存寻址以及 32 位和 64 位操作之类的功能。
下图解释了内核的工作机制。内核模块用于处理来自应用程序的系统调用。I/O 模块用于与硬件通信。
内核通过以下功能提供对设备驱动程序的访问:
设备至驱动程序映射。内核将维护设备树。树中的每个节点都表示一个虚拟设备或物理设备。内核通过将设备节点名称与系统中安装的驱动程序集进行匹配,从而将每个节点绑定到驱动程序。仅当存在驱动程序绑定时,应用程序才能访问设备。
DDI/DKI 接口。DDI/DKI(Device Driver Interface/Driver-Kernel Interface,设备驱动程序接口/驱动程序内核接口)接口可对驱动程序和内核、设备硬件以及引导/配置软件之间的交互进行标准化。这些接口使驱动程序独立于内核,并且改进了驱动程序在相同体系架构下不同操作系统版本间的可移植性。
LDI。LDI(Layered Driver Interface,分层驱动程序接口)是 DDI/DKI 的扩展。LDI 允许内核模块访问系统中的其他设备。LDI 还允许确定内核当前使用的设备。请参见第 14 章。
Solaris 内核是多线程的。在多处理器计算机上,多个内核线程可以运行内核代码并且可以并发运行。内核线程也可能随时被其他内核线程抢先。
内核的多线程特征对设备驱动程序强加了某些附加限制。有关多线程注意事项的更多信息,请参见第 3 章。必须对设备驱动程序进行编码,使其在许多不同线程请求时按需运行。对于每个线程,驱动程序必须处理重叠的 I/O 请求的争用问题。
Solaris 虚拟内存系统的完整概述超出本书范围,但在讨论设备驱动程序时使用了两个特别重要的虚拟内存术语: 虚拟地址和地址空间。
虚拟地址。虚拟地址是由内存管理单元 (memory management unit, MMU) 映射到物理硬件地址的地址。驱动程序可直接访问的所有地址都属于内核虚拟地址。内核虚拟地址引用内核地址空间。
地址空间。地址空间是一组虚拟地址段。每个地址段都是一个连续范围的虚拟地址。每个用户进程都拥有一个称为用户地址空间的地址空间。内核拥有其自己的地址空间,称为内核地址空间。
设备在文件系统中表示为特殊文件。在 Solaris OS 中, 这些文件驻留在 /devices 目录分层结构中。
特殊文件的类型可以为块,也可以为字符。该类型表示了设备驱动程序的种类。驱动程序可以实现这两种类型。例如,磁盘驱动程序导出字符接口以供 fsck(1) 和 mkfs(1) 实用程序使用,导出块接口以供文件系统使用。
每个特殊文件都与一个设备编号 (dev_t) 关联。设备编号由主设备号和次要设备号组成。主设备号标识与特殊文件关联的设备驱动程序。次要设备号由设备驱动程序创建,供其用来进一步标识特殊文件。通常,次要设备号是一种编码,用于标识驱动程序应访问的设备实例以及应执行的访问类型。例如,次要设备号可以标识用于备份的磁带设备,并可指定完成备份操作后需要将磁带反绕。
在 System V Release 4 (SVR4) 中,设备驱动程序与 UNIX 内核其余部分之间的接口被标准化为 DDI/DKI。DDI/DKI 在 Reference Manual Collection 的第 9 部分中进行介绍。第 9E 节介绍驱动程序入口点,第 9F 节介绍驱动程序可调用的函数,而第 9S 节介绍设备驱动程序使用的内核数据结构。请参见 Intro(9E)、Intro(9F) 和 Intro(9S)。
DDI/DKI 旨在对设备驱动程序与内核其余部分之间的所有接口进 行标准化并进行说明。此外,无论处理器体系结构是 SPARC 还是 x86,DDI/DKI 都允许任何运行 Solaris OS 的计算机的驱动程序的源代码和二进制代码保持兼容。仅使用 DDI/DKI 中包含的内核功能的驱动程序称为与 DDI/DKI 兼容的设备驱动程序。
DDI/DKI 允许您为运行 Solaris OS 的任何计算机编写与平台无关的设备驱动程序。通过这些二进制代码兼容的驱动程序,您可以更方便地将第三方硬件和软件集成到运行 Solaris OS 的任何计算机中。DDI/DKI 与体系结构无关,从而允许同一驱动程序在一组不同的计算机体系结构中工作。
平台无关性是通过在以下方面设计 DDI 实现的:
动态装入和卸载模块
电源管理
中断处理
从内核或用户进程访问设备空间,即寄存器映射和内存映射
使用 DMA 服务从设备访问内核或用户进程空间
管理设备属性
Solaris OS 中的设备表示为互连的设备信息节点树。设备树描述特定计算机的已装入设备的配置。
系统将会生成树结构,其中包含有关引导时连接到计算机的设备的信息。此外,系统正常运行时也可以动态重新配置设备树。设备树从表示平台的根设备节点开始。
根节点下面是设备树的分支。分支由一个或多个总线结点设备和一个终止叶设备组成。
总线结点设备可为设备树中的从属设备提供总线映射和转换服务。PCI - PCI 网桥、PCMCIA 适配器和 SCSI HBA 都是结点设备的示例。编写结点设备驱动程序的讨论仅限于 SCSI HBA 驱动程序的开发(请参见第 18 章)。
叶设备通常为外围设备,如磁盘、磁带、网络适配器、帧缓存器等。叶设备驱动程序可以导出传统的字符驱动程序接口和块驱动程序接口。通过这些接口,用户进程可在存储设备或通信设备中读取和写入数据。
系统通过以下步骤来生成树:
CPU 经过初始化后搜索固件。
主要固件(OpenBoot、基本输入/输出系统 (Basic Input/Output System, BIOS) 或 Bootconf)初始化并创建包含已知或自标识硬件的设备树。
当主要固件在设备中发现兼容固件时,主要固件将初始化该设备并检索设备属性。
该固件将查找并引导操作系统。
内核从树的根节点开始,搜索匹配的设备驱动程序并将该驱动程序绑定到设备。
如果设备是结点,则内核会查找固件尚未检测到的子设备。内核会将所有子设备都添加到树的子树节点下面。
内核从步骤 5 开始重复该过程,直到无需再创建设备节点。
每个驱动程序都会导出设备操作结构 dev_ops(9S),以定义设备驱动程序可以执行的操作。设备操作结构包含通用操作(如 attach(9E)、detach(9E) 和 getinfo(9E))的函数指针。该结构同时还包含了一组与特定总线结点驱动程序操作相关的函数指针,以及一组与特定叶结点设备驱动程序操作相关的函数指针。
树结构将在节点之间创建父子关系。此父子关系是体系结构无关性的关键。当叶驱动程序或总线结点驱动程序本质上需要依赖于体系结构的服务时,该驱动程序会请求其父级提供该服务。采用此方法,不管计算机或处理器的体系结构是什么,驱动程序都可以正常运行。下图显示了典型的设备树。
子树节点可以有一个或多个子节点。叶节点表示各个设备。
libdevinfo 库提供访问设备树内容的编程接口。
prtconf(1M) 命令显示设备树的完整内容。
/devices 分层结构是设备树的表示形式。使用 ls(1) 命令查看该分层结构。
/devices 仅显示将驱动程序配置到系统中的设备。prtconf(1M) 命令显示所有设备节点,而不管系统中是否存在设备驱动程序。
libdevinfo 库提供用于访问所有公共设备配置数据的接口。有关接口列表,请参见 libdevinfo(3LIB) 手册页。
以下摘录的 prtconf(1M) 命令示例显示了系统中的所有设备。
System Configuration: Sun Microsystems sun4u Memory size: 128 Megabytes System Peripherals (Software Nodes): SUNW,Ultra-5_10 packages (driver not attached) terminal-emulator (driver not attached) deblocker (driver not attached) obp-tftp (driver not attached) disk-label (driver not attached) SUNW,builtin-drivers (driver not attached) sun-keyboard (driver not attached) ufs-file-system (driver not attached) chosen (driver not attached) openprom (driver not attached) client-services (driver not attached) options, instance #0 aliases (driver not attached) memory (driver not attached) virtual-memory (driver not attached) pci, instance #0 pci, instance #0 ebus, instance #0 auxio (driver not attached) power, instance #0 SUNW,pll (driver not attached) se, instance #0 su, instance #0 su, instance #1 ecpp (driver not attached) fdthree, instance #0 eeprom (driver not attached) flashprom (driver not attached) SUNW,CS4231 (driver not attached) network, instance #0 SUNW,m64B (driver not attached) ide, instance #0 disk (driver not attached) cdrom (driver not attached) dad, instance #0 sd, instance #15 pci, instance #1 pci, instance #0 pci108e,1000 (driver not attached) SUNW,hme, instance #1 SUNW,isptwo, instance #0 sd (driver not attached) st (driver not attached) sd, instance #0 (driver not attached) sd, instance #1 (driver not attached) sd, instance #2 (driver not attached) ... SUNW,UltraSPARC-IIi (driver not attached) SUNW,ffb, instance #0 pseudo, instance #0
/devices 分层结构提供了表示设备树的名称空间。下面是 /devices 名称空间的缩写列表。样例输出对应于先前显示的示例设备树和 prtconf(1M) 输出。
/devices /devices/pseudo /devices/pci@1f,0:devctl /devices/SUNW,ffb@1e,0:ffb0 /devices/pci@1f,0 /devices/pci@1f,0/pci@1,1 /devices/pci@1f,0/pci@1,1/SUNW,m64B@2:m640 /devices/pci@1f,0/pci@1,1/ide@3:devctl /devices/pci@1f,0/pci@1,1/ide@3:scsi /devices/pci@1f,0/pci@1,1/ebus@1 /devices/pci@1f,0/pci@1,1/ebus@1/power@14,724000:power_button /devices/pci@1f,0/pci@1,1/ebus@1/se@14,400000:a /devices/pci@1f,0/pci@1,1/ebus@1/se@14,400000:b /devices/pci@1f,0/pci@1,1/ebus@1/se@14,400000:0,hdlc /devices/pci@1f,0/pci@1,1/ebus@1/se@14,400000:1,hdlc /devices/pci@1f,0/pci@1,1/ebus@1/se@14,400000:a,cu /devices/pci@1f,0/pci@1,1/ebus@1/se@14,400000:b,cu /devices/pci@1f,0/pci@1,1/ebus@1/ecpp@14,3043bc:ecpp0 /devices/pci@1f,0/pci@1,1/ebus@1/fdthree@14,3023f0:a /devices/pci@1f,0/pci@1,1/ebus@1/fdthree@14,3023f0:a,raw /devices/pci@1f,0/pci@1,1/ebus@1/SUNW,CS4231@14,200000:sound,audio /devices/pci@1f,0/pci@1,1/ebus@1/SUNW,CS4231@14,200000:sound,audioctl /devices/pci@1f,0/pci@1,1/ide@3 /devices/pci@1f,0/pci@1,1/ide@3/sd@2,0:a /devices/pci@1f,0/pci@1,1/ide@3/sd@2,0:a,raw /devices/pci@1f,0/pci@1,1/ide@3/dad@0,0:a /devices/pci@1f,0/pci@1,1/ide@3/dad@0,0:a,raw /devices/pci@1f,0/pci@1 /devices/pci@1f,0/pci@1/pci@2 /devices/pci@1f,0/pci@1/pci@2/SUNW,isptwo@4:devctl /devices/pci@1f,0/pci@1/pci@2/SUNW,isptwo@4:scsi
将驱动程序绑定到设备指的是系统选择用于管理特定设备的驱动程序的过程。绑定名称是将驱动程序与设备信息树连接在一起的唯一设备结点名称。对于设备树中的每个设备,系统都会尝试从已安装的驱动程序列表中选择一个驱动程序。
每个设备节点都有关联的 name 属性。可以在系统引导期间通过外部代理(如 PROM )或通过 driver.conf 配置文件指定此属性。无论在哪种情况下,name 属性都表示指定给设备树中的设备的节点名称。节点名称是在 /devices 中可见并列在 prtconf(1M) 输出中的名称。
设备节点也可以有关联的 compatible 属性。compatible 属性包含设备的一个或多个可能的驱动程序名称或驱动程序别名的有序列表。
系统使用 compatible 属性和 name 属性来为设备选择驱动程序。如果 compatible 属性存在,则系统会首先尝试将 compatible 属性的内容与系统中的驱动程序匹配。系统将从 compatible 属性列表的第一个驱动程序名称开始,尝试将该驱动程序名称与系统中的已知驱动程序匹配。系统将会处理该列表中的每一项,直到找到匹配项或者到达列表结尾。
如果 name 属性或 compatible 属性的内容与系统中的某个驱动程序匹配,则将该驱动程序绑定到设备节点。如果未找到匹配项,则不会将任何驱动程序绑定到设备节点。
某些设备将通用设备名称指定为 name 属性的值。通用设备名称用于描述设备的功能,不实际标识设备的特定驱动程序。例如,SCSI 主机总线适配器可能具有通用设备名称 scsi。以太网设备可能具有通用设备名称 ethernet。
通过 compatible 属性,系统可以确定具有通用设备名称的设备的备用驱动程序名称,例如,glm 对应于 scsi HBA 设备驱动程序,hme 对应于 ethernet 设备驱动程序。
具有通用设备名称的设备需要提供 compatible 属性。
有关通用设备名称的完整说明,请参见 IEEE 1275 Open Firmware Boot Standard。
下图显示了具有特定设备名称的设备节点。驱动程序绑定名称 SUNW,ffb 与设备节点名称同名。
下图显示了具有通用设备名称 display 的设备节点。驱动程序绑定名称 SUNW,ffb 是 compatible 属性驱动程序列表中与系统驱动程序列表中的驱动程序匹配的第一个名称。在这种情况下,display 是帧缓存器的通用设备名称。
本章介绍 Solaris 多线程内核的锁定原语和线程同步机制。设计设备驱动程序时应当充分利用多线程的特性。本章介绍有关以下主题的信息:
在传统 UNIX 系统中,每一部分内核代码都采用两种方法终止:通过显式调用 sleep(1) 放弃对处理器的控制权或通过硬件中断。Solaris OS 的运行方式与此不同。系统可随时抢占内核线程以运行其他线程。由于所有内核线程共享内核地址空间,并且通常需要读取和修改相同的数据,因此内核提供了大量的锁定原语以防止线程损坏共享数据。这些机制包括互斥锁(又称为 mutex)、读取器/写入器锁以及信号量。
数据的存储类用于指示驱动程序是否需要采取显式步骤控制对数据的访问。共有三个数据存储类:
自动(栈)数据。每个线程有一个专用栈,因此驱动程序永远无需锁定自动变量。
全局静态数据。全局静态数据可由驱动程序中任意数量的线程共享。驱动程序有时可能需要锁定此类型的数据。
内核堆数据。驱动程序中任意数量的线程均可共享内核堆数据,如 kmem_alloc(9F) 分配的数据。驱动程序需要随时保护共享数据。
互斥锁 (mutex) 通常与一组数据关联,并控制对这些数据的访问。互斥锁提供了一种一次仅允许一个线程访问这些数据的方法。互斥锁函数包括:
释放任何关联的存储空间。
获取互斥锁。
释放互斥锁。
初始化互斥锁。
测试以确定当前线程是否持有互斥锁。仅限于在 ASSERT(9F) 中使用。
获取互斥锁(如果可用),但不阻塞。
设备驱动程序通常会为每个驱动程序数据结构分配一个互斥锁。互斥锁通常是该结构中类型为 kmutex_t 的字段。调用 mutex_init(9F) 以初始化要使用的互斥锁。通常在执行 attach(9E) 时为每个设备互斥锁进行此调用,在执行 _init(9E) 时为全局驱动程序互斥锁进行此调用。
例如,
struct xxstate *xsp; /* ... */ mutex_init(&xsp->mu, NULL, MUTEX_DRIVER, NULL); /* ... */
有关互斥锁初始化的较完整示例,请参见第 6 章。
驱动程序在卸载之前必须使用 mutex_destroy(9F) 销毁互斥锁。通常在执行 detach(9E) 时为每个设备互斥锁进行销毁操作,在执行 _fini(9E) 时为全局驱动程序互斥锁进行销毁操作。
驱动程序如果需要读写共享数据结构,必须执行以下操作:
获取互斥锁
访问数据
释放互斥锁
互斥锁的作用域(即互斥锁保护的数据)完全由程序员决定。仅当访问数据结构的每个代码路径都保护数据结构并且同时持有互斥锁时,互斥锁才会保护该数据结构。
读取器/写入器锁可控制对数据集的访问。读取器/写入器锁之所以这样命名,是由于许多线程可同时持有读锁,但仅有一个线程可持有写锁。
大多数设备驱动程序不使用读取器/写入器锁。这些锁的速度比互斥锁要慢。这些锁仅当保护通常进行读取但不经常写入的数据时才会提高性能。在此情况下,对互斥锁的争用可能会成为一个瓶颈,因此使用读取器/写入器锁效率可能更高。下表概述了读取器/写入器函数。有关详细信息,请参见 rwlock(9F) 手册页。读取器/写入器锁函数包括:
销毁读取器/写入器锁
将读取器/写入器锁的持有者从写入器降级为读取器
获取读取器/写入器锁
释放读取器/写入器锁
初始化读取器/写入器锁
确定是否持有用于读/写操作的读取器/写入器锁
尝试在无需等待的情况下获取读取器/写入器锁
尝试将读取器/写入器锁的持有者从读取器升级为写入器
计数信号量可用作管理设备驱动程序中线程的替代原语。有关更多信息,请参见 semaphore(9F) 手册页。信号函数包括:
销毁信号。
初始化信号。
减小信号,可能会阻塞。
减小信号,但不会阻塞(如果信号处于待处理状态)。请参见线程无法接收信号。
尝试减小信号,但不阻塞。
增加信号,可能会解除阻塞等待者。
除了保护共享数据外,驱动程序通常需要在多个线程之间同步执行。
条件变量是线程同步的标准形式。这些变量专门用于互斥锁。关联互斥锁可以确保条件的检查是原子操作,并且线程可以基于关联的条件变量阻塞,同时不会忽略对条件的更改或条件已更改的信号。
condvar(9F) 函数包括:
向基于条件变量等待的所有线程发出信号。
销毁条件变量。
初始化条件变量。
向基于条件变量等待的一个线程发出信号。
等待条件、超时或信号。请参见线程无法接收信号。
等待条件或超时。
等待条件。
等待条件或在收到信号时返回零。请参见线程无法接收信号。
针对每个条件声明 kcondvar_t 类型的条件变量。通常,条件变量是驱动程序定义的数据结构中的一个变量。使用 cv_init(9F) 可初始化每个条件变量。与互斥锁类似,条件变量通常在执行 attach(9E) 时初始化。以下是一个初始化条件变量的典型示例:
cv_init(&xsp->cv, NULL, CV_DRIVER, NULL);
有关条件变量初始化的较完整示例,请参见第 6 章。
要使用条件变量,请在等待条件的代码路径中执行以下步骤:
获取用于保护条件的互斥锁。
测试条件。
如果测试结果表明不允许线程继续执行,请使用 cv_wait(9F) 根据条件阻塞当前线程。cv_wait(9F) 函数将在阻塞线程之前释放互斥锁,并在返回之前重新获取互斥锁。从 cv_wait(9F) 返回时,重复该测试。
测试表明允许线程继续执行后,请将条件设置为其新值。例如,将设备标志设置为繁忙。
释放互斥锁。
请在代码路径中执行以下步骤以发出条件信号:
获取用于保护条件的互斥锁。
设置条件。
使用 cv_broadcast(9F) 向阻塞的线程发出信号。
释放互斥锁。
以下示例使用繁忙标志以及互斥锁和条件变量来强制 read(9E) 例程进行等待,直到设备不再繁忙时为止,然后开始传送。
static int xxread(dev_t dev, struct uio *uiop, cred_t *credp) { struct xxstate *xsp; /* ... */ mutex_enter(&xsp->mu); while (xsp->busy) cv_wait(&xsp->cv, &xsp->mu); xsp->busy = 1; mutex_exit(&xsp->mu); /* perform the data access */ } static uint_t xxintr(caddr_t arg) { struct xxstate *xsp = (struct xxstate *)arg; mutex_enter(&xsp->mu); xsp->busy = 0; cv_broadcast(&xsp->cv); mutex_exit(&xsp->mu); }
如果使用 cv_wait(9F) 根据某个条件将线程阻塞,但该条件不发生,则该线程将永远等待。要避免这种情况,请使用 cv_timedwait(9F),它取决于执行唤醒的其他线程。cv_timedwait() 采取绝对等待时间作为参数。如果时间已到但未发生事件,则 cv_timedwait() 将返回 -1。如果满足条件,则 cv_timedwait() 将返回一个正值。
cv_timedwait(9F) 要求自上次重新引导系统以来的绝对等待时间(以时钟周期表示)。通过使用 ddi_get_lbolt(9F) 检索当前值可确定该等待时间。驱动程序通常具有的是最大等待秒数或微秒数,因此需要使用 drv_usectohz(9F) 将该值转换为时钟周期,然后与 ddi_get_lbolt(9F) 的值相加。
以下示例说明如何使用 cv_timedwait(9F) 最多等待五秒钟便访问设备,然后向调用方返回 EIO。
clock_t cur_ticks, to; mutex_enter(&xsp->mu); while (xsp->busy) { cur_ticks = ddi_get_lbolt(); to = cur_ticks + drv_usectohz(5000000); /* 5 seconds from now */ if (cv_timedwait(&xsp->cv, &xsp->mu, to) == -1) { /* * The timeout time 'to' was reached without the * condition being signaled. */ /* tidy up and exit */ mutex_exit(&xsp->mu); return (EIO); } } xsp->busy = 1; mutex_exit(&xsp->mu);
虽然设备驱动程序写入器通常首选使用 cv_timedwait(9F) 而不是 cv_wait(9F),但是有时选用 cv_wait(9F) 会更好。例如,如果驱动程序基于以下条件等待,则使用 cv_wait(9F) 更合适:
内部驱动程序状态发生变化,在此情况下状态变化可能要求执行一些命令或设置要经过的时间
驱动程序的某些部分必须单线程执行
已在管理可能超时的情况,如 "A" 取决于 "B",同时 "B" 使用 cv_timedwait(9F)
驱动程序可能正在等待不会产生或长时间不会发生的条件。在此类情况下,用户可发送信号中止该线程。根据驱动程序设计,信号可能无法将驱动程序唤醒。
cv_wait_sig(9F) 允许使用信号解除阻塞线程。借助此功能,用户可以通过使用kill(1) 向线程发送信号或键入中断字符,从而免于可能的长时间等待。如果 cv_wait_sig(9F) 由于收到信号而返回,则会返回零;如果条件发生,则返回非零值。但是,对于可能未收到信号的情况,请参见线程无法接收信号。
以下示例说明如何使用 cv_wait_sig(9F) 以允许使用信号解除阻塞线程。
mutex_enter(&xsp->mu); while (xsp->busy) { if (cv_wait_sig(&xsp->cv, &xsp->mu) == 0) { /* Signaled while waiting for the condition */ /* tidy up and exit */ mutex_exit(&xsp->mu); return (EINTR); } } xsp->busy = 1; mutex_exit(&xsp->mu);
cv_timedwait_sig(9F) 与 cv_timedwait(9F) 和 cv_wait_sig(9F) 相似,不同之处在于,达到超时后,如果没有发出条件信号,则 cv_timedwait_sig() 将返回 -1,如果向线程发送了信号(例如 kill(2)),则将返回 0。
对于 cv_timedwait(9F) 和 cv_timedwait_sig(9F),系统将使用上次重新引导系统以来的绝对时钟周期度量时间。
在设计大多数设备驱动程序时,都应该确保锁定方案简单易懂。使用额外的锁允许更多并发,但会增加开销。使用的锁越少,占用的时间越短,但允许的并发会更少。通常,对每个数据结构使用一个互斥锁,对驱动程序必须等待的每个事件或条件使用一个条件变量,对驱动程序的每个主要全局数据集使用一个互斥锁。请避免长时间持有互斥锁。选择锁定方案时,请遵循以下指导原则:
优先使用程序入口点的多线程语义。
确保所有程序入口点可重入。可以通过将静态变量更改为自动变量来减少共享数据量。
如果驱动程序获取了多个互斥锁,请在所有代码路径中按相同顺序获取和释放这些互斥锁。
在相同的功能空间中持有锁和释放锁。
调用可阻塞的 DDI 接口时请避免持有驱动程序互斥锁,例如使用 KM_SLEEP 调用 kmem_alloc(9F)。
要查看锁的用法,请使用 lockstat(1M)。lockstat(1M) 可监视所有内核锁定事件、收集有关事件的频率和计时数据,并显示这些数据。
有关多线程操作的更多详细信息,请参见《多线程编程指南》。
同一线程不可重复获取互斥锁。如果已持有互斥锁,则再次尝试声明此互斥锁会导致产生以下故障消息:
panic: recursive mutex_enter. mutex %x caller %x
释放当前线程未持有的互斥锁会产生以下故障消息:
panic: mutex_adaptive_exit: mutex not held by thread
以下故障消息仅在单处理器上出现:
panic: lock_set: lock held and only one CPU
lock_set 故障消息指明线程持有自旋互斥锁 (spin mutex),并且该锁将会永久旋转,因为没有其他 CPU 可以释放此互斥锁。如果驱动程序忘记释放某个代码路径上的互斥锁,或在持有互斥锁时阻塞,则会发生此情况。
具有高级中断的设备调用的例程阻塞(如 cv_wait(9F))时通常会导致出现 lock_set 故障消息。另一个常见原因是高级处理程序通过调用 mutex_enter(9F) 获取自适应互斥锁。
线程收到信号时,可以唤醒 sema_p_sig()、cv_wait_sig() 和 cv_timedwait_sig() 函数。由于某些线程无法接收信号,因此可能会出现问题。例如,如果由于应用程序调用 close(2) 而导致调用 close(9E),则可以收到信号。但是,如果是从 exit(2) 处理(关闭所有打开的文件描述符)中调用 close(9E),则线程无法收到信号。如果线程无法收到信号,则 sema_p_sig() 的行为与 sema_p() 相同,cv_wait_sig() 的行为与 cv_wait() 相同,cv_timedwait_sig() 的行为与 cv_timedwait() 相同。
对于可能永远不会发生的事件,请注意避免永久休眠。永远不会发生的事件会创建不可中止 (defunct) 的线程并使设备不可用,除非重新引导系统。失效进程无法接收信号。
要检测当前线程是否可接收信号,请使用 ddi_can_receive_sig(9F) 函数。如果 ddi_can_receive_sig() 函数返回 B_TRUE,则以上函数可在收到信号时唤醒。如果 ddi_can_receive_sig() 函数返回 B_FALSE,则以上函数无法在收到信号时唤醒。如果 ddi_can_receive_sig() 函数返回 B_FALSE,则驱动程序会使用替代方法(如 timeout(9F) 函数)重新唤醒。
出现此问题的一个重要情况是使用串行端口。如果远程系统声明了流量控制,并且 close(9E) 函数在尝试清空输出数据时阻塞,则端口会堵塞,直到解决流量控制情况或重新引导系统为止。此类驱动程序应检测到此情况并设置计时器,以便在流量控制情况持续过长时间时中止清空操作。
此问题还会影响 qwait_sig(9F) 函数。此函数将在《STREAMS Programming Guide》中的第 7 章 “STREAMS Framework – Kernel Level”中介绍。
属性是用户定义的名称-值对结构,该结构使用 DDI/DKI 接口进行管理。本章介绍有关以下主题的信息:
设备特性 (attribute) 信息可由称为属性 (property) 的名称-值对表示法表示。
例如,设备寄存器和板载内存可由 reg 属性表示。reg 属性是描述设备硬件寄存器的软件抽象术语。reg 属性的值对设备寄存器地址位置和大小进行编码。驱动程序使用 reg 属性访问设备寄存器。
另外一个示例是 interrupt 属性。interrupt 属性表示设备中断。interrupt 属性的值对设备中断 PIN 进行编码。
可以为属性指定五种类型的值:
字节数组-任意长度的一系列字节
整数属性-整数值
整数数组属性-整数数组
字符串属性-以 null 结尾的字符串
字符串数组属性-以 null 结尾的字符串列表
没有值的属性被视为布尔属性。对于布尔属性,如果存在,则值为 True;如果不存在,则值为 False。
严格地说,DDI/DKI 软件属性名称没有限制。但建议使用某些限制。IEEE 1275-1994 Standard for Boot Firmware 引导固件标准按如下方法定义属性:
属性是由 1 到 31 个可列显字符组成的人工可读文本字符串。属性名称不能包含大写字符或字符 "/"、"\"、":"、 "["、"]" 和 "@"。以字符 "+" 开头的属性名称保留供 IEEE 1275-1994 的将来修订使用。
根据约定,属性名称中不能使用下划线。可使用连字符 (-)。根据约定,以问号字符 (?) 结尾的属性名称包含字符串值(通常为 TRUE 或 FALSE),例如 auto-boot?。
IEEE 1275 工作组的出版物中列出了预定义的属性名称。有关如何获取这些出版物的信息,请访问 http://playground.sun.com/1275/。有关在驱动程序配置文件中添加属性的讨论,请参见 driver.conf(4) 手册页。pm(9P) 和 pm-components(9P) 手册页说明了如何在电源管理中使用属性。有关应该如何在设备驱动程序中记录属性的信息,请阅读 sd(7D) 手册页。
要为驱动程序创建属性,或者更新现有属性,请将 DDI 驱动程序更新接口(如 ddi_prop_update_int(9F) 或 ddi_prop_update_string(9F))中的一个接口与相应的属性类型一起使用。有关可用属性接口的列表,请参见表 4–1。这些接口通常从驱动程序的 attach(9E) 入口点调用。在以下示例中,ddi_prop_update_string() 创建一个名为 pm-hardware-state 且值为 needs-suspend-resume 的字符串属性。
/* 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");
在大多数情况下,使用 ddi_prop_update() 例程即可满足更新属性的要求。但是,有时更新经常更改的属性值的系统开销可能会导致性能问题。有关使用属性值的本地实例以避免使用 ddi_prop_update() 的说明,请参见prop_op() 入口点。
驱动程序可以请求其父级的属性,而后者又可以请求其父级。驱动程序可以控制是否将请求传递到其父级以上。
例如,以下示例中的 esp 驱动程序为每个目标维护一个名为 targetx-sync-speed 的整数属性。targetx-sync-speed 中的 x 表示目标编号。prtconf(1M) 命令以详细模式显示驱动程序属性。以下示例列出了 esp 驱动程序的部分内容。
% prtconf -v ... esp, instance #0 Driver software properties: name <target2-sync-speed> length <4> value <0x00000fa0>. ... |
系列 |
属性接口 |
说明 |
---|---|---|
ddi_prop_lookup |
查找属性,如果该属性存在,则成功返回。如果该属性不存在,则失败。 |
|
|
查找并返回整数属性 |
|
|
查找并返回 64 位整数属性 |
|
|
查找并返回整数数组属性 |
|
|
查找并返回 64 位整数数组属性 |
|
|
查找并返回字符串属性 |
|
|
查找并返回字符串数组属性 |
|
|
查找并返回字节数组属性 |
|
ddi_prop_update |
更新或创建整数属性 |
|
|
更新或创建单个 64 位整数属性 |
|
|
更新或创建整数数组属性 |
|
|
更新或创建字符串属性 |
|
|
更新或创建字符串数组属性 |
|
|
更新或创建 64 位整数数组属性 |
|
|
更新或创建字节数组属性 |
|
ddi_prop_remove |
删除单个属性 |
|
|
删除与设备关联的所有属性 |
尽可能使用 64 位版本的 int
属性接口(如 ddi_prop_update_int64(9F)),而不要使用 32 位版本(如 ddi_prop_update_int(9F))。
向系统报告设备属性或驱动程序属性通常需要使用 prop_op(9E) 入口点。如果驱动程序无需创建或管理其自己的属性,则 ddi_prop_op(9F) 函数可用于此入口点。
如果在驱动程序的 cb_ops(9S) 结构中定义了 ddi_prop_op(),则 ddi_prop_op(9F) 可用作设备驱动程序的 prop_op(9E) 入口点。ddi_prop_op() 使叶设备可在设备的属性列表中搜索并获取属性值。
如果驱动程序需要维护其值经常更改的属性,则应在 cb_ops() 结构中定义特定于驱动程序的 prop_op 例程,而不是调用 ddi_prop_op()。此方法可避免由于重复使用 ddi_prop_update() 而造成的效率低下。然后,驱动程序应在其软状态结构或驱动程序变量中维护属性值的副本。
prop_op(9E) 入口点向系统报告特定驱动程序属性的值和设备属性的值。在许多情况下,ddi_prop_op(9F) 例程在 cb_ops(9S) 结构中可用作驱动程序的 prop_op() 入口点。ddi_prop_op() 会执行所有必需的处理过程。对于处理设备属性请求时不需要进行特殊处理的驱动程序,ddi_prop_op() 即可满足要求。
但是,有时驱动程序必须提供 prop_op() 入口点。例如,如果驱动程序维护其值经常更改的属性,则针对每次更改使用 ddi_prop_update(9F) 更新属性便不能满足要求。相反,驱动程序应在实例的软状态下维护属性的阴影副本。然后,驱动程序可在值发生变化时更新阴影副本,而无需使用任何 ddi_prop_update() 例程。prop_op() 入口点必须拦截此属性的请求,并使用 ddi_prop_update() 例程之一更新属性的值,然后将请求传递到 ddi_prop_op() 以处理属性请求。
在以下示例中,prop_op() 拦截 temperature 属性的请求。属性发生变化时,驱动程序将更新状态结构中的变量。但是,仅当发出请求时才会更新该属性。然后,驱动程序使用 ddi_prop_op() 处理该属性请求。如果属性请求不特定于某个设备,则驱动程序不会拦截该请求。dev 参数的值等于 DDI_DEV_T_ANY(通配符设备编号)时即是这种情况。
static int xx_prop_op(dev_t dev, dev_info_t *dip, ddi_prop_op_t prop_op, int flags, char *name, caddr_t valuep, int *lengthp) { minor_t instance; struct xxstate *xsp; if (dev != DDI_DEV_T_ANY) { return (ddi_prop_op(dev, dip, prop_op, flags, name, valuep, lengthp)); } instance = getminor(dev); xsp = ddi_get_soft_state(statep, instance); if (xsp == NULL) return (DDI_PROP_NOTFOUND); if (strcmp(name, "temperature") == 0) { ddi_prop_update_int(dev, dip, name, temperature); } /* other cases */ }
驱动程序使用事件来响应状态更改。本章提供以下有关事件的信息:
驱动程序使用任务队列来管理任务之间的资源相关性。本章提供有关任务队列的以下信息:
系统经常需要对用户操作或系统请求之类的条件更改做出响应。例如,设备可能会在某个组件开始过热时发出警告,或者可能在将 DVD 插入驱动器后启动影片播放机。设备驱动程序可以使用称为事件的特殊消息来通知系统发生了状态更改。
事件是指设备驱动程序向相关实体发送的消息,用以指示发生了状态更改。在 Solaris OS 中,事件以用户定义的名称-值对结构的形式实现,这些结构使用 nvlist* 函数进行管理。(请参见 nvlist_alloc(9F) 手册页。)事件由供应商、类以及子类组成。例如,可以定义一个类用于监视环境条件。环境类可以具有子类,用来指示温度、风扇状态以及电源方面的变化。
发生状态更改时,设备将通知驱动程序。驱动程序随后将使用 ddi_log_sysevent(9F) 函数在称为 sysevent 的队列中记录此事件。sysevent 队列会将事件传递到用户级,以便通过 syseventd 守护进程或 syseventconfd 守护进程进行处理。这些守护进程会将通知发送到订阅了指定事件通知的所有应用程序。
用户级应用程序的设计者可以使用以下两种方法处理事件:
应用程序可以使用 libsysevent(3LIB) 中的例程向 syseventd 守护进程订阅发生指定事件时的通知。
开发者可以编写单独的用户级应用程序来响应事件。此类型应用程序需要通过 syseventadm(1M) 进行注册。syseventconfd 遇到指定事件时,应用程序会根据实际情况运行并处理此事件。
下图对此流程进行了说明。
设备驱动程序使用 ddi_log_sysevent(9F) 接口生成和记录系统事件。
ddi_log_sysevent() 使用以下语法:
int ddi_log_sysevent(dev_info_t *dip, char *vendor, char *class, char *subclass, nvlist_t *attr-list, sysevent_id_t *eidp, int sleep-flag);
其中:
指向相应驱动程序处理的 dev_info 节点的指针。
指向定义驱动程序供应商的字符串的指针。第三方驱动程序应使用其公司的股票代号或类似的持久标识符。Sun 提供的驱动程序会使用 DDI_VENDOR_SUNW。
指向定义事件类的字符串的指针。class 是特定于驱动程序的值。表示影响设备的一组环境条件的字符串可能即是一个类的示例。事件使用方必须能够理解该值。
表示 class 参数子集的特定于驱动程序的字符串。例如,在表示环境条件的类中,事件子类可能是指设备的温度。事件使用方必须能够理解该值。
指向列出与事件关联的名称-值特性的 nvlist_t 结构的指针。名称-值特性是驱动程序定义的,可以是指设备的特定特性或条件。
例如,可同时读取 CD-ROM 和 DVD 的设备。此设备可能具有一个名称为 disc_type 并且值等于 cd_rom 或 dvd 的特性。
与 class 和 subclass 一样,事件使用方必须能够解释名称-值对。
有关名称-值对以及 nvlist_t 结构的更多信息,请参见定义事件特性以及 nvlist_alloc(9F) 手册页。
如果事件没有任何特性,则此参数应设置为 NULL。
sysevent_id_t 结构的地址。sysevent_id_t 结构用于提供事件的唯一标识。ddi_log_sysevent (9F) 将向此结构返回系统提供的事件序列号和时间戳。有关 sysevent_id_t 结构的更多信息,请参见 ddi_log_sysevent(9F) 手册页。
指示调用者如何处理不可用资源可能性的标志。如果 sleep-flag 设置为 DDI_SLEEP,则驱动程序会阻塞,直到资源可用为止。如果设置为 DDI_NOSLEEP,则分配不会休眠且不能保证成功。如果返回了 DDI_ENOMEM,则驱动程序以后需要重试该操作。
即使设置为 DDI_SLEEP,此界面也可能返回错误(如系统繁忙),syseventd 守护进程不响应或不尝试在中断上下文中记录事件。
设备驱动程序可执行以下任务来记录事件:
使用 nvlist_alloc(9F) 为特性列表分配内存
向特性列表添加名称-值对
使用 ddi_log_sysevent(9F) 函数在 sysevent 队列中记录事件
不再需要特性列表时调用 nvlist_free(9F)
以下示例说明如何使用 ddi_log_sysevent()。
char *vendor_name = "DDI_VENDOR_JGJG" char *my_class = "JGJG_event"; char *my_subclass = "JGJG_alert"; nvlist_t *nvl; /* ... */ nvlist_alloc(&nvl, nvflag, kmflag); /* ... */ (void) nvlist_add_byte_array(nvl, propname, (uchar_t *)propval, proplen + 1); /* ... */ if (ddi_log_sysevent(dip, vendor_name, my_class, my_subclass, nvl, NULL, DDI_SLEEP)!= DDI_SUCCESS) cmn_err(CE_WARN, "error logging system event"); nvlist_free(nvl);
事件特性定义为名称-值对列表。Solaris DDI 提供了用于在名称-值对中存储信息的例程和结构。名称-值对保留在 nvlist_t 结构中,此结构对于驱动程序是不透明的。名称-值对的值可以是布尔值、int、字节、字符串、nvlist 或这些数据类型的数组。int 可以定义为 16 位、32 位或 64 位,可以带符号,也可不带符号。
下面是创建名称-值对列表的步骤。
使用 nvlist_alloc(9F) 创建 nvlist_t 结构。
nvlist_alloc() 接口会采用以下三个参数:
nvlp-指向 nvlist_t 结构指针的指针
nvflag-指示名称-值对的名称唯一性的标志。如果此标志设置为 NV_UNIQUE_NAME_TYPE,则会从列表中删除与新对的名称和类型相匹配的任何现有对。如果标志设置为 NV_UNIQUE_NAME,则会删除任何同名的现有对,而不考虑对的类型。只要对的类型不同,通过指定 NV_UNIQUE_NAME_TYPE,列表即可包含两个或多个同名的对,但如果指定 NV_UNIQUE_NAME,则列表中只能有一个对名称实例。如果未设置标志,则不会执行任何唯一性检查,将由列表的使用方负责处理同名的对。
kmflag-指示内核内存分配策略的标志。如果此参数设置为 KM_SLEEP,则驱动程序会阻塞,直到请求的内存可进行分配为止。KM_SLEEP 分配可能会休眠,但是保证会成功。KM_NOSLEEP 分配保证不会休眠,但是可能会在当前无可用内存时返回 NULL。
使用名称-值对填充 nvlist。例如,要添加字符串,请使用 nvlist_add_string(9F)。要添加 32 位整数数组,请使用 nvlist_add_int32_array(9F)。nvlist_add_boolean(9F) 手册页包含用于添加对的可用接口的完整列表。
要取消分配列表,请使用 nvlist_free(9F)。
以下代码样例说明如何创建名称-值对列表。
nvlist_t* create_nvlist() { int err; char *str = "child"; int32_t ints[] = {0, 1, 2}; nvlist_t *nvl; err = nvlist_alloc(&nvl, NV_UNIQUE_NAME, 0); /* allocate list */ if (err) return (NULL); if ((nvlist_add_string(nvl, "name", str) != 0) || (nvlist_add_int32_array(nvl, "prop", ints, 3) != 0)) { nvlist_free(nvl); return (NULL); } return (nvl); }
驱动程序可通过相应类型的查找函数(如 nvlist_lookup_int32_array(9F))来检索 nvlist 中的元素,此类查找函数将要搜索的名称-值对的名称作为参数。
仅当在调用 nvlist_alloc(9F) 时指定了 NV_UNIQUE_NAME 或 NV_UNIQUE_NAME_TYPE 的情况下,这些接口才会正常工作。否则,将返回 ENOTSUP,因为此列表不能包含多个同名的对。
可以将名称-值列表中的各对放在连续内存中。此方法有助于将列表传递给已订阅了通知的实体。第一步是使用 nvlist_size(9F) 获取列表所需的内存块的大小。第二步是使用 nvlist_pack(9F) 将列表压缩到缓冲区中。收到缓冲区内容的使用方可使用 nvlist_unpack(9F) 解压缩缓冲区。
用户级开发者和内核级开发者均可使用用于处理名称-值对的函数。可以在《man pages section 3: Library Interfaces and Headers》和《man pages section 9: DDI and DKI Kernel Functions》中找到这些函数的相同手册页。有关针对名称-值对执行操作的函数的列表,请参见下表。
表 5–1 使用名称-值对的函数
手册页 |
用途/函数 |
---|---|
向列表中添加名称-值对。函数包括: nvlist_add_boolean()、nvlist_add_boolean_value ()、nvlist_add_byte()、nvlist_add_int8()、 nvlist_add_uint8()、nvlist_add_int16()、nvlist_add_uint16 ()、nvlist_add_int32()、nvlist_add_uint32()、 nvlist_add_int64()、nvlist_add_uint64()、nvlist_add_string ()、nvlist_add_nvlist()、nvlist_add_nvpair()、 nvlist_add_boolean_array()、nvlist_add_int8_array、nvlist_add_uint8_array ()、nvlist_add_nvlist_array()、nvlist_add_byte_array ()、nvlist_add_int16_array()、nvlist_add_uint16_array ()、nvlist_add_int32_array()、nvlist_add_uint32_array ()、nvlist_add_int64_array()、nvlist_add_uint64_array ()、nvlist_add_string_array() |
|
处理名称-值列表缓冲区。函数包括: nvlist_alloc()、nvlist_free()、 nvlist_size()、nvlist_pack()、nvlist_unpack ()、nvlist_dup()、nvlist_merge() |
|
搜索名称-值对。函数包括: nvlist_lookup_boolean()、nvlist_lookup_boolean_value ()、nvlist_lookup_byte()、nvlist_lookup_int8 ()、nvlist_lookup_int16()、nvlist_lookup_int32 ()、nvlist_lookup_int64()、nvlist_lookup_uint8 ()、nvlist_lookup_uint16()、nvlist_lookup_uint32 ()、nvlist_lookup_uint64()、nvlist_lookup_string ()、nvlist_lookup_nvlist()、nvlist_lookup_boolean_array、nvlist_lookup_byte_array()、nvlist_lookup_int8_array()、 nvlist_lookup_int16_array()、nvlist_lookup_int32_array()、 nvlist_lookup_int64_array()、nvlist_lookup_uint8_array()、 nvlist_lookup_uint16_array()、nvlist_lookup_uint32_array()、 nvlist_lookup_uint64_array()、nvlist_lookup_string_array()、 nvlist_lookup_nvlist_array()、nvlist_lookup_pairs() |
|
获取名称-值对数据。函数包括: nvlist_next_nvpair()、nvpair_name()、 nvpair_type() |
|
删除名称-值对。函数包括: nv_remove()、nv_remove_all() |
本节讨论如何使用任务队列来延迟处理某些任务并将这些任务的执行委托给另一个内核线程。
内核编程中的一项常见操作是对某个任务进行调度,使它以后由另一线程执行。以下示例给出了可能需要以后由另一线程执行某个任务的一些原因:
当前的代码路径对时间有关键要求。要执行的其他任务对时间没有关键要求。
其他任务可能需要获取另一个线程当前持有的锁。
在当前上下文中无法进行阻塞。但其他任务可能需要阻塞,例如,它需要等待内存。
某种情况正在阻止代码路径完成,但是当前的代码路径不能休眠或失败。需要将当前任务排入队列,以便在该情况消失后执行。
需要以并行方式启动多个任务。
对于上面的每种情况,任务都在不同的上下文中执行。不同的上下文通常是持有一组不同锁的不同内核线程,并可能具有不同的优先级。任务队列提供一个通用内核 API 来调度异步任务。
任务队列是一个任务列表,一个或多个线程为该列表提供服务。如果任务队列只有一个服务线程,则所有任务肯定会按照它们在列表中添加的先后顺序执行。如果任务队列有多个服务线程,则任务的执行顺序是未知的。
如果任务队列有多个服务线程,请确保某个任务的执行不依赖于其他任何任务的执行。任务之间的相关性会导致产生死锁。
以下 DDI 接口管理任务队列。这些接口在 sys/sunddi.h 头文件中定义。有关这些接口的更多信息,请参见 taskq(9F) 手册页。
ddi_taskq_t |
不透明句柄 |
TASKQ_DEFAULTPRI |
系统缺省优先级 |
DDI_SLEEP |
可以阻塞以获得内存 |
DDI_NOSLEEP |
不能阻塞以获得内存 |
ddi_taskq_create() |
创建任务队列 |
ddi_taskq_destroy() |
销毁任务队列 |
ddi_taskq_dispatch() |
在任务队列中添加任务 |
ddi_taskq_wait() |
等待暂挂的任务完成 |
ddi_taskq_suspend() |
暂挂任务队列 |
ddi_taskq_suspended() |
检查任务队列是否已暂挂 |
ddi_taskq_resume() |
恢复暂挂的任务队列 |
在驱动程序中的典型应用是在调用 attach(9E) 时创建任务队列。大多数 taskq_dispatch() 调用都来自中断上下文。
要了解 Solaris 驱动程序中使用的任务队列,请访问 http://hub.opensolaris.org/bin/view/Main/。 在右上角单击 "Source Browser"(源代码浏览器)。在搜索区域的 "Symbol"(符号)字段中,输入 ddi_taskq_create。在 "File Path"(文件路径)字段中输入 amr。在“项目”列表中选择 onnv。单击 "Search"(搜索)按钮。在搜索结果中,应可看到 Dell PERC 3DC/4SC/4DC/4Di RAID 设备的 SCSI HBA 驱动程序 (amr.c)。
单击文件名 amr.c。将在 amr_attach() 入口点中调用 ddi_taskq_create() 函数。ddi_taskq_destroy() 函数将在 amr_detach() 入口点中调用,也会在 amr_attach() 入口点的错误处理部分中调用。ddi_taskq_dispatch () 函数在 amr_done() 函数中调用,而后者在 amr_intr() 函数中调用。amr_intr () 函数是一个中断处理函数,它是 amr_attach() 入口点中的 ddi_add_intr(9F) 函数的参数。
本节介绍两种可用来监视任务队列所使用的系统资源的方法。任务队列会导出任务队列线程使用系统时间的相关统计信息。任务队列还会使用 DTrace SDT 探测器来确定任务队列何时开始执行某个任务,以及何时完成执行。
每个任务队列都有一组关联的 kstat 计数器。检查以下 kstat(1M) 命令的输出:
$ kstat -c taskq module: unix instance: 0 name: ata_nexus_enum_tq class: taskq crtime 53.877907833 executed 0 maxtasks 0 nactive 1 nalloc 0 priority 60 snaptime 258059.249256749 tasks 0 threads 1 totaltime 0 module: unix instance: 0 name: callout_taskq class: taskq crtime 0 executed 13956358 maxtasks 4 nactive 4 nalloc 0 priority 99 snaptime 258059.24981709 tasks 13956358 threads 2 totaltime 120247890619 |
以上所示的 kstat 输出包含以下信息:
任务队列的名称及其实例编号
已调度任务的数目 (tasks) 以及已执行任务的数目 (executed)
处理任务队列的内核线程数 ( threads) 及其优先级 (priority)
处理所有任务花费的总时间(以纳秒为单位)(totaltime)
以下示例说明如何使用 kstat 命令来观察计数器(已调度任务的数目)是如何随时间而递增的:
$ kstat -p unix:0:callout_taskq:tasks 1 5 unix:0:callout_taskq:tasks 13994642 unix:0:callout_taskq:tasks 13994711 unix:0:callout_taskq:tasks 13994784 unix:0:callout_taskq:tasks 13994855 unix:0:callout_taskq:tasks 13994926 |
任务队列提供了若干个有用的 SDT 探测器。本节介绍的所有探测器都具有以下两个参数:
ddi_taskq_create() 返回的任务队列指针
指向 taskq_ent_t 结构的指针。在 D 脚本中使用该指针可以提取函数和参数。
可以使用这些探测器来收集有关各个任务队列以及通过这些队列执行的各个任务的精确计时信息。例如,以下脚本每隔 10 秒列显通过任务队列调度的函数:
# !/usr/sbin/dtrace -qs sdt:genunix::taskq-enqueue { this->tq = (taskq_t *)arg0; this->tqe = (taskq_ent_t *) arg1; @[this->tq->tq_name, this->tq->tq_instance, this->tqe->tqent_func] = count(); } tick-10s { printa ("%s(%d): %a called %@d times\n", @); trunc(@); } |
在特定的计算机上,以上 D 脚本生成以下输出:
callout_taskq(1): genunix`callout_execute called 51 times callout_taskq(0): genunix`callout_execute called 701 times kmem_taskq(0): genunix`kmem_update_timeout called 1 times kmem_taskq(0): genunix`kmem_hash_rescale called 4 times callout_taskq(1): genunix`callout_execute called 40 times USB_hid_81_pipehndl_tq_1(14): usba`hcdi_cb_thread called 256 times callout_taskq(0): genunix`callout_execute called 702 times kmem_taskq(0): genunix`kmem_update_timeout called 1 times kmem_taskq(0): genunix`kmem_hash_rescale called 4 times callout_taskq(1): genunix`callout_execute called 28 times USB_hid_81_pipehndl_tq_1(14): usba`hcdi_cb_thread called 228 times callout_taskq(0): genunix`callout_execute called 706 times callout_taskq(1): genunix`callout_execute called 24 times USB_hid_81_pipehndl_tq_1(14): usba`hcdi_cb_thread called 141 times callout_taskq(0): genunix`callout_execute called 708 times |
自动配置表示驱动程序会将代码和静态数据装入内存中。随后在系统中注册此信息。在自动配置过程中还会连接由驱动程序控制的各个设备实例。
本章介绍有关以下主题的信息:
系统从用于自动配置的内核模块目录的 drv 子目录装入驱动程序二进制模块。请参见将驱动程序复制到模块目录。
将模块读入内存且解析了所有符号之后,系统将调用此模块的 _init(9E) 入口点。_init() 函数将调用 mod_install(9F),实际上就是装入此模块。
调用 mod_install() 期间,一旦调用了 mod_install(),其他线程便可以调用 attach(9E)。从编程角度来看,在调用 mod_install() 之前必须执行所有 _init() 初始化。如果 mod_install() 失败(即返回非零值),则必须取消初始化。
一旦 _init() 成功完成,便会在系统中正确注册驱动程序。实际上,此时驱动程序并不管理任何设备。设备管理是在设备配置过程中进行的。
为了节省系统内存或根据用户的明确请求,系统会卸载驱动程序二进制模块。从内存中删除驱动程序代码和数据之前,将调用该驱动程序的 _fini(9E) 入口点。当且仅当 _fini() 返回成功信息时,才会卸载驱动程序。
下图概述了设备驱动程序的结构。阴影区域突出显示驱动程序的数据结构和入口点。阴影区域的上半部分包括支持驱动程序装入和卸载的数据结构和入口点。下半部分与驱动程序配置相关。
为了支持自动配置,驱动程序需要静态初始化以下数据结构:
驱动程序依赖于图 5-1 中的数据结构。必须提供并正确初始化这些数据结构。没有这些数据结构,可能无法正确装入驱动程序。结果导致可能无法装入必需的例程。如果驱动程序不支持某个操作,则 nodev(9F) 例程的地址可以用作占位符。在某些情况下,驱动程序支持入口点,并且仅需要返回成功信息或失败信息。在这种情况下,可以使用例程 nulldev(9F) 的地址。
应该在编译时对这些结构进行初始化。在任何其他时间,驱动程序都不应访问或更改这些结构。
static struct modlinkage xxmodlinkage = { MODREV_1, /* ml_rev */ &xxmodldrv, /* ml_linkage[] */ NULL /* NULL termination */ };
第一个字段是装入子系统的模块的版本号。该字段应为 MODREV_1。第二个字段指向接下来定义的驱动程序的 modldrv 结构。该结构的最后一个元素应始终为 NULL。
static struct modldrv xxmodldrv = { &mod_driverops, /* drv_modops */ "generic driver v1.1", /* drv_linkinfo */ &xx_dev_ops /* drv_dev_ops */ };
该结构更加详细地描述模块。第一个字段提供有关模块安装的信息。对于驱动程序模块,该字段应设置为 &mod_driverops。第二个字段是将由 modinfo(1M) 显示的字符串。第二个字段应包含足够的信息,以便确定生成驱动程序二进制文件的源代码版本。最后一个字段指向下节所定义的驱动程序的 dev_ops 结构。
static struct dev_ops xx_dev_ops = { DEVO_REV, /* devo_rev */ 0, /* devo_refcnt */ xxgetinfo, /* devo_getinfo: getinfo(9E) */ nulldev, /* devo_identify: identify(9E) */ xxprobe, /* devo_probe: probe(9E) */ xxattach, /* devo_attach: attach(9E) */ xxdetach, /* devo_detach: detach(9E) */ nodev, /* devo_reset */ &xx_cb_ops, /* devo_cb_ops */ NULL, /* devo_bus_ops */ &xxpower /* devo_power: power(9E) */ };
使用 dev_ops(9S) 结构,内核可以找到设备驱动程序的自动配置入口点。devo_rev 字段标识结构的修订号。该字段必须设置为 DEVO_REV。devo_refcnt 字段必须初始化为零。应使用相应驱动程序的入口点地址填充函数地址字段,但以下情况除外:
将 devo_identify 字段设置为 nulldev(9F)。identify() 入口点已过时。
如果不需要 probe(9E) 例程,应将 devo_probe 字段设置为 nulldev(9F)。
将 devo_reset 字段设置为 nodev(9F)。nodev() 函数返回 ENXIO。
如果不需要 power() 例程,应将 devo_power 字段设置为 NULL。提供电源管理功能的设备的驱动程序必须具有 power(9E) 入口点。请参见第 12 章。
devo_cb_ops 成员应包含 cb_ops(9S) 结构的地址。devo_bus_ops 字段必须设置为 NULL。
static struct cb_ops xx_cb_ops = { xxopen, /* open(9E) */ xxclose, /* close(9E) */ xxstrategy, /* strategy(9E) */ xxprint, /* print(9E) */ xxdump, /* dump(9E) */ xxread, /* read(9E) */ xxwrite, /* write(9E) */ xxioctl, /* ioctl(9E) */ xxdevmap, /* devmap(9E) */ nodev, /* mmap(9E) */ xxsegmap, /* segmap(9E) */ xxchpoll, /* chpoll(9E) */ xxprop_op, /* prop_op(9E) */ NULL, /* streamtab(9S) */ D_MP | D_64BIT, /* cb_flag */ CB_REV, /* cb_rev */ xxaread, /* aread(9E) */ xxawrite /* awrite(9E) */ };
cb_ops(9S) 结构包含设备驱动程序的字符操作和块操作的入口点。驱动程序不支持的所有入口点应初始化为 nodev(9F)。例如,字符设备驱动程序应该将所有块字段(例如 cb_stategy)设置为 nodev(9F)。请注意,保留 mmap(9E) 入口点是为了兼容早期发行版。驱动程序应使用 devmap(9E) 入口点来进行设备内存映射。如果支持 devmap(9E),应将 mmap(9E) 设置为 nodev(9F)。
streamtab 字段表明驱动程序是否基于 STREAMS。只有第 19 章中讨论的网络设备驱动程序基于 STREAMS。所有不基于 STREAMS 的驱动程序必须将 streamtab 字段设置为 NULL。
cb_flag 成员包含以下标志:
D_64BIT 标志导致驱动程序使用 uio(9S) 结构的 uio_loffset 字段。驱动程序应在 cb_flag 字段中设置 D_64BIT 标志,以便正确处理 64 位偏移。
D_DEVMAP 标志支持 devmap(9E) 入口点。有关 devmap(9E) 的信息,请参见第 10 章。
cb_rev 是 cb_ops 结构修订号。该字段必须设置为 CB_REV。
设备驱动程序必须是可动态装入的。驱动程序还应是可卸载的,以帮助节省内存资源。可卸载驱动程序还应易于测试、调试和修补。
每个设备驱动程序都需要实现 _init(9E)、_fini(9E) 和 _info(9E) 入口点以支持驱动程序的装入和卸载。以下示例给出了可装入驱动程序接口的典型实现。
static void *statep; /* for soft state routines */ static struct cb_ops xx_cb_ops; /* forward reference */ static struct dev_ops xx_ops = { DEVO_REV, 0, xxgetinfo, nulldev, xxprobe, xxattach, xxdetach, xxreset, nodev, &xx_cb_ops, NULL, xxpower }; static struct modldrv modldrv = { &mod_driverops, "xx driver v1.0", &xx_ops }; static struct modlinkage modlinkage = { MODREV_1, &modldrv, NULL }; int _init(void) { int error; ddi_soft_state_init(&statep, sizeof (struct xxstate), estimated_number_of_instances); /* further per-module initialization if necessary */ error = mod_install(&modlinkage); if (error != 0) { /* undo any per-module initialization done earlier */ ddi_soft_state_fini(&statep); } return (error); } int _fini(void) { int error; error = mod_remove(&modlinkage); if (error == 0) { /* release per-module resources if any were allocated */ ddi_soft_state_fini(&statep); } return (error); } int _info(struct modinfo *modinfop) { return (mod_info(&modlinkage, modinfop)); }
以下示例给出了典型的 _init(9E) 接口。
static void *xxstatep; int _init(void) { int error; const int max_instance = 20; /* estimated max device instances */ ddi_soft_state_init(&xxstatep, sizeof (struct xxstate), max_instance); error = mod_install(&xxmodlinkage); if (error != 0) { /* * Cleanup after a failure */ ddi_soft_state_fini(&xxstatep); } return (error); }
在 _init() 中装入驱动程序期间,驱动程序应执行所有一次性资源分配或数据初始化。例如,在该例程中驱动程序应初始化所有对于该驱动程序为全局互斥锁的互斥锁。但是,驱动程序不应使用 _init(9E) 来分配或初始化与设备特定实例有关的任何内容。必须在 attach(9E) 中完成每个实例的初始化。例如,如果打印机的驱动程序可以同时处理多台打印机,则该驱动程序应在 attach() 中分配特定于每台打印机实例的资源。
一旦 _init(9E) 调用了 mod_install(9F),驱动程序便不应更改连接至 modlinkage(9S) 结构的任何数据结构,因为系统可能会复制或更改这些数据结构。
以下示例给出了 _fini() 例程。
int _fini(void) { int error; error = mod_remove(&modlinkage); if (error != 0) { return (error); } /* * Cleanup resources allocated in _init() */ ddi_soft_state_fini(&xxstatep); return (0); }
同样,在 _fini() 中,驱动程序应该释放在 _init() 中分配的所有资源。驱动程序必须将其自身从系统模块列表中删除。
将驱动程序连接至硬件实例时,可能会调用 _fini()。在本示例中,mod_remove(9F) 返回失败信息。因此,在 mod_remove() 返回成功信息之前,不应释放驱动程序资源。
以下示例给出了 _info(9E) 例程。
int _info(struct modinfo *modinfop) { return (mod_info(&xxmodlinkage, modinfop)); }
系统基于节点名称和 compatible 属性为内核设备树中的每个节点选择驱动程序(请参见将驱动程序绑定到设备)。相同的驱动程序可能会绑定到多个设备节点。驱动程序可以根据系统指定的实例编号来区分不同的节点。
为设备节点选择驱动程序之后,将调用该驱动程序的 probe(9E) 入口点以确定系统上是否存在该设备。如果 probe() 成功,将调用该驱动程序的 attach(9E) 入口点以设置和管理设备。当且仅当 attach() 返回成功信息时,才能打开该设备(请参见attach() 入口点)。
可能会取消配置设备以节省系统内存资源,或在系统仍在运行时使设备可以移除。要取消配置设备,系统首先会检查是否引用了设备实例。此检查将调用驱动程序的 getinfo(9E) 入口点以获取仅为该驱动程序所知的信息(请参见getinfo() 入口点)。如果未引用设备实例,将调用驱动程序的 detach(9E) 例程来取消配置设备(请参见detach() 入口点)。
要进行更新,每个驱动程序都必须定义内核用于设备配置的以下入口点:
请注意,attach()、detach() 和 getinfo() 是必需的。只有无法自我识别的设备需要 probe()。对于自标识设备,可以提供显式 probe() 例程,或者在 dev_ops 结构中为 probe() 入口点指定 nulldev(9F)。
系统会为每个设备指定一个实例编号。驱动程序可能无法可靠地预测指定给某个特定设备的实例编号值。驱动程序应通过调用 ddi_get_instance(9F) 来检索已指定的特定实例编号。
实例编号代表了系统中的设备。内核会为特定驱动程序的每个 dev_info(即设备树中的每个节点)指定一个实例编号。此外,实例编号可提供一种便捷的、为特定于某个物理设备的数据建立索引的机制。实例编号的最常见用法是 ddi_get_soft_state(9F),也就是使用实例编号检索特定物理设备的软状态数据。
对于伪设备(即伪结点的子结点),其实例编号是采用 instance 属性在 driver.conf(4) 文件中定义的。如果 driver.conf 文件不包含 instance 属性,则未定义此行为。对于硬件设备节点,当 OS 首次发现此类设备时,系统会为其指定实例编号。实例编号在系统重新引导以及 OS 升级期间保持不变。
驱动程序负责管理其次要设备号名称空间。例如,sd 驱动程序需要向每个磁盘的文件系统导出八个字符次要节点和八个块次要节点。每个次要节点代表部分磁盘的块接口或字符接口。getinfo(9E) 入口点通知系统有关次要设备号到设备实例的映射(请参见getinfo() 入口点)。
对于非自我识别设备,probe(9E) 入口点应确定系统上是否存在硬件设备。
对于 probe(),要确定是否存在设备实例,probe()需要执行通常 attach(9E) 也执行的许多任务。尤其是,probe() 可能需要映射设备寄存器。
探测设备寄存器是特定于设备的。驱动程序通常必须执行一系列硬件测试来确保硬件确实存在。测试条件必须足够严格以避免错误地识别设备。例如,在设备实际上不可用的情况下可能显示存在该设备,因为异常设备在行为上看起来与预期的设备相似。
测试返回以下标志:
DDI_PROBE_SUCCESS(探测成功)
DDI_PROBE_FAILURE(探测失败)
DDI_PROBE_DONTCARE(探测不成功,但仍需调用 attach(9E))
DDI_PROBE_PARTIAL(现在不存在实例,但将来可能会出现)
对于给定设备实例,直到 probe(9E) 在该设备上至少成功一次时,才会调用 attach(9E)。
probe(9E) 必须释放 probe() 已分配的所有资源,因为可能会调用 probe() 多次。但是,即使 probe(9E) 已成功,也不一定要调用 attach(9E)。
可以在驱动程序的 probe(9E) 例程中使用 ddi_dev_is_sid(9F) 来确定设备是否可以自我识别。在为同一设备的自我识别版本和非自我识别版本编写驱动程序时,ddi_dev_is_sid() 非常有用。
以下示例是一个样例 probe() 例程。
static int xxprobe(dev_info_t *dip) { ddi_acc_handle_t dev_hdl; ddi_device_acc_attr_t dev_attr; Pio_csr *csrp; uint8_t csrval; /* * if the device is self identifying, no need to probe */ if (ddi_dev_is_sid(dip) == DDI_SUCCESS) return (DDI_PROBE_DONTCARE); /* * Initalize the device access attributes and map in * the devices CSR register (register 0) */ dev_attr.devacc_attr_version = DDI_DEVICE_ATTR_V0; dev_attr.devacc_attr_endian_flags = DDI_STRUCTURE_LE_ACC; dev_attr.devacc_attr_dataorder = DDI_STRICTORDER_ACC; if (ddi_regs_map_setup(dip, 0, (caddr_t *)&csrp, 0, sizeof (Pio_csr), &dev_attr, &dev_hdl) != DDI_SUCCESS) return (DDI_PROBE_FAILURE); /* * Reset the device * Once the reset completes the CSR should read back * (PIO_DEV_READY | PIO_IDLE_INTR) */ ddi_put8(dev_hdl, csrp, PIO_RESET); csrval = ddi_get8(dev_hdl, csrp); /* * tear down the mappings and return probe success/failure */ ddi_regs_map_free(&dev_hdl); if ((csrval & 0xff) == (PIO_DEV_READY | PIO_IDLE_INTR)) return (DDI_PROBE_SUCCESS); else return (DDI_PROBE_FAILURE); }
调用驱动程序的 probe(9E) 例程时,驱动程序并不知道正在探测的设备是否存在于总线上。因此,驱动程序可能会尝试访问不存在设备的设备寄存器。结果,在某些总线上可能会产生总线故障。
以下示例给出了使用 ddi_poke8(9F) 来检查设备是否存在的 probe(9E) 例程。ddi_poke8() 谨慎地尝试将值写入指定的虚拟地址,必要时使用父结点驱动程序协助进程。如果地址无效或无法在不出现错误的情况下写入值,则会返回错误代码。另请参见 ddi_peek(9F)。
在本示例中,使用 ddi_regs_map_setup(9F) 来映射设备寄存器。
static int xxprobe(dev_info_t *dip) { ddi_acc_handle_t dev_hdl; ddi_device_acc_attr_t dev_attr; Pio_csr *csrp; uint8_t csrval; /* * if the device is self-identifying, no need to probe */ if (ddi_dev_is_sid(dip) == DDI_SUCCESS) return (DDI_PROBE_DONTCARE); /* * Initialize the device access attrributes and map in * the device's CSR register (register 0) */ dev_attr.devacc_attr_version - DDI_DEVICE_ATTR_V0; dev_attr.devacc_attr_endian_flags = DDI_STRUCTURE_LE_ACC; dev_attr.devacc_attr_dataorder = DDI_STRICTORDER_ACC; if (ddi_regs_map_setup(dip, 0, (caddr_t *)&csrp, 0, sizeof (Pio_csr), &dev_attr, &dev_hdl) != DDI_SUCCESS) return (DDI_PROBE_FAILURE); /* * The bus can generate a fault when probing for devices that * do not exist. Use ddi_poke8(9f) to handle any faults that * might occur. * * Reset the device. Once the reset completes the CSR should read * back (PIO_DEV_READY | PIO_IDLE_INTR) */ if (ddi_poke8(dip, csrp, PIO_RESET) != DDI_SUCCESS) { ddi_regs_map_free(&dev_hdl); return (DDI_FAILURE); csrval = ddi_get8(dev_hdl, csrp); /* * tear down the mappings and return probe success/failure */ ddi_regs_map_free(&dev_hdl); if ((csrval & 0xff) == (PIO_DEV_READY | PIO_IDLE_INTR)) return (DDI_PROBE_SUCCESS); else return (DDI_PROBE_FAILURE); }
内核调用驱动程序的 attach(9E) 入口点来连接设备实例或针对已由电源管理框架暂停或关闭的设备实例恢复操作。本节仅讨论连接设备实例的操作。电源管理将在第 12 章中讨论。
调用驱动程序的 attach(9E) 入口点以连接每个绑定到驱动程序的设备实例。基于要连接的设备节点实例,并将 attach(9E) 的 cmd 参数指定为 DDI_ATTACH,来调用此入口点。attach 入口点主要包括以下类型的处理:
为了协助设备驱动程序编写人员分配状态结构,Solaris DDI/DKI 提供了一组内存管理例程,称为软件状态管理例程,也称为软状态例程。这些例程可动态分配、检索以及销毁指定大小的内存项,并可隐藏列表管理的详细信息。实例编号标识所需的内存项。此编号通常为系统指定的实例编号。
通常,驱动程序会为与其连接的每个设备实例分配软状态结构,方法是调用 ddi_soft_state_zalloc(9F) 并传递设备的实例编号。由于两个设备节点不能具有相同的实例编号,所以对于已经分配出去的给定实例编号,ddi_soft_state_zalloc(9F) 将失败。
驱动程序的字符入口点或块入口点(cb_ops(9S))通过先解码来自传递到入口点函数的 dev_t 参数的设备实例编号,来引用特定的软状态结构。随后,驱动程序调用 ddi_get_soft_state(9F),传递每个驱动程序的软状态列表和生成的实例编号。返回值 NULL 表明实际上不存在该设备并且应由驱动程序返回相应的代码。
有关实例编号和设备编号(dev_t 编号)之间关系的更多信息,请参见创建从设备节点。
驱动程序在连接期间应初始化所有基于实例的锁和条件变量。添加任何中断处理程序之前,必须先初始化驱动程序中断处理程序所获取的所有锁。有关锁的初始化和使用的说明,请参见第 3 章。有关中断处理程序和锁问题的讨论,请参见第 8 章。
连接过程的一个重要部分是为设备实例创建次要节点。次要节点包含由设备和 DDI 框架导出的信息。系统使用此信息为 /devices 下的次要节点创建特殊文件。
驱动程序调用 ddi_create_minor_node(9F) 时会创建次要节点。驱动程序提供次要设备号、次要名称、次要节点类型,以及次要节点是代表块设备还是字符设备。
驱动程序可以为设备创建任意数量的次要节点。Solaris DDI/DKI 期望某些类别的设备具有以特定格式创建的次要节点。例如,期望磁盘驱动程序为连接的每个物理磁盘实例创建 16 个次要节点。将创建八个代表块设备接口 a - h 的次要节点,另外八个次要节点代表字符设备接口 a,raw - h,raw。
传递给 ddi_create_minor_node(9F) 的次要设备号全部由驱动程序定义。次要设备号通常是设备实例编号和次要节点标识符的编码。在前面的示例中,驱动程序会为每个次要节点创建次要设备号,方法是将设备的实例编号左移三位,再将该结果与次要节点索引进行“或”运算。次要节点索引值的范围介于 0 和 7 之间。请注意,次要节点 a 和 a,raw 共用同一次要设备号。这些次要节点根据传递到 ddi_create_minor_node() 的 spec_type 参数来区分。
传递给 ddi_create_minor_node(9F) 的次要节点类型对设备类型进行分类,如磁盘、磁带、网络接口、帧缓存器等。
下表列出了可以创建的可能的节点类型。
表 6–1 可能节点类型
常量 |
说明 |
---|---|
DDI_NT_SERIAL |
串行端口 |
DDI_NT_SERIAL_DO |
拨出端口 |
DDI_NT_BLOCK |
硬盘 |
DDI_NT_BLOCK_CHAN |
带有通道或目标编号的硬盘 |
DDI_NT_CD |
ROM 驱动器 (CD-ROM) |
DDI_NT_CD_CHAN |
带有通道或目标编号的 ROM 驱动器 |
DDI_NT_FD |
软盘 |
DDI_NT_TAPE |
磁带机 |
DDI_NT_NET |
网络设备 |
DDI_NT_DISPLAY |
显示设备 |
DDI_NT_MOUSE |
鼠标 |
DDI_NT_KEYBOARD |
键盘 |
DDI_NT_AUDIO |
音频设备 |
DDI_PSEUDO |
通用的伪设备 |
节点类型 DDI_NT_BLOCK、DDI_NT_BLOCK_CHAN、DDI_NT_CD 和 DDI_NT_CD_CHAN 会使 devfsadm(1M) 将设备实例标识为磁盘,并在 /dev/dsk 或 /dev/rdsk 目录中创建名称。
节点类型 DDI_NT_TAPE 会使 devfsadm(1M) 将设备实例标识为磁带,并在 /dev/rmt 目录中创建名称。
节点类型 DDI_NT_SERIAL 和 DDI_NT_SERIAL_DO 会使 devfsadm(1M) 执行以下操作:
将设备实例标识为串行端口
在 /dev/term 目录中创建名称
向 /etc/inittab 文件中添加项
供应商提供的字符串应包括使字符串唯一的标识值,如名称或股票名称。该字符串可与 devfsadm(1M) 和 devlinks.tab 文件(请参见 devlinks(1M) 手册页)一起使用以在 /dev 中创建逻辑名称。
在相应实例上的 attach(9E) 成功之前,可能会对次要设备调用 open(9E)。然后 open() 必须返回 ENXIO,这将导致系统尝试连接该设备。如果 attach() 成功,则会自动重试 open()。
/* * Attach an instance of the driver. We take all the knowledge we * have about our board and check it against what has been filled in * for us from our FCode or from our driver.conf(4) file. */ static int xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd) { int instance; Pio *pio_p; ddi_device_acc_attr_t da_attr; static int pio_validate_device(dev_info_t *); switch (cmd) { case DDI_ATTACH: /* * first validate the device conforms to a configuration this driver * supports */ if (pio_validate_device(dip) == 0) return (DDI_FAILURE); /* * Allocate a soft state structure for this device instance * Store a pointer to the device node in our soft state structure * and a reference to the soft state structure in the device * node. */ instance = ddi_get_instance(dip); if (ddi_soft_state_zalloc(pio_softstate, instance) != 0) return (DDI_FAILURE); pio_p = ddi_get_soft_state(pio_softstate, instance); ddi_set_driver_private(dip, (caddr_t)pio_p); pio_p->dip = dip; /* * Before adding the interrupt, get the interrupt block * cookie associated with the interrupt specification to * initialize the mutex used by the interrupt handler. */ if (ddi_get_iblock_cookie(dip, 0, &pio_p->iblock_cookie) != DDI_SUCCESS) { ddi_soft_state_free(pio_softstate, instance); return (DDI_FAILURE); } mutex_init(&pio_p->mutex, NULL, MUTEX_DRIVER, pio_p->iblock_cookie); /* * Now that the mutex is initialized, add the interrupt itself. */ if (ddi_add_intr(dip, 0, NULL, NULL, pio_intr, (caddr_t)instance) != DDI_SUCCESS) { mutex_destroy(&pio_p>mutex); ddi_soft_state_free(pio_softstate, instance); return (DDI_FAILURE); } /* * Initialize the device access attributes for the register mapping */ dev_acc_attr.devacc_attr_version = DDI_DEVICE_ATTR_V0; dev_acc_attr.devacc_attr_endian_flags = DDI_STRUCTURE_LE_ACC; dev_acc_attr.devacc_attr_dataorder = DDI_STRICTORDER_ACC; /* * Map in the csr register (register 0) */ if (ddi_regs_map_setup(dip, 0, (caddr_t *)&(pio_p->csr), 0, sizeof (Pio_csr), &dev_acc_attr, &pio_p->csr_handle) != DDI_SUCCESS) { ddi_remove_intr(pio_p->dip, 0, pio_p->iblock_cookie); mutex_destroy(&pio_p->mutex); ddi_soft_state_free(pio_softstate, instance); return (DDI_FAILURE); } /* * Map in the data register (register 1) */ if (ddi_regs_map_setup(dip, 1, (caddr_t *)&(pio_p->data), 0, sizeof (uchar_t), &dev_acc_attr, &pio_p->data_handle) != DDI_SUCCESS) { ddi_remove_intr(pio_p->dip, 0, pio_p->iblock_cookie); ddi_regs_map_free(&pio_p->csr_handle); mutex_destroy(&pio_p->mutex); ddi_soft_state_free(pio_softstate, instance); return (DDI_FAILURE); } /* * Create an entry in /devices for user processes to open(2) * This driver will create a minor node entry in /devices * of the form: /devices/..../pio@X,Y:pio */ if (ddi_create_minor_node(dip, ddi_get_name(dip), S_IFCHR, instance, DDI_PSEUDO, 0) == DDI_FAILURE) { ddi_remove_intr(pio_p->dip, 0, pio_p->iblock_cookie); ddi_regs_map_free(&pio_p->csr_handle); ddi_regs_map_free(&pio_p->data_handle); mutex_destroy(&pio_p->mutex); ddi_soft_state_free(pio_softstate, instance); return (DDI_FAILURE); } /* * reset device (including disabling interrupts) */ ddi_put8(pio_p->csr_handle, pio_p->csr, PIO_RESET); /* * report the name of the device instance which has attached */ ddi_report_dev(dip); return (DDI_SUCCESS); case DDI_RESUME: return (DDI_SUCCESS); default: return (DDI_FAILURE); } }
attach() 例程不能对不同设备实例上的调用顺序做出任何假设。系统可以并行调用不同设备实例上的 attach()。系统还可以在不同设备实例上同时调用 attach() 和 detach()。
内核调用驱动程序的 detach(9E) 入口点,通过电源管理来分离设备的某个实例或暂停对设 备某个实例的操作。本节讨论分离设备实例的操作。有关电源管理问题的讨论,请参阅第 12 章。
调用驱动程序detach() 入口点以分离绑定到该驱动程序的设备的某个实例。该入口点是使用要分离的设备节点的实例和指定为该入口点的 cmd 参数的 DDI_DETACH 来调用的。
驱动程序需要取消或等待所有超时或回调完成,然后在返回前释放分配给设备实例的所有资源。如果由于某种原因,驱动程序无法取消未完成的回调以释放资源,则驱动程序需要将设备返回至其初始状态并从入口点返回 DDI_FAILURE,使设备实例保持连接状态。
有两种类型的回调例程: 可取消回调例程和不可取消回调例程。驱动程序在 detach(9E) 期间可以原子方式取消 timeout(9F) 和 bufcall(9F) 回调例程。其他类型的回调例程,如 scsi_init_pkt(9F) 和 ddi_dma_buf_bind_handle(9F),则不能被取消。驱动程序必须要么阻塞在 detach() 中直到回调完成,要么使分离请求失败。
/* * detach(9e) * free the resources that were allocated in attach(9e) */ static int xxdetach(dev_info_t *dip, ddi_detach_cmd_t cmd) { Pio *pio_p; int instance; switch (cmd) { case DDI_DETACH: instance = ddi_get_instance(dip); pio_p = ddi_get_soft_state(pio_softstate, instance); /* * turn off the device * free any resources allocated in attach */ ddi_put8(pio_p->csr_handle, pio_p->csr, PIO_RESET); ddi_remove_minor_node(dip, NULL); ddi_regs_map_free(&pio_p->csr_handle); ddi_regs_map_free(&pio_p->data_handle); ddi_remove_intr(pio_p->dip, 0, pio_p->iblock_cookie); mutex_destroy(&pio_p->mutex); ddi_soft_state_free(pio_softstate, instance); return (DDI_SUCCESS); case DDI_SUSPEND: default: return (DDI_FAILURE); } }
系统调用 getinfo(9E) 以获取仅为驱动程序所知的配置信息。次要设备号到设备实例的映射完全由驱动程序控制。有时系统需要询问驱动程序特定的 dev_t 代表哪个设备。
getinfo() 函数可以采用 DDI_INFO_DEVT2INSTANCE 或 DDI_INFO_DEVT2DEVINFO 作为其 infocmd 参数。DDI_INFO_DEVT2INSTANCE 命令请求设备的实例编号。DDI_INFO_DEVT2DEVINFO 命令请求指向设备的 dev_info 结构的指针。
如果是 DDI_INFO_DEVT2INSTANCE,则 arg 为 dev_t,并且 getinfo() 必须将 dev_t 中的次要设备号转换为实例编号。在以下示例中,次要设备号是实例编号,因此 getinfo() 仅传回次要设备号。在这种情况下,驱动程序不能假定状态结构可用,因为可能在调用 attach() 之前调用 getinfo()。由驱动程序定义的次要设备号和实例编号之间的映射关系可以与此示例中的不同。但是,在所有情况下,映射必须是静态的。
如果是 DDI_INFO_DEVT2DEVINFO,则 arg 仍为 dev_t,因此,getinfo() 首先将设备的实例编号解码。然后 getinfo() 传送回保存在相应设备的驱动程序软状态结构中的 dev_info 指针,如以下示例所示。
/* * getinfo(9e) * Return the instance number or device node given a dev_t */ static int xxgetinfo(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result) { int error; Pio *pio_p; int instance = getminor((dev_t)arg); switch (infocmd) { /* * return the device node if the driver has attached the * device instance identified by the dev_t value which was passed */ case DDI_INFO_DEVT2DEVINFO: pio_p = ddi_get_soft_state(pio_softstate, instance); if (pio_p == NULL) { *result = NULL; error = DDI_FAILURE; } else { mutex_enter(&pio_p->mutex); *result = pio_p->dip; mutex_exit(&pio_p->mutex); error = DDI_SUCCESS; } break; /* * the driver can always return the instance number given a dev_t * value, even if the instance is not attached. */ case DDI_INFO_DEVT2INSTANCE: *result = (void *)instance; error = DDI_SUCCESS; break; default: *result = NULL; error = DDI_FAILURE; } return (error); }
getinfo() 例程必须与驱动程序创建的次要节点保持同步。如果次要节点不同步,则任何热插拔操作都可能失败并导致系统混乱。
使用 Solaris DDI 接口,驱动程序可以提供设备 ID,即设备的永久唯一标识符。设备 ID 可用于识别或查找设备。设备 ID 独立于 /devices 名称或设备编号 (dev_t)。应用程序可以使用 libdevid(3LIB) 中定义的函数来读取和处理由驱动程序注册的设备 ID。
在驱动程序可以导出设备 ID 之前,驱动程序需要检验设备是否可以提供唯一 ID 或者将主机生成的唯一 ID 存储在正常情况下不可访问的区域中。例如,通用编号 (world-wide number, WWN) 是设备提供的唯一 ID。例如,设备 NVRAM 和保留扇区是不可访问区域,主机生成的唯一 ID 可以安全地存储在此区域中。
通常,驱动程序在其 attach(9E) 处理程序中初始化和注册设备 ID。如上所述,驱动程序负责注册永久设备 ID。同时,驱动程序可能需要处理可直接提供唯一 ID (WWN) 的设备和向稳定存储器写入及从稳定存储器读取虚构 ID 的设备。
如果设备可以为驱动程序提供唯一的标识符,则驱动程序可以直接使用此标识符初始化设备 ID 并使用 Solaris DDI 注册此 ID。
/* * The device provides a guaranteed unique identifier, * in this case a SCSI3-WWN. The WWN for the device has been * stored in the device's soft state. */ if (ddi_devid_init(dip, DEVID_SCSI3_WWN, un->un_wwn_len, un->un_wwn, &un->un_devid) != DDI_SUCCESS) return (DDI_FAILURE); (void) ddi_devid_register(dip, un->un_devid);
驱动程序还可能为不直接提供唯一 ID 的设备注册设备 ID。注册这些 ID 需要设备能够存储并检索保留区中的少量数据。随后,驱动程序可创建虚构设备 ID 并将其写入保留区中。
/* * the device doesn't supply a unique ID, attempt to read * a fabricated ID from the device's reserved data. */ if (xxx_read_deviceid(un, &devid_buf) == XXX_OK) { if (ddi_devid_valid(devid_buf) == DDI_SUCCESS) { devid_sz = ddi_devi_sizeof(devid_buf); un->un_devid = kmem_alloc(devid_sz, KM_SLEEP); bcopy(devid_buf, un->un_devid, devid_sz); ddi_devid_register(dip, un->un_devid); return (XXX_OK); } } /* * we failed to read a valid device ID from the device * fabricate an ID, store it on the device, and register * it with the DDI */ if (ddi_devid_init(dip, DEVID_FAB, 0, NULL, &un->un_devid) == DDI_FAILURE) { return (XXX_FAILURE); } if (xxx_write_deviceid(un) != XXX_OK) { ddi_devid_free(un->un_devid); un->un_devid = NULL; return (XXX_FAILURE); } ddi_devid_register(dip, un->un_devid); return (XXX_OK);
通常,驱动程序会注销并释放处理 detach(9E) 时分配的所有设备 ID。驱动程序首先调用 ddi_devid_unregister(9F) 来注销设备实例的设备 ID。然后,驱动程序必须通过调用 ddi_devid_free(9F) 并传送已由 ddi_devid_init(9F) 返回的句柄来释放设备 ID 句柄自身。驱动程序负责管理为 WWN 或序列号数据分配的任何空间。
Solaris OS 为驱动程序开发者提供了一整套用于访问设备内存的接口。这些接口旨在通过处理处理器和设备字节存储顺序之间的不匹配,并强制实施设备可能具有的任何数据顺序相关性,使驱动程序与平台无关。通过使用这些接口,可以开发一种可在 SPARC 和 x86 处理器体系结构以及每个相应处理器系列的各种平台上运行的单个源驱动程序。
本章介绍有关以下主题的信息:
系统会为支持程控 I/O 的设备指定一个或多个总线地址空间区域,这些区域映射到设备的可寻址区域。这些映射在与设备相关的 reg 属性中描述为值对。每个值对描述一段总线地址。
驱动程序通过指定寄存器编号(即 regspec,设备的 reg 属性的索引)来标识特定的总线地址映射。reg 属性标识设备的 busaddr 和 size。驱动程序在调用 DDI 函数(如 ddi_regs_map_setup(9F))时传递寄存器编号。驱动程序通过调用 ddi_dev_nregs(9F) 可以确定已为设备指定的可映射区域数。
主机的数据格式可以与设备的数据格式具有不同的字节序特征。在这种情况下,主机与设备间传送的数据需要进行字节交换,才能符合目标位置的数据格式要求。与主机具有相同字节序特征的设备无需对数据进行字节交换。
驱动程序通过在传递给 ddi_regs_map_setup(9F) 的 ddi_device_acc_attr(9S) 结构中设置相应的标志来指定设备的字节序特征。然后,DDI 框架在驱动程序调用 ddi_getX 例程(如 ddi_get8(9F))或 ddi_putX 例程(如 ddi_put16(9F))来读/写设备内存时,执行任何所需的字节交换。
平台可以重新排列数据的负载和存储,以优化平台的性能。由于某些设备可能不允许重新排列,因此驱动程序在设置到设备的映射时需要指定设备的排序要求。
此结构描述设备的字节序和数据顺序要求。驱动程序需要对此结构进行初始化并将其作为一个参数传递给 ddi_regs_map_setup(9F)。
typedef struct ddi_device_acc_attr { ushort_t devacc_attr_version; uchar_t devacc_attr_endian_flags; uchar_t devacc_attr_dataorder; } ddi_device_acc_attr_t;
指定 DDI_DEVICE_ATTR_V0
描述设备的字节序特征。指定为一个位值,其可能值包括:
DDI_NEVERSWAP_ACC-从不交换数据
DDI_STRUCTURE_BE_ACC-设备数据格式为大端字节序
DDI_STRUCTURE_LE_ACC-设备数据格式为小端字节序
描述 CPU 根据设备的要求引用数据时必须遵循的顺序。指定为一个枚举值,其中数据访问限制的排列顺序为最严格到最不严格。
DDI_STRICTORDER_ACC-主机必须按程序员指定的顺序发出引用。此标志为缺省行为。
DDI_UNORDERED_OK_ACC-允许主机重新排列到设备内存的负载和存储。
DDI_MERGING_OK_ACC-允许主机将单个存储合并到连续位置。此设置还表明需要重新排列。
DDI_LOADCACHING_OK_ACC-允许主机从设备读取数据,直到发生存储。
DDI_STORECACHING_OK_ACC-允许主机对写入设备的数据进行高速缓存。然后,主机可以延迟将数据写入设备,直到将来某一时间。
系统对数据的访问可能会比驱动程序在 devacc_attr_dataorder 中所做指定更严格。就数据访问而言,由从必须遵循严格的数据排序到可以执行高速缓存存储操作,驱动程序对主机的限制依次降低。
驱动程序通常会在执行 attach(9E) 期间映射设备的所有区域。驱动程序通过调用 ddi_regs_map_setup(9F)、指定要映射的区域寄存器编号、区域的设备访问属性以及偏移和大小来映射设备内存区域。DDI 框架为设备区域设置映射并将一个不透明句柄返回给驱动程序。在从设备区域读取数据或向其中写入数据时,此数据访问句柄将作为一个参数传递给 ddi_get8(9F) 或 ddi_put8(9F) 系列例程。
驱动程序通过检查设备导出的映射数来验证设备映射的形式与驱动程序预期的形式是否匹配。驱动程序调用 ddi_dev_nregs(9F),然后调用 ddi_dev_regsize(9F) 来验证每个映射的大小。
下面的简单示例说明了 DDI 数据访问接口。此驱动程序用于虚构的小端字节序设备,该设备每次接受一个字符并在准备好接受另一个字符时生成中断。此设备实现两个寄存器集: 第一个是 8 位 CSR 寄存器,第二个是 8 位数据寄存器。
#define CSR_REG 0 #define DATA_REG 1 /* * Initialize the device access attributes for the register * mapping */ dev_acc_attr.devacc_attr_version = DDI_DEVICE_ATTR_V0; dev_acc_attr.devacc_attr_endian_flags = DDI_STRUCTURE_LE_ACC; dev_acc_attr.devacc_attr_dataorder = DDI_STRICTORDER_ACC; /* * Map in the csr register (register 0) */ if (ddi_regs_map_setup(dip, CSR_REG, (caddr_t *)&(pio_p->csr), 0, sizeof (Pio_csr), &dev_acc_attr, &pio_p->csr_handle) != DDI_SUCCESS) { mutex_destroy(&pio_p->mutex); ddi_soft_state_free(pio_softstate, instance); return (DDI_FAILURE); } /* * Map in the data register (register 1) */ if (ddi_regs_map_setup(dip, DATA_REG, (caddr_t *)&(pio_p->data), 0, sizeof (uchar_t), &dev_acc_attr, &pio_p->data_handle) \ != DDI_SUCCESS) { mutex_destroy(&pio_p->mutex); ddi_regs_map_free(&pio_p->csr_handle); ddi_soft_state_free(pio_softstate, instance); return (DDI_FAILURE); }
驱动程序结合使用 ddi_get8(9F) 和 ddi_put8(9F) 系列例程以及 ddi_regs_map_setup(9F) 返回的句柄,以与设备相互传送数据。DDI 框架自动处理为满足主机或设备的字节序格式所需的任何字节交换,并强制实施设备可能具有的任何存储排序约束。
DDI 提供了用于传送 8 位、16 位、32 位和 64 位数据的接口,以及用于重复传送多个值的接口。有关这些接口的完整列表和说明,请参见 ddi_get8(9F)、ddi_put8(9F)、ddi_rep_get8(9F) 和 ddi_rep_put8(9F) 系列例程的手册页。
以下示例建立在示例 7–1 的基础上,其中,驱动程序映射了设备的 CSR 寄存器和数据寄存器。在本示例中,调用驱动程序的 write(9E) 入口点时,会将数据缓冲区写入(每次一个字节)设备。
static int pio_write(dev_t dev, struct uio *uiop, cred_t *credp) { int retval; int error = OK; Pio *pio_p = ddi_get_soft_state(pio_softstate, getminor(dev)); if (pio_p == NULL) return (ENXIO); mutex_enter(&pio_p->mutex); /* * enable interrupts from the device by setting the Interrupt * Enable bit in the devices CSR register */ ddi_put8(pio_p->csr_handle, pio_p->csr, (ddi_get8(pio_p->csr_handle, pio_p->csr) | PIO_INTR_ENABLE)); while (uiop->uio_resid > 0) { /* * This device issues an IDLE interrupt when it is ready * to accept a character; the interrupt can be cleared * by setting PIO_INTR_CLEAR. The interrupt is reasserted * after the next character is written or the next time * PIO_INTR_ENABLE is toggled on. * * wait for interrupt (see pio_intr) */ cv_wait(&pio_p->cv, &pio_p->mutex); /* * get a character from the user's write request * fail the write request if any errors are encountered */ if ((retval = uwritec(uiop)) == -1) { error = retval; break; } /* * pass the character to the device by writing it to * the device's data register */ ddi_put8(pio_p->data_handle, pio_p->data, (uchar_t)retval); } /* * disable interrupts by clearing the Interrupt Enable bit * in the CSR */ ddi_put8(pio_p->csr_handle, pio_p->csr, (ddi_get8(pio_p->csr_handle, pio_p->csr) & ~PIO_INTR_ENABLE)); mutex_exit(&pio_p->mutex); return (error); }
除通过 ddi_get8(9F) 和 ddi_put8(9F) 接口系列实现所有设备访问之外,Solaris OS 还提供特定于特殊总线实现的接口。虽然在某些平台上这些函数会更加有效,但使用这些例程会限制驱动程序在设备的各总线版本间保持可移植的能力。
对于内存映射访问,设备寄存器会出现在内存地址空间中。驱动程序可以将 ddi_getX 系列例程和 ddi_putX 系列用作标准设备访问接口的备用接口。
对于 I/O 空间访问,设备寄存器会出现在 I/O 空间中,其中每个可寻址元素都称为 I/O 端口。驱动程序可以将 ddi_io_get8(9F) 和 ddi_io_put8(9F) 例程用作标准设备访问接口的备用接口。
要在不使用常规设备访问接口的情况下访问 PCI 配置空间,驱动程序需要通过调用 pci_config_setup(9F)(而非 ddi_regs_map_setup(9F))来映射 PCI 配置空间。然后,驱动程序可以调用 pci_config_get8(9F) 和 pci_config_put8(9F) 接口系列,以访问 PCI 配置空间。
本章介绍用于处理中断的机制,如分配、注册、服务以及删除中断。本章介绍有关以下主题的信息:
中断是指设备发送给 CPU 的硬件信号。中断将通知 CPU 需要注意设备,并且 CPU 应该停止任何当前活动并对设备进行响应。如果 CPU 未在执行优先级比中断优先级高的任务,则 CPU 会暂停当前线程。然后,CPU 会调用发送中断信号的设备的中断处理程序。中断处理程序的工作是服务设备并防止此设备中断。中断处理程序返回后,CPU 便会恢复出现中断之前所执行的工作。
Solaris DDI/DKI 提供了用于执行以下任务的接口:
确定中断类型和注册要求
注册中断
服务中断
屏蔽中断
获取中断待处理信息
获取和设置优先级信息
I/O 总线以两种常用方法来实现中断:向量化和轮询。这两种方法通常都会提供总线中断优先级别。向量化设备还会提供中断向量。轮询设备则不提供中断向量。
为了与不断发展的总线技术保持同步,Solaris OS 已经得到了增强,可适应更新类型的中断以及已经使用多年的较为传统的中断。具体来说,操作系统目前可识别三种类型的中断:
传统中断-传统或固定中断是指使用早期总线技术的中断。使用这些技术,可通过一个或多个“带外”(即,独立于总线的主线)连线的外部管脚来发送中断信号。较新的总线技术(如 PCI Express)通过带内机制模拟传统中断来维持软件兼容性。主机 OS 将这些模仿中断视为传统中断。
消息告知中断-消息告知中断 (message-signalled interrupt, MSI) 使用带内消息而不是使用管脚,可在主桥 (host bridge) 中确定中断的地址。(有关主桥 (host bridge) 的更多信息,请参见PCI 局部总线。)MSI 可以将数据与中断消息一起发送。每个 MSI 都不是共享的,这样可以保证指定给某一设备的 MSI 在系统中是唯一的。一个 PCI 函数最多可以请求 32 条 MSI 消息。
扩展消息告知中断-扩展消息告知中断 (Extended message-signalled interrupt, MSI-X) 是 MSI 的增强版本。MSI-X 中断具有以下新增的优点:
支持 2048 条而不是 32 条消息
针对每条消息支持独立的消息地址和消息数据
支持按消息屏蔽
软件分配的向量少于硬件请求的向量时可具有更大灵活性。软件可以在多个 MSI-X 插槽中重用相同的 MSI-X 地址和数据。
一些较新的总线技术(如 PCI Express)要求使用 MSI,但是可以使用 INTx 仿真来处理传统中断。INTx 仿真用于实现兼容性,但是这并不被认为是好的做法。
总线会在总线中断级别设置设备中断的优先级。然后,总线中断级别将映射到处理器中断级别。映射到高于调度程序优先级别的 CPU 中断优先级的总线中断级别称为高级别中断。高级别中断处理程序仅限于调用以下 DDI 接口:
使用与高级别中断关联的中断优先级初始化的互斥锁上的 mutex_enter(9F) 和 mutex_exit(9F)
以下 DDI get 和 put 例程:ddi_get8(9F)、ddi_put8(9F)、ddi_get16(9F)、ddi_put16(9F)、ddi_get32(9F)、ddi_put32(9F)、ddi_get64(9F) 和 ddi_put64(9F)。
总线中断级别本身无法确定设备是否会发生高级别中断。特定的总线中断级别可以在一个平台映射到高级别中断,而在其他平台上则映射到普通中断。
不要求驱动程序来支持具有高级别中断的设备。但是,要求驱动程序检查中断级别。如果中断优先级高于或等于系统最高优先级,中断处理程序会在高级别中断环境下运行。在这种情况下,驱动程序可能无法连接,或者驱动程序可能会使用双级别方案来处理中断。有关更多信息,请参见处理高级别中断 。
系统仅有的有关设备中断的信息为总线中断的优先级别和中断请求编号。例如,SPARC 计算机中 S 总线上的 IPL 即是总线中断的优先级别;x86 计算机中 ISA 总线上的 IRQ 即是中断请求编号。
注册中断处理程序之后,系统会将其添加到每个 IPL 或 IRQ 的潜在中断处理程序的列表中。出现中断时,系统必须确定与给定的 IPL 或 IRQ 关联的所有设备中实际导致此中断的设备。系统会针对指定的 IPL 或 IRQ 调用所有中断处理程序,直到一个处理程序声明中断为止。
以下总线可以支持轮询中断:
S 总线
ISA
PCI
标准 (MSI) 和扩展 (MSI-X) 消息告知中断均作为带内消息实现。消息告知中断可作为使用软件指定的地址和值的写操作进行发送。
常规 PCI 规范包括可选的消息告知中断 (Message Signaled Interrupt, MSI) 支持。MSI 是作为发送的写操作实现的带内消息。MSI 的地址和数据由软件指定,并特定于主桥 (host bridge)。由于消息是带内消息,因此消息的接收可用于“推送”与中断关联的数据。根据定义,MSI 中断是独享的。指定给设备的每条 MSI 消息保证在系统中均为唯一消息。PCI 函数可以请求 1、2、4、8、16 或 32 条 MSI 消息。请注意,系统软件为函数分配的 MSI 消息数可以少于函数所请求的数量。可限制主桥 (host bridge) 中为设备分配的唯一 MSI 消息的数量。
MSI-X 中断是 MSI 中断的增强版本,与 MSI 中断有相同功能,具有以下关键区别:
每个设备最多支持 2048 个 MSI-X 中断向量。
每个中断向量的地址和数据项都是唯一的。
MSI-X 支持按函数屏蔽和按向量屏蔽。
利用 MSI-X 中断,未分配的设备中断向量可以使用先前添加或初始化的 MSI-X 中断向量共享相同的向量地址、向量数据、中断处理程序和处理程序参数。使用 ddi_intr_dup_handler(9F) 函数可相对于关联设备上未分配的中断向量为 Solaris OS 提供的资源设置别名。例如,如果为驱动程序分配了 2 个 MSI-X 中断,并且设备支持 32 个中断,则驱动程序可以使用 ddi_intr_dup_handler() 相对于设备上其他 30 个中断为其收到的 2 个中断设置别名。
ddi_intr_dup_handler() 函数可以复制使用 ddi_intr_add_handler(9F) 添加或使用 ddi_intr_enable(9F) 初始化的中断。
复制的中断最初处于禁用状态。可使用 ddi_intr_enable() 启用复制的中断。您不能删除原始 MSI-X 中断处理程序,除非删除了与此原始中断处理程序相关联的所有复制的中断处理程序。要删除复制的中断处理程序,请首先调用 ddi_intr_disable(9F),然后调用 ddi_intr_free(9F)。当删除与该原始中断处理程序相关联的所有复制的中断处理程序后,就可以使用 ddi_intr_remove_handler(9F) 删除该原始 MSI-X 中断处理程序。有关示例,请参见 ddi_intr_dup_handler (9F) 手册页。
Solaris DDI/DKI 支持软件中断(也称为软中断)。软中断通过软件而不是硬件设备启动。另外,还必须在系统中添加和删除这些中断的处理程序。软中断处理程序在中断上下文中运行,因此可用于执行许多属于中断处理程序的任务。
硬件中断处理程序必须快速执行其任务,因为它们可能必须在执行这些任务的同时暂停其他系统活动。对于高级别中断处理程序,更需要满足此要求,这些处理程序在高于系统调度程序的优先级别上运行。高级别中断处理程序将屏蔽所有较低优先级中断的操作,包括系统时钟的中断操作。因此,该中断处理程序必须避免涉及到可能导致其休眠的活动,如获取互斥锁。
如果处理程序休眠,则系统可能会挂起,因为时钟会被屏蔽,从而无法调度休眠线程。因此,高级别中断处理程序通常在高优先级别执行最少量的工作,并将其他任务委托给运行优先级别低于高级别中断处理程序的软件中断。由于软件中断处理程序运行的优先级别低于系统调度程序,因此软件中断处理程序可以执行高级别中断处理程序无法执行的操作。
Solaris OS 提供了用于注册和取消注册中断的框架,并且提供了对消息告知中断 (Message Signaled Interrupt, MSI) 的支持。通过中断管理界面,可以处理优先级、功能和中断屏蔽,并可获取待处理信息。
返回可用于指定硬件设备和中断类型的中断的数量。
返回设备支持的指定中断类型的中断的数量。
返回设备和主机均支持的硬件中断类型。
针对指定的中断返回中断功能标志。
为指定类型的中断分配系统资源和中断向量。
针对指定的中断句柄释放系统资源和中断向量。
通过使用 DDI_INTR_FLAG_LEVEL 和 DDI_INTR_FLAG_EDGE 标志来设置指定中断的功能。
添加中断处理程序。
仅适用于 MSI-X。将分配的中断向量的地址和数据对复制到同一设备上未使用的中断向量。
删除指定的中断处理程序。
启用指定的中断。
禁用指定的中断。
仅用于 MSI。启用指定范围的中断。
仅用于 MSI。禁用指定范围的中断。
如果已启用指定的中断,则设置中断屏蔽码。
如果已启用指定的中断,则清除中断屏蔽码。
如果主桥 (host bridge) 或设备支持这种中断待处理位,则读取此位。
返回指定中断的当前软件优先级设置。
设置指定中断的中断优先级别。
返回高级别中断的最低优先级别。
添加软中断处理程序。
触发指定的软中断。
删除指定的软中断处理程序。
返回指定中断的软中断优先级。
更改指定软中断的相对软中断优先级。
更改软中断优先级
检查待处理中断
设置中断屏蔽码
清除中断屏蔽码
使用 ddi_intr_set_softint_pri(9F) 函数将软中断优先级到更改为 9。
if (ddi_intr_set_softint_pri(mydev->mydev_softint_hdl, 9) != DDI_SUCCESS) cmn_err (CE_WARN, "ddi_intr_set_softint_pri failed");
使用 ddi_intr_get_pending(9F) 函数检查中断是否处于待处理状态。
if (ddi_intr_get_pending(mydevp->htable[0], &pending) != DDI_SUCCESS) cmn_err(CE_WARN, "ddi_intr_get_pending() failed"); else if (pending) cmn_err(CE_NOTE, "ddi_intr_get_pending(): Interrupt pending");
使用 ddi_intr_set_mask(9F) 函数设置中断屏蔽,以防止设备收到中断。
if ((ddi_intr_set_mask(mydevp->htable[0]) != DDI_SUCCESS)) cmn_err(CE_WARN, "ddi_intr_set_mask() failed");
使用 ddi_intr_clr_mask(9F) 函数清除中断屏蔽。如果没有启用指定的中断,ddi_intr_clr_mask(9F) 函数将失败。如果 ddi_intr_clr_mask(9F ) 函数成功,则设备将开始生成中断。
if (ddi_intr_clr_mask(mydevp->htable[0]) != DDI_SUCCESS) cmn_err(CE_WARN, "ddi_intr_clr_mask() failed");
设备驱动程序必须首先通过调用 ddi_intr_add_handler(9F) 向系统注册中断处理程序,然后才能接收和服务中断。注册中断处理程序会为系统提供一种将中断处理程序与中断规范相关联的方法。如果设备可能负责中断,则会调用中断处理程序。此处理程序负责确定其是否应处理中断,如果是,则负责声明该中断。
可在 mdb 或 kmdb 调试器中使用 ::interrupts 命令检索支持的 SPARC 和 x86 系统上设备的已注册中断信息。
要注册驱动程序的中断处理程序,驱动程序通常会在其 attach(9E) 入口点执行以下步骤。
使用 ddi_intr_get_supported_types(9F) 确定支持的中断类型。
使用 ddi_intr_get_nintrs(9F) 确定支持的中断类型的数量。
使用 kmem_zalloc(9F) 为 DDI 中断句柄分配内存。
对于分配的每个中断类型,执行以下步骤:
使用 ddi_intr_get_pri(9F) 获取中断的优先级。
如果需要为中断设置新的优先级,请使用 ddi_intr_set_pri(9F)。
使用 mutex_init(9F) 将锁初始化。
使用 ddi_intr_add_handler(9F) 注册中断的处理程序。
使用 ddi_intr_enable(9F) 启用中断。
执行以下步骤以释放每个中断:
使用 ddi_intr_disable(9F) 禁用每个中断。
使用 ddi_intr_remove_handler(9F) 删除中断处理程序。
使用 mutex_destroy(9F) 删除锁。
使用 ddi_intr_free(9F) 和 kmem_free(9F) 释放中断,从而释放为 DDI 中断句柄分配的内存。
以下示例说明如何为名为 mydev 的设备安装中断处理程序。此示例假设 mydev 仅支持一个中断。
/* Determine which types of interrupts supported */ ret = ddi_intr_get_supported_types(mydevp->mydev_dip, &type); if ((ret != DDI_SUCCESS) || (!(type & DDI_INTR_TYPE_FIXED))) { cmn_err(CE_WARN, "Fixed type interrupt is not supported"); return (DDI_FAILURE); } /* Determine number of supported interrupts */ ret = ddi_intr_get_nintrs(mydevp->mydev_dip, DDI_INTR_TYPE_FIXED, &count); /* * Fixed interrupts can only have one interrupt. Check to make * sure that number of supported interrupts and number of * available interrupts are both equal to 1. */ if ((ret != DDI_SUCCESS) || (count != 1)) { cmn_err(CE_WARN, "No fixed interrupts"); return (DDI_FAILURE); } /* Allocate memory for DDI interrupt handles */ mydevp->mydev_htable = kmem_zalloc(sizeof (ddi_intr_handle_t), KM_SLEEP); ret = ddi_intr_alloc(mydevp->mydev_dip, mydevp->mydev_htable, DDI_INTR_TYPE_FIXED, 0, count, &actual, 0); if ((ret != DDI_SUCCESS) || (actual != 1)) { cmn_err(CE_WARN, "ddi_intr_alloc() failed 0x%x", ret); kmem_free(mydevp->mydev_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } /* Sanity check that count and available are the same. */ ASSERT(count == actual); /* Get the priority of the interrupt */ if (ddi_intr_get_pri(mydevp->mydev_htable[0], &mydevp->mydev_intr_pri)) { cmn_err(CE_WARN, "ddi_intr_alloc() failed 0x%x", ret); (void) ddi_intr_free(mydevp->mydev_htable[0]); kmem_free(mydevp->mydev_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } cmn_err(CE_NOTE, "Supported Interrupt pri = 0x%x", mydevp->mydev_intr_pri); /* Test for high level mutex */ if (mydevp->mydev_intr_pri >= ddi_intr_get_hilevel_pri()) { cmn_err(CE_WARN, "Hi level interrupt not supported"); (void) ddi_intr_free(mydevp->mydev_htable[0]); kmem_free(mydevp->mydev_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } /* Initialize the mutex */ mutex_init(&mydevp->mydev_int_mutex, NULL, MUTEX_DRIVER, DDI_INTR_PRI(mydevp->mydev_intr_pri)); /* Register the interrupt handler */ if (ddi_intr_add_handler(mydevp->mydev_htable[0], mydev_intr, (caddr_t)mydevp, NULL) !=DDI_SUCCESS) { cmn_err(CE_WARN, "ddi_intr_add_handler() failed"); mutex_destroy(&mydevp->mydev_int_mutex); (void) ddi_intr_free(mydevp->mydev_htable[0]); kmem_free(mydevp->mydev_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } /* Enable the interrupt */ if (ddi_intr_enable(mydevp->mydev_htable[0]) != DDI_SUCCESS) { cmn_err(CE_WARN, "ddi_intr_enable() failed"); (void) ddi_intr_remove_handler(mydevp->mydev_htable[0]); mutex_destroy(&mydevp->mydev_int_mutex); (void) ddi_intr_free(mydevp->mydev_htable[0]); kmem_free(mydevp->mydev_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } return (DDI_SUCCESS); }
以下示例说明如何删除传统中断。
/* disable interrupt */ (void) ddi_intr_disable(mydevp->mydev_htable[0]); /* Remove interrupt handler */ (void) ddi_intr_remove_handler(mydevp->mydev_htable[0]); /* free interrupt handle */ (void) ddi_intr_free(mydevp->mydev_htable[0]); /* free memory */ kmem_free(mydevp->mydev_htable, sizeof (ddi_intr_handle_t));
要注册驱动程序的中断处理程序,驱动程序通常会在其 attach(9E) 入口点执行以下步骤。
使用 ddi_intr_get_supported_types(9F) 确定支持的中断类型。
使用 ddi_intr_get_nintrs(9F) 确定支持的 MSI 中断类型的数量。
使用 ddi_intr_alloc(9F) 为 MSI 中断分配内存。
对于分配的每个中断类型,执行以下步骤:
使用 ddi_intr_get_pri(9F) 获取中断的优先级。
如果需要为中断设置新的优先级,请使用 ddi_intr_set_pri(9F)。
使用 mutex_init(9F) 将锁初始化。
使用 ddi_intr_add_handler(9F) 注册中断的处理程序。
使用以下函数之一启用所有中断:
使用 ddi_intr_block_enable(9F) 启用某个块中的所有中断。
在循环中使用 ddi_intr_enable(9F) 单独启用每个中断。
以下示例说明如何为名为 mydev 的设备注册 MSI 中断。
/* Get supported interrupt types */ if (ddi_intr_get_supported_types(devinfo, &intr_types) != DDI_SUCCESS) { cmn_err(CE_WARN, "ddi_intr_get_supported_types failed"); goto attach_fail; } if (intr_types & DDI_INTR_TYPE_MSI) mydev_add_msi_intrs(mydevp); /* Check count, available and actual interrupts */ static int mydev_add_msi_intrs(mydev_t *mydevp) { dev_info_t *devinfo = mydevp->devinfo; int count, avail, actual; int x, y, rc, inum = 0; /* Get number of interrupts */ rc = ddi_intr_get_nintrs(devinfo, DDI_INTR_TYPE_MSI, &count); if ((rc != DDI_SUCCESS) || (count == 0)) { cmn_err(CE_WARN, "ddi_intr_get_nintrs() failure, rc: %d, " "count: %d", rc, count); return (DDI_FAILURE); } /* Get number of available interrupts */ rc = ddi_intr_get_navail(devinfo, DDI_INTR_TYPE_MSI, &avail); if ((rc != DDI_SUCCESS) || (avail == 0)) { cmn_err(CE_WARN, "ddi_intr_get_navail() failure, " "rc: %d, avail: %d\n", rc, avail); return (DDI_FAILURE); } if (avail < count) { cmn_err(CE_NOTE, "nitrs() returned %d, navail returned %d", count, avail); } /* Allocate memory for MSI interrupts */ mydevp->intr_size = count * sizeof (ddi_intr_handle_t); mydevp->htable = kmem_alloc(mydevp->intr_size, KM_SLEEP); rc = ddi_intr_alloc(devinfo, mydevp->htable, DDI_INTR_TYPE_MSI, inum, count, &actual, DDI_INTR_ALLOC_NORMAL); if ((rc != DDI_SUCCESS) || (actual == 0)) { cmn_err(CE_WARN, "ddi_intr_alloc() failed: %d", rc); kmem_free(mydevp->htable, mydevp->intr_size); return (DDI_FAILURE); } if (actual < count) { cmn_err(CE_NOTE, "Requested: %d, Received: %d", count, actual); } mydevp->intr_cnt = actual; /* * Get priority for first msi, assume remaining are all the same */ if (ddi_intr_get_pri(mydevp->htable[0], &mydev->intr_pri) != DDI_SUCCESS) { cmn_err(CE_WARN, "ddi_intr_get_pri() failed"); /* Free already allocated intr */ for (y = 0; y < actual; y++) { (void) ddi_intr_free(mydevp->htable[y]); } kmem_free(mydevp->htable, mydevp->intr_size); return (DDI_FAILURE); } /* Call ddi_intr_add_handler() */ for (x = 0; x < actual; x++) { if (ddi_intr_add_handler(mydevp->htable[x], mydev_intr, (caddr_t)mydevp, NULL) != DDI_SUCCESS) { cmn_err(CE_WARN, "ddi_intr_add_handler() failed"); /* Free already allocated intr */ for (y = 0; y < actual; y++) { (void) ddi_intr_free(mydevp->htable[y]); } kmem_free(mydevp->htable, mydevp->intr_size); return (DDI_FAILURE); } } (void) ddi_intr_get_cap(mydevp->htable[0], &mydevp->intr_cap); if (mydev->m_intr_cap & DDI_INTR_FLAG_BLOCK) { /* Call ddi_intr_block_enable() for MSI */ (void) ddi_intr_block_enable(mydev->m_htable, mydev->m_intr_cnt); } else { /* Call ddi_intr_enable() for MSI non block enable */ for (x = 0; x < mydev->m_intr_cnt; x++) { (void) ddi_intr_enable(mydev->m_htable[x]); } } return (DDI_SUCCESS); }
以下示例说明如何删除 MSI 中断。
static void mydev_rem_intrs(mydev_t *mydev) { int x; /* Disable all interrupts */ if (mydev->m_intr_cap & DDI_INTR_FLAG_BLOCK) { /* Call ddi_intr_block_disable() */ (void) ddi_intr_block_disable(mydev->m_htable, mydev->m_intr_cnt); } else { for (x = 0; x < mydev->m_intr_cnt; x++) { (void) ddi_intr_disable(mydev->m_htable[x]); } } /* Call ddi_intr_remove_handler() */ for (x = 0; x < mydev->m_intr_cnt; x++) { (void) ddi_intr_remove_handler(mydev->m_htable[x]); (void) ddi_intr_free(mydev->m_htable[x]); } kmem_free(mydev->m_htable, mydev->m_intr_size); }
本节介绍可以生成多种不同可中断条件的设备驱动程序如何利用中断资源管理功能来优化其中断向量的分配。
中断资源管理功能可动态管理驱动程序的中断配置,从而使设备驱动程序能够使用多个中断资源。未使用中断资源管理功能时,中断处理的配置通常仅在驱动程序的 attach (9E) 例程内进行。中断资源管理功能监控系统更改,根据这些更改重新计算分配给各设备的中断向量数量,并向各受影响的参与驱动程序发出关于驱动程序新中断向量分配情况的通知。参与驱动程序是注册了回调处理程序的驱动程序,如回调接口中所述。可能导致中断向量重新分配的更改包括添加或删除设备,或者显式请求,如修改所请求的中断向量数量中所述。
中断资源管理功能在各 Solaris 平台上不可用。此功能仅对利用 MSI-X 中断的 PCIe 设备可用。
利用中断资源管理功能的驱动程序必须能够在该功能不可用时正确做出调整。
中断资源管理功能可用时,可使驱动程序访问更多的中断向量,超过可通过其他方式为此驱动程序分配的数量。驱动程序在利用更多中断向量时可以更有效地处理中断条件。
中断资源管理功能根据以下约束动态调整分配给各参与驱动程序的中断向量数量:
可用总数。系统中存在的限定数量的中断向量。
请求的总数。可为驱动程序分配较少的中断向量,但始终不能超过所请求的中断向量数量。
对其他驱动程序公平。多个驱动程序以与各驱动程序请求的中断向量总数相关的方式共享可用中断向量的总数。
在任何给定时间为一个设备提供的可用中断向量的数量可能会有所不同:
在系统中动态添加或删除其他设备
驱动程序根据负荷动态更改所请求中断向量的数量
驱动程序必须提供以下支持,以利用中断资源管理功能:
回调支持。驱动程序必须注册回调处理程序,从而在系统改变其可用中断数量时获得通知。驱动程序必须能够增加或减少其使用的中断。
中断请求。驱动程序必须指定需要使用的中断数量。
中断用法。在任何时候,驱动程序都必须请求正确数量的中断,基于:
其硬件能生成哪些可中断条件
有多少处理器可用于并行处理那些条件
中断灵活性。驱动程序必须足够灵活,能够以最适合各中断向量的当前可用中断数量的方式为其分配一个或多个可中断的条件。在可用中断的数量增加或减少时,驱动程序可能需要随时重新配置这些分配。
驱动程序必须使用以下接口来注册回调支持。
表 8–1 回调支持接口
接口 |
数据结构 |
说明 |
---|---|---|
ddi_cb_register() |
ddi_cb_flags_t、ddi_cb_handle_t |
注册回调处理程序函数,以接收特定类型的操作。 |
ddi_cb_unregister() |
ddi_cb_handle_t |
取消注册回调处理程序函数。 |
(*ddi_cb_func_t)() |
ddi_cb_action_t |
接收回调操作和与要处理的各操作相关的特定参数。 |
使用 ddi_cb_register(9F) 函数为驱动程序注册回调处理程序函数。
int ddi_cb_register (dev_info_t *dip, ddi_cb_flags_t cbflags, ddi_cb_func_t cbfunc, void *arg1, void *arg2, ddi_cb_handle_t *ret_hdlp);
驱动程序仅可注册一个回调函数。这是用于处理所有独立回调操作的回调函数。cbflags 参数确定驱动程序应在发生哪些类型的操作时接收这些操作。cbfunc() 例程将在驱动程序应处理相关操作时调用。在每次执行 cbfunc() 例程时,驱动程序都会指定应发送给其本身的两个专用参数(arg1 和 arg2)。
cbflags() 参数属于枚举类型,指定驱动程序支持哪些操作。
typedef enum { DDI_CB_FLAG_INTR } ddi_cb_flags_t;
为了注册对中断资源管理操作的支持,驱动程序必须注册处理程序,并包含 DDI_CB_FLAG_INTR 标志。 回调处理程序成功注册后,将通过 ret_hdlp 参数返回一个不透明的句柄。驱动程序使用完回调处理程序之后,驱动程序可使用 ret_hdlp 参数取消注册此回调处理程序。
在驱动程序的 attach(9F) 入口点中注册回调处理程序。在驱动程序的软状态中保存不透明的句柄。在驱动程序的 detach(9F) 入口点中取消注册回调处理程序。
使用 ddi_cb_unregister(9F) 函数为驱动程序取消注册回调处理程序函数。
int ddi_cb_unregister (ddi_cb_handle_t hdl);
在驱动程序的 detach(9F) 入口点中执行此调用。在此调用之后,驱动程序将不再接收回调操作。
驱动程序也会失去因拥有注册的回调处理函数而从系统中获得的其他所有支持。例如,此前为驱动程序提供的某些中断向量会在取消注册其回调处理函数后立即收回。成功返回之前,ddi_cb_unregister() 函数会通知驱动程序由于系统支持缺失所导致的任何最终操作。
使用注册的回调处理函数来接收回调操作,接收特定于要处理的各操作的参数。
typedef int (*ddi_cb_func_t)(dev_info_t *dip, ddi_cb_action_t cbaction, void *cbarg, void *arg1, void *arg2);
cbaction 参数指定驱动程序接收到的回调要处理哪种操作。
typedef enum { DDI_CB_INTR_ADD, DDI_CB_INTR_REMOVE } ddi_cb_action_t;
DDI_CB_INTR_ADD 操作表示驱动程序中断可用数量增加。DDI_CB_INTR_REMOVE 操作表示驱动程序中断可用数量减少。将 cbarg 参数的类型强制转换为 int,以确定添加或删除的中断数量。cbarg 值表示可用中断数量的变化。
例如,获得可用中断数量的变化:
count = (int)(uintptr_t)cbarg;
如果 cbaction 为 DDI_CB_INT _ADD,则应添加 cbarg 数量的中断向量。如果 cbaction 为 DDI_CB_INT _REMOVE,则应释放 cbarg 数量的中断向量。
有关 arg1 和 arg2 的说明, 请参见 ddi_cb_registe (9F)。
回调处理函数必须能够在函数注册的整个时间段内正确执行。回调函数不能依赖任何可能会在回调函数成功取消注册之前销毁的数据结构。
回调处理函数必须返回以下值之一:
DDI_SUCCESS(如果正确处理了操作)
DDI_FAILURE(如果遇到内部错误)
DDI_ENOTSUP(如果接收到无法识别的操作)
驱动程序必须使用以下接口从系统请求中断向量。
表 8–2 中断向量请求接口
接口 |
数据结构 |
说明 |
---|---|---|
ddi_intr_alloc() |
ddi_intr_handle_t |
分配中断。 |
ddi_intr_set_nreq() |
更改所请求的中断向量数量。 |
使用 ddi_intr_alloc(9F) 函数来初始分配中断。
int ddi_intr_alloc (dev_info_t *dip, ddi_intr_handle_t *h_array, int type, int inum, int count, int *actualp, int behavior);
调用此函数之前,驱动程序必须分配一个可包含所请求中断数量的足够大的空句柄数组。ddi_intr_alloc() 函数会尝试分配 count 数量的中断句柄,并使用以 inum 参数指定的偏移开头的指定中断向量初始化数组。 actualp 参数返回所分配的中断向量的实际数量。
驱动程序可通过两种方式使用 ddi_intr_alloc() 函数:
驱动程序可以在单独的步骤中多次调用 ddi_intr_alloc() 函数,为中断句柄数组中的各成员分配中断向量。
驱动程序可以一次性地调用 ddi_intr_alloc() 函数,一次为设备分配所有中断向量。
如果您正在使用中断资源管理功能,则调用一次 ddi_intr_alloc() 即可分配所有中断向量。count 参数是驱动程序请求的中断向量的总数。如果 actualp 中的值小于 count 值,则系统无法完全满足请求。中断资源管理功能将保存此请求(count 转为·nreq - 请参见下文),稍后还可能会为此驱动程序分配更多中断向量。
使用中断资源管理功能时,对 ddi_intr_alloc() 的其他调用不会更改请求的中断向量总数。使用 ddi_intr_set_nreq(9F) 函数更改请求的中断向量数量。
使用 ddi_intr_set_nreq(9F) 函数修改请求的中断向量数量。
int ddi_intr_set_nreq (dev_info_t *dip, int nreq);
中断资源管理功能可用时,驱动程序可使用 ddi_intr_set_nreq() 函数动态调整请求的中断向量总数。附加了驱动程序之后,驱动程序可能会以此响应存在的实际负载。
驱动程序必须首先调用 ddi_intr_alloc(9F) 来请求初始数量的中断向量。完成 ddi_intr_alloc() 调用之后,驱动程序可随时调用 ddi_intr_set_nreq() 来更改请求的大小。指定的 nreq 值是驱动程序请求的中断向量的新总数。中断资源管理功能可能会根据这个新请求重新平衡系统中分配给各驱动程序的中断数量。只要中断资源管理功能重新平衡了分配给驱动程序的中断数量,各受影响的驱动程序就会接收到该驱动程序可以使用的中断向量增加或减少的回调通知。
例如,如果驱动程序将中断与其处理的特定事务并行使用,则可动态调整所请求中断向量的总数。存储驱动程序必须将 DMA 引擎与各进行中的事务相关联,因而需要中断向量。驱动程序可在 open(9F) 和 close(9F) 例程中调用 ddi_intr_set_nreq(),以便根据驱动程序的实际使用情况调整中断使用的比例。
支持多种不同可中断条件的设备的驱动程序必须能够将这些条件映射到任意数量的中断向量。驱动程序不能假设已经分配的中断向量仍然可用。某些当前可用中断稍后可能会被系统收回,以满足系统中其他驱动程序的需求。
驱动程序必须能够:
确定其硬件支持的中断数量。
确定适合使用的中断数量。例如,系统中的处理器总数可能会影响此项评估。
随时将所需中断的数量与可用中断的数量相比较。
总而言之,驱动程序必须能够选择一系列中断处理函数,并为其硬件编程,使之能够根据需求和中断可用性生成中断。在某些情况下,可能有多个针对同一个向量的中断,该中断向量的中断处理程序必须确定发生的是哪个中断。驱动程序将中断映射到中断向量的情况可能会影响设备性能。
网络设备驱动程序是中断资源管理的一种理想的备选设备驱动程序类型。网络设备硬件支持多个传输和接收信道。
网络设备在一个接收信道接收到包或者在一个传输信道传输包时,设备将生成唯一的中断条件。硬件可以为可能发生的各事件发送特定的 MSI-X 中断。硬件中的表格确定为各事件生成哪个 MSI-X 中断。
为了优化性能,驱动程序会向系统请求足够多的中断,以使每个中断都有自己的中断向量。在 attach(9F) 例程中初次调用 ddi_intr_alloc(9F) 时,驱动程序会发出此请求。
随后,驱动程序评估通过 actualp 中的 ddi_intr_alloc() 接收到的实际中断数量。 可能会接收到所请求的全部中断,也可能会接收到较少的中断。
驱动程序内的独立函数使用可用中断总数计算为各事件生成哪些 MSI-X 中断。此函数会相应在硬件中对该表进行编程。
如果驱动程序接收了全部请求的中断向量,硬件表中的每个条目都将有自己唯一的 MSI-X 中断。中断条件和中断向量之间存在一对一的映射。硬件为每种类型的事件生成唯一的 MSI-X 中断。
如果驱动程序可用的中断向量更少,则硬件表中必定要多次出现某些 MSI-X 中断数。硬件为多种类型的事件生成相同的 MSI-X 中断。
驱动程序应有两个不同的中断处理程序函数。
一个中断处理程序执行特定任务来响应一项中断。这个简单的函数仅可处理由一种可能硬件事件所生成的中断。
第二个中断处理程序更为复杂。此函数用于处理有多个中断映射到同一个 MSI-X 中断向量的情况。
在这一部分的示例驱动程序中,xx_setup_interrupts() 函数使用可用中断向量的数量来为硬件编程,并为其中每一个中断向量调用相应的中断处理程序。将在两个位置调用 xx_setup_interrupts() 函数:在 xx_attach() 中调用 di_intr_alloc() 之后,在 xx_cbfunc() 回调处理程序函数中调整了中断向量分配之后。
int xx_setup_interrupts(xx_state_t *statep, int navail, xx_intrs_t *xx_intrs_p);
xx_setup_interrupts() 函数是通过 xx_intrs_t 数据结构的数组调用的。
typedef struct { ddi_intr_handler_t inthandler; void *arg1; void *arg2; } xx_intrs_t;
无论中断资源管理功能是否可用,驱动程序中都必须存在这种 xx_setup_interrupts() 功能。驱动程序必须能够在中断向量少于附加过程中请求的数量时正常工作。如果中断资源管理功能可用,您就可以修改驱动程序,使其动态适应新的可用中断向量数量。
驱动程序必须独立于中断资源管理功能的可用性提供的其他功能,包括停止硬件和恢复硬件的能力。某些与电源管理和热插拔相关的事件需要停止和恢复。在处理中断回调操作时也必须利用停止和恢复。
在 xx_detach() 中调用了停止函数。
int xx_quiesce(xx_state_t *statep);
在 xx_attach() 中调用了恢复函数。
int xx_resume(xx_state_t *statep);
通过以下修改增强此设备驱动程序,使其使用中断资源管理功能:
注册回调处理程序。驱动程序必须为指明何时将有更少或更多的中断可用的操作注册。
处理回调。驱动程序必须停止其硬件、重新编程中断处理、恢复硬件以响应各个此类回调操作。
/* * attach(9F) routine. * * Creates soft state, registers callback handler, initializes * hardware, and sets up interrupt handling for the driver. */ xx_attach(dev_info_t *dip, ddi_attach_cmd_t cmd) { xx_state_t *statep = NULL; xx_intr_t *intrs = NULL; ddi_intr_handle_t *hdls; ddi_cb_handle_t cb_hdl; int instance; int type; int types; int nintrs; int nactual; int inum; /* Get device instance */ instance = ddi_get_instance(dip); switch (cmd) { case DDI_ATTACH: /* Get soft state */ if (ddi_soft_state_zalloc(state_list, instance) != 0) return (DDI_FAILURE); statep = ddi_get_soft_state(state_list, instance); ddi_set_driver_private(dip, (caddr_t)statep); statep->dip = dip; /* Initialize hardware */ xx_initialize(statep); /* Register callback handler */ if (ddi_cb_register(dip, DDI_CB_FLAG_INTR, xx_cbfunc, statep, NULL, &cb_hdl) != 0) { ddi_soft_state_free(state_list, instance); return (DDI_FAILURE); } statep->cb_hdl = cb_hdl; /* Select interrupt type */ ddi_intr_get_supported_types(dip, &types); if (types & DDI_INTR_TYPE_MSIX) { type = DDI_INTR_TYPE_MSIX; } else if (types & DDI_INTR_TYPE_MSI) { type = DDI_INTR_TYPE_MSI; } else { type = DDI_INTR_TYPE_FIXED; } statep->type = type; /* Get number of supported interrupts */ ddi_intr_get_nintrs(dip, type, &nintrs); /* Allocate interrupt handle array */ statep->hdls_size = nintrs * sizeof (ddi_intr_handle_t); statep->hdls = kmem_zalloc(statep->hdls_size, KMEM_SLEEP); /* Allocate interrupt setup array */ statep->intrs_size = nintrs * sizeof (xx_intr_t); statep->intrs = kmem_zalloc(statep->intrs_size, KMEM_SLEEP); /* Allocate interrupt vectors */ ddi_intr_alloc(dip, hdls, type, 0, nintrs, &nactual, 0); statep->nactual = nactual; /* Configure interrupt handling */ xx_setup_interrupts(statep, statep->nactual, statep->intrs); /* Install and enable interrupt handlers */ for (inum = 0; inum < nactual; inum++) { ddi_intr_add_handler(&hdls[inum], intrs[inum].inthandler, intrs[inum].arg1, intrs[inum].arg2); ddi_intr_enable(hdls[inum]); } break; case DDI_RESUME: /* Get soft state */ statep = ddi_get_soft_state(state_list, instance); if (statep == NULL) return (DDI_FAILURE); /* Resume hardware */ xx_resume(statep); break; } return (DDI_SUCESS); } /* * detach(9F) routine. * * Stops the hardware, disables interrupt handling, unregisters * a callback handler, and destroys the soft state for the driver. */ xx_detach(dev_info_t *dip, ddi_detach_cmd_t cmd) { xx_state_t *statep = NULL; int instance; int inum; /* Get device instance */ instance = ddi_get_instance(dip); switch (cmd) { case DDI_DETACH: /* Get soft state */ statep = ddi_get_soft_state(state_list, instance); if (statep == NULL) return (DDI_FAILURE); /* Stop device */ xx_uninitialize(statep); /* Disable and free interrupts */ for (inum = 0; inum < statep->nactual; inum++) { ddi_intr_disable(statep->hdls[inum]); ddi_intr_remove_handler(statep->hdls[inum]); ddi_intr_free(statep->hdls[inum]); } /* Unregister callback handler */ ddi_cb_unregister(statep->cb_hdl); /* Free interrupt handle array */ kmem_free(statep->hdls, statep->hdls_size); /* Free interrupt setup array */ kmem_free(statep->intrs, statep->intrs_size); /* Free soft state */ ddi_soft_state_free(state_list, instance); break; case DDI_SUSPEND: /* Get soft state */ statep = ddi_get_soft_state(state_list, instance); if (statep == NULL) return (DDI_FAILURE); /* Suspend hardware */ xx_quiesce(statep); break; } return (DDI_SUCCESS); } /* * (*ddi_cbfunc)() routine. * * Adapt interrupt usage when availability changes. */ int xx_cbfunc(dev_info_t *dip, ddi_cb_action_t cbaction, void *cbarg, void *arg1, void *arg2) { xx_state_t *statep = (xx_state_t *)arg1; int count; int inum; int nactual; switch (cbaction) { case DDI_CB_INTR_ADD: case DDI_CB_INTR_REMOVE: /* Get change in availability */ count = (int)(uintptr_t)cbarg; /* Suspend hardware */ xx_quiesce(statep); /* Tear down previous interrupt handling */ for (inum = 0; inum < statep->nactual; inum++) { ddi_intr_disable(statep->hdls[inum]); ddi_intr_remove_handler(statep->hdls[inum]); } /* Adjust interrupt vector allocations */ if (cbaction == DDI_CB_INTR_ADD) { /* Allocate additional interrupt vectors */ ddi_intr_alloc(dip, statep->hdls, statep->type, statep->nactual, count, &nactual, 0); /* Update actual count of available interrupts */ statep->nactual += nactual; } else { /* Free removed interrupt vectors */ for (inum = statep->nactual - count; inum < statep->nactual; inum++) { ddi_intr_free(statep->hdls[inum]); } /* Update actual count of available interrupts */ statep->nactual -= count; } /* Configure interrupt handling */ xx_setup_interrupts(statep, statep->nactual, statep->intrs); /* Install and enable interrupt handlers */ for (inum = 0; inum < statep->nactual; inum++) { ddi_intr_add_handler(&statep->hdls[inum], statep->intrs[inum].inthandler, statep->intrs[inum].arg1, statep->intrs[inum].arg2); ddi_intr_enable(statep->hdls[inum]); } /* Resume hardware */ xx_resume(statep); break; default: return (DDI_ENOTSUP); } return (DDI_SUCCESS); }
驱动程序框架和设备各自将要求置于中断处理程序上。所有中断处理程序均要求执行以下任务:
中断处理程序首先会检查设备,确定其是否发出了中断。如果设备未发出中断,则处理程序必须返回 DDI_INTR_UNCLAIMED。通过此步骤可实现设备轮询。在给定中断优先级别的任何设备都可能发出了中断。设备轮询将通知系统此设备是否已发出了中断。
通知设备正在对其进行服务。
通知设备服务是大多数设备所需的特定于设备的操作。例如,需要将 S 总线设备中断,直到驱动程序通知 S 总线设备停止。此方法可保证对在同一优先级别中断的所有 S 总线设备都进行服务。
执行任何与 I/O 请求有关的处理。
设备会由于不同原因而发生中断,如传送完成或传送错误。此步骤可涉及使用数据访问函数来读取设备的数据缓冲区,检查设备的错误寄存器,以及在数据结构中相应地设置状态字段。中断分发和处理相对比较耗时。
执行可以防止其他中断的任何附加处理。
例如,从设备中读取数据的下一项。
必须始终声明 MSI 中断。
对于 MSI-X 中断,声明中断是可选的。在任一情况下都无需检查中断的拥有权,因为 MSI 和 MSI-X 中断不是与其他设备共享的。
支持热插拔和多个 MSI 或 MSI-X 中断的驱动程序应针对热插拔事件保留单独的中断,并针对此中断注册单独的 ISR(interrupt service routine,中断服务例程)。
static uint_t mydev_intr(caddr_t arg1, caddr_t arg2) { struct mydevstate *xsp = (struct mydevstate *)arg1; uint8_t status; volatile uint8_t temp; /* * Claim or reject the interrupt.This example assumes * that the device's CSR includes this information. */ mutex_enter(&xsp->high_mu); /* use data access routines to read status */ status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr); if (!(status & INTERRUPTING)) { mutex_exit(&xsp->high_mu); return (DDI_INTR_UNCLAIMED); /* dev not interrupting */ } /* * Inform the device that it is being serviced, and re-enable * interrupts. The example assumes that writing to the * CSR accomplishes this. The driver must ensure that this data * access operation makes it to the device before the interrupt * service routine returns. For example, using the data access * functions to read the CSR, if it does not result in unwanted * effects, can ensure this. */ ddi_put8(xsp->data_access_handle, &xsp->regp->csr, CLEAR_INTERRUPT | ENABLE_INTERRUPTS); /* flush store buffers */ temp = ddi_get8(xsp->data_access_handle, &xsp->regp->csr); mutex_exit(&xsp->mu); return (DDI_INTR_CLAIMED); }
中断例程执行的大多数步骤都依赖于设备本身的特定信息。查询设备的硬件手册可确定中断原因,检测错误状态并访问设备数据寄存器。
高级别中断是指中断在调度程序级别或更高级别的那类中断。此级别不允许运行调度程序。因此,调度程序无法抢占高级别中断处理程序。高级别中断不会因为调度程序而阻塞。高级别中断只能使用互斥锁进行锁定。
驱动程序必须确定设备是否在使用高级别中断。注册中断时,请在驱动程序的 attach(9E) 入口点进行此测试。请参见高级别中断处理示例。
如果从 ddi_intr_get_pri(9F) 返回的中断优先级高于或等于从 ddi_intr_get_hilevel_pri(9F) 返回的优先级,则表明驱动程序无法连接,或者驱动程序可能实现高级别的中断处理程序。高级别的中断处理程序可使用优先级较低的软件中断来处理该设备。要允许更大的并发性,请使用单独的互斥锁来防止高级中断处理程序使用数据。
如果从 ddi_intr_get_pri(9F) 返回的中断优先级低于从 ddi_intr_get_hilevel_pri(9F) 返回的优先级,则 attach(9E) 入口点将进行常规中断注册。在这种情况下不需要软中断。
使用表示高级别中断的中断优先级初始化的互斥锁称为高级互斥锁。虽然持有高级互斥锁,但是驱动程序仍会受到与高级别中断处理程序相同的限制。
在以下示例中,高级互斥锁 (xsp->high_mu) 仅用于保护在高级别中断处理程序和软中断处理程序之间共享的数据。受保护的数据包括高级别中断处理程序和低级处理程序使用的队列,以及用于指示低级处理程序正在运行的标志。单独的低级互斥锁 (xsp->low_mu) 可防止软中断处理程序使用驱动程序的其余部分。
static int mydevattach(dev_info_t *dip, ddi_attach_cmd_t cmd) { struct mydevstate *xsp; /* ... */ ret = ddi_intr_get_supported_types(dip, &type); if ((ret != DDI_SUCCESS) || (!(type & DDI_INTR_TYPE_FIXED))) { cmn_err(CE_WARN, "ddi_intr_get_supported_types() failed"); return (DDI_FAILURE); } ret = ddi_intr_get_nintrs(dip, DDI_INTR_TYPE_FIXED, &count); /* * Fixed interrupts can only have one interrupt. Check to make * sure that number of supported interrupts and number of * available interrupts are both equal to 1. */ if ((ret != DDI_SUCCESS) || (count != 1)) { cmn_err(CE_WARN, "No fixed interrupts found"); return (DDI_FAILURE); } xsp->xs_htable = kmem_zalloc(count * sizeof (ddi_intr_handle_t), KM_SLEEP); ret = ddi_intr_alloc(dip, xsp->xs_htable, DDI_INTR_TYPE_FIXED, 0, count, &actual, 0); if ((ret != DDI_SUCCESS) || (actual != 1)) { cmn_err(CE_WARN, "ddi_intr_alloc failed 0x%x", ret"); kmem_free(xsp->xs_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } ret = ddi_intr_get_pri(xsp->xs_htable[0], &intr_pri); if (ret != DDI_SUCCESS) { cmn_err(CE_WARN, "ddi_intr_get_pri failed 0x%x", ret"); (void) ddi_intr_free(xsp->xs_htable[0]); kmem_free(xsp->xs_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } if (intr_pri >= ddi_intr_get_hilevel_pri()) { mutex_init(&xsp->high_mu, NULL, MUTEX_DRIVER, DDI_INTR_PRI(intr_pri)); ret = ddi_intr_add_handler(xsp->xs_htable[0], mydevhigh_intr, (caddr_t)xsp, NULL); if (ret != DDI_SUCCESS) { cmn_err(CE_WARN, "ddi_intr_add_handler failed 0x%x", ret"); mutex_destroy(&xsp>xs_int_mutex); (void) ddi_intr_free(xsp->xs_htable[0]); kmem_free(xsp->xs_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } /* add soft interrupt */ if (ddi_intr_add_softint(xsp->xs_dip, &xsp->xs_softint_hdl, DDI_INTR_SOFTPRI_MAX, xs_soft_intr, (caddr_t)xsp) != DDI_SUCCESS) { cmn_err(CE_WARN, "add soft interrupt failed"); mutex_destroy(&xsp->high_mu); (void) ddi_intr_remove_handler(xsp->xs_htable[0]); (void) ddi_intr_free(xsp->xs_htable[0]); kmem_free(xsp->xs_htable, sizeof (ddi_intr_handle_t)); return (DDI_FAILURE); } xsp->low_soft_pri = DDI_INTR_SOFTPRI_MAX; mutex_init(&xsp->low_mu, NULL, MUTEX_DRIVER, DDI_INTR_PRI(xsp->low_soft_pri)); } else { /* * regular interrupt registration continues from here * do not use a soft interrupt */ } return (DDI_SUCCESS); }
高级别中断例程用于服务设备并对数据进行排队。如果低级例程未运行,则高级例程会触发软件中断,如以下示例所示。
static uint_t mydevhigh_intr(caddr_t arg1, caddr_t arg2) { struct mydevstate *xsp = (struct mydevstate *)arg1; uint8_t status; volatile uint8_t temp; int need_softint; mutex_enter(&xsp->high_mu); /* read status */ status = ddi_get8(xsp->data_access_handle, &xsp->regp->csr); if (!(status & INTERRUPTING)) { mutex_exit(&xsp->high_mu); return (DDI_INTR_UNCLAIMED); /* dev not interrupting */ } ddi_put8(xsp->data_access_handle,&xsp->regp->csr, CLEAR_INTERRUPT | ENABLE_INTERRUPTS); /* flush store buffers */ temp = ddi_get8(xsp->data_access_handle, &xsp->regp->csr); /* read data from device, queue data for low-level interrupt handler */ if (xsp->softint_running) need_softint = 0; else { xsp->softint_count++; need_softint = 1; } mutex_exit(&xsp->high_mu); /* read-only access to xsp->id, no mutex needed */ if (need_softint) { ret = ddi_intr_trigger_softint(xsp->xs_softint_hdl, NULL); if (ret == DDI_EPENDING) { cmn_err(CE_WARN, "ddi_intr_trigger_softint() soft interrupt " "already pending for this handler"); } else if (ret != DDI_SUCCESS) { cmn_err(CE_WARN, "ddi_intr_trigger_softint() failed"); } } return (DDI_INTR_CLAIMED); }
低级中断例程由用于触发软件中断的高级别中断例程启动。低级中断例程会一直运行,直到没有其他要处理的对象为止,如以下示例所示。
static uint_t mydev_soft_intr(caddr_t arg1, caddr_t arg2) { struct mydevstate *mydevp = (struct mydevstate *)arg1; /* ... */ mutex_enter(&mydevp->low_mu); mutex_enter(&mydevp->high_mu); if (mydevp->softint_count > 1) { mydevp->softint_count--; mutex_exit(&mydevp->high_mu); mutex_exit(&mydevp->low_mu); return (DDI_INTR_CLAIMED); } if ( /* queue empty */ ) { mutex_exit(&mydevp->high_mu); mutex_exit(&mydevp->low_mu); return (DDI_INTR_UNCLAIMED); } mydevp->softint_running = 1; while (EMBEDDED COMMENT:data on queue) { ASSERT(mutex_owned(&mydevp->high_mu); /* Dequeue data from high-level queue. */ mutex_exit(&mydevp->high_mu); /* normal interrupt processing */ mutex_enter(&mydevp->high_mu); } mydevp->softint_running = 0; mydevp->softint_count = 0; mutex_exit(&mydevp->high_mu); mutex_exit(&mydevp->low_mu); return (DDI_INTR_CLAIMED); }
许多设备都可以临时控制总线。这些设备可以执行涉及主内存和其他设备的数据传送。由于设备执行这些操作的过程中无需借助于 CPU,因此该类型的数据传送称为直接内存访问 (direct memory access, DMA)。可以执行的 DMA 传送类型如下:
两个设备之间
设备和内存之间
内存和内存之间
本章仅介绍设备和内存之间的传送。本章提供有关以下主题的信息:
Solaris 设备驱动程序接口/驱动程序内核接口 (Device Driver Interface/Driver-Kernel Interface, DDI/DKI) 为 DMA 提供了独立于体系结构的高级别模型。通过此模型,框架(即 DMA 例程)可以隐藏此类体系结构特定的详细信息,例如:
设置 DMA 映射
生成分散/集中列表
确保 I/O 和 CPU 高速缓存一致
DDI/DKI 中使用了若干个抽象术语来描述 DMA 事务的各个方面:
DMA 句柄-成功调用 ddi_dma_alloc_handle(9F) 后返回的不透明对象。在后续的 DMA 子例程调用中可以使用 DMA 句柄来引用此类 DMA 对象。
DMA cookie-ddi_dma_cookie(9S) 结构 (ddi_dma_cookie_t) 用于描述 DMA 对象中可由设备完全寻址的连续部分。该 cookie 包含对 DMA 引擎进行编程所需的 DMA 寻址信息。
设备驱动程序不会直接将对象映射到内存中,而是为内存对象分配 DMA 资源。然后,DMA 例程将执行为 DMA 访问设置对象时所需的任何特定于平台的操作。驱动程序将收到一个 DMA 句柄,用于标识为该对象分配的 DMA 资源。此句柄对于设备驱动程序而言是不透明的。驱动程序必须保存句柄并在后续调用中将其传递给 DMA 例程。驱动程序不应以任何方式解释句柄。
针对 DMA 句柄定义的操作可提供以下服务:
处理 DMA 资源
同步 DMA 对象
检索已分配资源的特性
设备可执行以下三种类型的 DMA:
总线主控器 DMA
第三方 DMA
第一方 DMA
在设备用作实际总线主控器的情况下,驱动程序应直接对该设备的 DMA 寄存器进行编程。例如,如果 DMA 引擎驻留在设备板上,则设备便会用作总线主控器。传送地址和计数从 DMA cookie 中获取,并将传递给设备。
第三方 DMA 使用驻留在主系统板上的系统 DMA 引擎,该引擎中有若干个可供设备使用的 DMA 通道。设备依赖于系统的 DMA 引擎来执行设备与内存之间的数据传送。驱动程序使用 DMA 引擎例程(请参见 ddi_dmae(9F) 函数)对 DMA 引擎进行初始化和编程。每次进行 DMA 数据传送时,驱动程序都会对 DMA 引擎进行编程,然后会向设备发出命令,以便借助该引擎来启动传送操作。
执行第一方 DMA 时,设备使用系统 DMA 引擎中的通道来驱动该设备的 DMA 总线循环。使用 ddi_dmae_1stparty(9F) 函数可在级联模式下对此通道进行配置,以免 DMA 引擎干扰传送。
设备运行的平台可提供直接内存访问 (direct memory access, DMA) 或直接虚拟内存访问 (direct virtual memory access, DVMA)。
在支持 DMA 的平台上,系统会为设备提供物理地址以执行传送。在此情况下,DMA 对象的传送实际上会包含许多在物理上不连续的传送。例如,当应用程序传送跨越若干连续虚拟页(但这些虚拟页映射到物理上不连续的页)的缓冲区时。要处理不连续的内存,用于这些平台的设备通常需要具有特定种类的分散/集中 DMA 功能。通常,x86 系统会为直接内存传送提供物理地址。
在支持 DVMA 的平台上,系统会为设备提供虚拟地址以执行传送。在此情况下,基础平台提供的内存管理单元 (memory management unit, MMU) 会将对这些虚拟地址的设备访问转换为正确的物理地址。设备会与可映射到不连续物理页的连续虚拟映像之间来回进行传送。在这些平台上运行的设备无需分散/集中 DMA 功能。通常,SPARC 平台会为直接内存传送提供虚拟地址。
DMA 句柄是表示对象(通常为内存缓冲区或地址)的不透明指针。设备通过 DMA 句柄可执行 DMA 传送。对 DMA 例程的若干个不同调用可使用句柄来标识为对象分配的 DMA 资源。
DMA 句柄所表示的对象全部包含在一个或多个 DMA cookie 中。DMA cookie 表示 DMA 引擎在数据传送中使用的一段连续内存。系统会根据以下信息将对象划分为多个 cookie:
驱动程序提供的 ddi_dma_attr(9S) 特性结构
目标对象的内存位置
目标对象的对齐
如果一个对象不满足 DMA 引擎的限制,则必须将该对象分为多个 DMA 窗口。一次只能为一个窗口激活和分配资源。使用 ddi_dma_getwin(9F) 函数可在一个对象内的多个窗口之间切换。每个 DMA 窗口都包含一个或多个 DMA cookie。有关更多信息,请参见DMA 窗口。
一些 DMA 引擎可以接受多个 cookie。此类引擎不用借助系统即可执行分散/集中 I/O。如果从一个绑定中返回多个 cookie,则驱动程序应重复调用 ddi_dma_nextcookie(9F) 以检索每个 cookie。然后,必须将这些 cookie 编程到引擎中。随后可对设备进行编程,以传送这些 DMA cookie 聚集所包含的总字节数。
不同类型的 DMA 之间,DMA 传送的步骤都相似。以下各节提供了执行 DMA 传送的方法。
在来自文件系统的缓冲区的块驱动程序中,不必确保 DMA 对象是否已在内存中锁定。该文件系统已在内存中锁定了数据。
描述 DMA 特性。通过此步骤,例程可确保设备能够访问缓冲区。
分配 DMA 句柄。
确保 DMA 对象已在内存中锁定。请参见 physio(9F) 或 ddi_umem_lock(9F) 手册页。
为该对象分配 DMA 资源。
对设备的 DMA 引擎进行编程。
启动引擎。
传送完成后,继续执行总线主控器操作。
执行所需的对象同步。
释放 DMA 资源。
释放 DMA 句柄。
分配 DMA 通道。
使用 ddi_dmae_1stparty(9F) 配置通道。
确保 DMA 对象已在内存中锁定。请参见 physio(9F) 或 ddi_umem_lock(9F) 手册页。
为该对象分配 DMA 资源。
对设备的 DMA 引擎进行编程。
启动引擎。
传送完成后,继续执行总线主控器操作。
执行所需的对象同步。
释放 DMA 资源。
取消分配 DMA 通道。
分配 DMA 通道。
使用 ddi_dmae_getattr(9F) 检索系统的 DMA 引擎特性。
在内存中锁定 DMA 对象。请参见 physio(9F) 或 ddi_umem_lock(9F) 手册页。
为该对象分配 DMA 资源。
使用 ddi_dmae_prog(9F) 对系统 DMA 引擎进行编程,以执行传送。
执行所需的对象同步。
使用 ddi_dmae_stop(9F) 停止 DMA 引擎。
释放 DMA 资源。
取消分配 DMA 通道。
某些硬件平台会以特定于总线的方式限制 DMA 功能。驱动程序应使用 ddi_slaveonly(9F) 来确定设备是否位于可以执行 DMA 的插槽中。
设备可以访问的地址的限制
最大传送计数
地址对齐限制
设备驱动程序必须通过 ddi_dma_attr(9S) 结构向系统通知任何 DMA 引擎限制。此操作可以确保设备的 DMA 引擎可以访问系统分配的 DMA 资源。系统可能对设备特性实施附加限制,但绝不会取消驱动程序实施的任何限制。
typedef struct ddi_dma_attr { uint_t dma_attr_version; /* version number */ uint64_t dma_attr_addr_lo; /* low DMA address range */ uint64_t dma_attr_addr_hi; /* high DMA address range */ uint64_t dma_attr_count_max; /* DMA counter register */ uint64_t dma_attr_align; /* DMA address alignment */ uint_t dma_attr_burstsizes; /* DMA burstsizes */ uint32_t dma_attr_minxfer; /* min effective DMA size */ uint64_t dma_attr_maxxfer; /* max DMA xfer size */ uint64_t dma_attr_seg; /* segment boundary */ int dma_attr_sgllen; /* s/g length */ uint32_t dma_attr_granular; /* granularity of device */ uint_t dma_attr_flags; /* Bus specific DMA flags */ } ddi_dma_attr_t;
其中:
特性结构的版本号。dma_attr_version 应设置为 DMA_ATTR_V0。
DMA 引擎可以访问的最低总线地址。
DMA 引擎可以访问的最高总线地址。
指定 DMA 引擎可在一个 cookie 中处理的最大传送计数。该限制表示为最大计数减 1。此计数用作位掩码,因此计数也必须比 2 的幂小 1。
指定通过 ddi_dma_mem_alloc(9F) 分配内存时的对齐要求。例如,以页边界对齐。dma_attr_align 字段仅在分配内存时使用。在绑定操作过程中,将省略此字段。对于绑定操作,驱动程序必须确保缓冲区已正确对齐。
指定设备支持的突发流量大小。突发流量大小是指设备在放弃总线之前可以传输的数据量。此成员是突发流量大小的二进制编码,这些大小是 2 的幂次方。例如,如果设备能够执行 1 字节、2 字节、4 字节和 16 字节突发传输,则应将此字段设置为 0x17。系统还会使用此字段来确定对齐限制。
设备可以执行的最小有效传送大小。此大小还会影响对齐和填充的限制。
描述 DMA 引擎在一个 I/O 命令中可以容纳的最大字节数。此限制仅在 dma_attr_maxxfer 小于 (dma_attr_count_max + 1) * dma_attr_sgllen 时才会有意义。
DMA 引擎的地址寄存器的上限。当地址寄存器的高 8 位为包含段号的锁存器时,通常会使用 dma_attr_seg 。低 24 位用于寻址段。在此情况下,dma_attr_seg 会设置为 0xFFFFFF,这可以防止系统在为对象分配资源时跨越 24 位段边界。
指定分散/集中列表中的最大项数。dma_attr_sgllen 是 DMA 引擎在对设备的一个 I/O 请求中可以使用的 cookie 数。如果 DMA 引擎不包含任何分散/集中列表,则此字段应设置为 1。
此字段用于提供设备 DMA 传送能力的粒度(以字节为单位)。指定海量存储设备的扇区大小即是关于如何使用该值的一个例子。如果绑定操作需要部分映射,则可使用此字段确保 DMA 窗口中的 cookie 大小之和为粒度的整数倍。但是,如果设备没有分散/集中功能,则 DDI 无法确保粒度。对于此情况,dma_attr_granular 字段的值应为 1。
此字段可以设置为 DDI_DMA_FORCE_PHYSICAL,这表示如果系统同时支持物理 I/O 地址和虚拟 I/O 地址,则系统应返回物理 I/O 地址而非虚拟 I/O 地址。如果系统不支持物理 DMA,则 ddi_dma_alloc_handle(9F) 的返回值为 DDI_DMA_BADATTR。在此情况下,驱动程序必须清除 DDI_DMA_FORCE_PHYSICAL 并重试该操作。
在 SPARC 计算机中,S 总线上的 DMA 引擎具有以下特性:
仅访问 0xFF000000 到 0xFFFFFFFF 范围内的地址
32 位 DMA 计数器寄存器
可处理按字节对齐的传送
支持 1 字节、2 字节和 4 字节突发流量大小
最小有效传送大小为 1 字节
32 位地址寄存器
无分散/集中列表
仅对扇区执行操作,例如磁盘
在 SPARC 计算机中,S 总线上的 DMA 引擎具有以下特性结构:
static ddi_dma_attr_t attributes = { DMA_ATTR_V0, /* Version number */ 0xFF000000, /* low address */ 0xFFFFFFFF, /* high address */ 0xFFFFFFFF, /* counter register max */ 1, /* byte alignment */ 0x7, /* burst sizes: 0x1 | 0x2 | 0x4 */ 0x1, /* minimum transfer size */ 0xFFFFFFFF, /* max transfer size */ 0xFFFFFFFF, /* address register max */ 1, /* no scatter-gather */ 512, /* device operates on sectors */ 0, /* attr flag: set to 0 */ };
在 x86 计算机中,ISA 总线上的 DMA 引擎具有以下特性:
仅访问前 16 MB 内存
在一次 DMA 传送中不能跨越 1 MB 的边界
16 位计数器寄存器
可处理按字节对齐的传送
支持 1 字节、2 字节和 4 字节突发流量大小
最小有效传送大小为 1 字节
最多可以支持 17 个分散/集中传送
仅对扇区执行操作,例如磁盘
在 x86 计算机中,ISA 总线上的 DMA 引擎具有以下特性结构:
static ddi_dma_attr_t attributes = { DMA_ATTR_V0, /* Version number */ 0x00000000, /* low address */ 0x00FFFFFF, /* high address */ 0xFFFF, /* counter register max */ 1, /* byte alignment */ 0x7, /* burst sizes */ 0x1, /* minimum transfer size */ 0xFFFFFFFF, /* max transfer size */ 0x000FFFFF, /* address register max */ 17, /* scatter-gather */ 512, /* device operates on sectors */ 0, /* attr flag: set to 0 */ };
本节介绍如何管理 DMA 资源。
为内存对象分配 DMA 资源之前,必须防止该对象移动。否则,在设备尝试向该对象进行写入时,系统会从内存中删除该对象。缺少对象会导致数据传送失败,并且可能损坏系统。防止内存对象在 DMA 传送过程中移动的过程称为锁定对象。
以下对象类型不要求显式锁定:
通过执行 strategy(9E) 获得的来自文件系统的缓冲区。这些缓冲区已由文件系统锁定。
设备驱动程序中分配的内核内存,如 ddi_dma_mem_alloc(9F) 分配的内核内存。
对于其他对象(如用户空间中的缓冲区),必须使用 physio(9F) 或 ddi_umem_lock(9F) 来锁定对象。使用这些函数来锁定对象通常在字符设备驱动程序的 read(9E) 或 write(9E) 例程中执行。有关示例,请参见数据传输方法。
DMA 句柄是一个不透明的对象,用作对后续分配的 DMA 资源的引用。DMA 句柄通常在驱动程序的使用 ddi_dma_alloc_handle(9F) 的 attach() 入口点中分配。ddi_dma_alloc_handle() 函数采用 dip 引用的设备信息以及 ddi_dma_attr(9S) 结构描述的设备的 DMA 特性作为参数。ddi_dma_alloc_handle() 函数的语法如下所示:
int ddi_dma_alloc_handle(dev_info_t *dip, ddi_dma_attr_t *attr, int (*callback)(caddr_t), caddr_t arg, ddi_dma_handle_t *handlep);
其中:
指向设备的 dev_info 结构的指针。
指向 ddi_dma_attr(9S) 结构的指针,如DMA 特性中所述。
用于处理资源分配故障的回调函数的地址。
要传递给回调函数的参数。
指向 DMA 句柄的指针,用于存储返回的句柄。
以下两个接口用于分配 DMA 资源:
ddi_dma_buf_bind_handle(9F)-与 buf(9S) 结构结合使用
ddi_dma_addr_bind_handle(9F)-与虚拟地址结合使用
如果存在驱动程序的 xxstart() 例程,则 DMA 资源通常在 xxstart() 例程中分配。有关 xxstart() 的讨论,请参见异步数据传输(块驱动程序)。这两个接口的语法如下:
int ddi_dma_addr_bind_handle(ddi_dma_handle_t handle, struct as *as, caddr_t addr, size_t len, uint_t flags, int (*callback)(caddr_t), caddr_t arg, ddi_dma_cookie_t *cookiep, uint_t *ccountp); int ddi_dma_buf_bind_handle(ddi_dma_handle_t handle, struct buf *bp, uint_t flags, int (*callback)(caddr_t), caddr_t arg, ddi_dma_cookie_t *cookiep, uint_t *ccountp);
以下参数对于 ddi_dma_addr_bind_handle(9F) 和 ddi_dma_buf_bind_handle(9F) 是通用的:
DMA 句柄和用于分配资源的对象。
表示传送方向和其他特性的标志集。DDI_DMA_READ 表示从设备向内存传送数据。DDI_DMA_WRITE 表示从内存向设备传送数据。有关可用标志的完整讨论,请参见 ddi_dma_addr_bind_handle(9F) 或 ddi_dma_buf_bind_handle(9F) 手册页。
用于处理资源分配故障的回调函数的地址。请参见 ddi_dma_alloc_handle(9F) 手册页。
要传递给回调函数的参数。
指向此对象的第一个 DMA cookie 的指针。
指向此对象的 DMA cookie 数的指针。
对于 ddi_dma_addr_bind_handle(9F),对象通过包含以下参数的地址范围进行描述:
指向地址空间结构的指针。as 的值必须为 NULL。
对象的基本内核地址。
对象长度(以字节为单位)。
对于 ddi_dma_buf_bind_handle(9F),对象通过 bp 所指向的 buf(9S) 结构进行描述。
对于具有 DMA 功能的设备,要使用的寄存器比前面示例中所用寄存器多。
设备寄存器结构中使用以下字段来支持具有 DMA 功能但不支持分散/集中的设备:
uint32_t dma_addr; /* starting address for DMA */ uint32_t dma_size; /* amount of data to transfer */
设备寄存器结构中使用以下字段来支持具有 DMA 功能并支持分散/集中的设备:
struct sglentry { uint32_t dma_addr; uint32_t dma_size; } sglist[SGLLEN]; caddr_t iopb_addr; /* When written, informs the device of the next */ /* command's parameter block address. */ /* When read after an interrupt, contains */ /* the address of the completed command. */
在示例 9–1 中,xxstart() 用作回调函数。特定设备状态结构用作 xxstart() 的参数。xxstart() 函数将尝试启动命令。如果由于资源不可用而无法启动该命令,则会安排以后在资源可用时调用 xxstart()。
由于 xxstart() 用作 DMA 回调,因此 xxstart() 必须遵守以下规则,DMA 回调中将强制执行这些规则:
不能假定资源可用。回调必须尝试再次分配资源。
回调必须向系统指明分配是否成功。如果回调未能分配资源,则应返回 DDI_DMA_CALLBACK_RUNOUT,在此情况下需要以后再次调用 xxstart()。DDI_DMA_CALLBACK_DONE 表示回调成功,因此不需要再进行回调。
static int xxstart(caddr_t arg) { struct xxstate *xsp = (struct xxstate *)arg; struct device_reg *regp; int flags; mutex_enter(&xsp->mu); if (xsp->busy) { /* transfer in progress */ mutex_exit(&xsp->mu); return (DDI_DMA_CALLBACK_RUNOUT); } xsp->busy = 1; regp = xsp->regp; if ( /* transfer is a read */ ) { flags = DDI_DMA_READ; } else { flags = DDI_DMA_WRITE; } mutex_exit(&xsp->mu); if (ddi_dma_buf_bind_handle(xsp->handle,xsp->bp,flags, xxstart, (caddr_t)xsp, &cookie, &ccount) != DDI_DMA_MAPPED) { /* really should check all return values in a switch */ mutex_enter(&xsp->mu); xsp->busy=0; mutex_exit(&xsp->mu); return (DDI_DMA_CALLBACK_RUNOUT); } /* Program the DMA engine. */ return (DDI_DMA_CALLBACK_DONE); }
驱动程序在 ddi_dma_attr(9S) 结构的 dma_attr_burstsizes 字段中指定其设备支持的 DMA 突发流量大小。此字段是所支持的突发流量大小的位图。但是,在分配 DMA 资源时,系统可能会对设备实际使用的突发流量大小施加更多限制。ddi_dma_burstsizes(9F) 例程可用来获取允许的突发流量大小。此例程将为设备返回适当的突发流量大小位图。分配 DMA 资源时,驱动程序可向系统请求用于其 DMA 引擎的适当突发流量大小。
#define BEST_BURST_SIZE 0x20 /* 32 bytes */ if (ddi_dma_buf_bind_handle(xsp->handle,xsp->bp, flags, xxstart, (caddr_t)xsp, &cookie, &ccount) != DDI_DMA_MAPPED) { /* error handling */ } burst = ddi_dma_burstsizes(xsp->handle); /* check which bit is set and choose one burstsize to */ /* program the DMA engine */ if (burst & BEST_BURST_SIZE) { /* program DMA engine to use this burst size */ } else { /* other cases */ }
一些设备驱动程序除了执行用户线程和内核请求的传送外,可能还需要为 DMA 传送分配内存。分配专用 DMA 缓冲区的一些示例包括设置用于与设备之间进行通信的共享内存以及分配中间传送缓冲区。使用 ddi_dma_mem_alloc(9F) 可为 DMA 传送分配内存。
int ddi_dma_mem_alloc(ddi_dma_handle_t handle, size_t length, ddi_device_acc_attr_t *accattrp, uint_t flags, int (*waitfp)(caddr_t), caddr_t arg, caddr_t *kaddrp, size_t *real_length, ddi_acc_handle_t *handlep);
其中:
DMA 句柄
所需分配的长度(以字节为单位)
指向设备访问特性结构的指针
数据传送模式标志。可能的值包括 DDI_DMA_CONSISTENT 和 DDI_DMA_STREAMING。
用于处理资源分配故障的回调函数的地址。请参见 ddi_dma_alloc_handle(9F) 手册页。
要传递给回调函数的参数
成功返回时包含已分配存储空间的地址的指针
分配的长度(以字节为单位)
指向数据访问句柄的指针
如果设备以不连续的方式进行访问,则应将 flags 参数设置为 DDI_DMA_CONSISTENT。由于会频繁应用于小型对象,因此使用 ddi_dma_sync(9F) 的同步步骤应尽可能为轻量步骤。这种访问类型通常称为一致访问。一致访问对用于设备与驱动程序之间通信的 I/O 参数块特别有用。
在 x86 平台上,物理上连续的 DMA 内存的分配有以下要求:
ddi_dma_attr(9S) 结构中分散/集中列表 dma_attr_sgllen 的长度必须设置为 1。
请勿指定 DDI_DMA_PARTIAL。DDI_DMA_PARTIAL 表示允许进行部分资源分配。
以下示例说明如何分配 IOPB 内存以及访问此内存必需的 DMA 资源。仍然必须分配 DMA 资源,并且必须将 DDI_DMA_CONSISTENT 标志传递给分配函数。
if (ddi_dma_mem_alloc(xsp->iopb_handle, size, &accattr, DDI_DMA_CONSISTENT, DDI_DMA_SLEEP, NULL, &xsp->iopb_array, &real_length, &xsp->acchandle) != DDI_SUCCESS) { /* error handling */ goto failure; } if (ddi_dma_addr_bind_handle(xsp->iopb_handle, NULL, xsp->iopb_array, real_length, DDI_DMA_READ | DDI_DMA_CONSISTENT, DDI_DMA_SLEEP, NULL, &cookie, &count) != DDI_DMA_MAPPED) { /* error handling */ ddi_dma_mem_free(&xsp->acchandle); goto failure; }
对于顺序、单向、块大小和按块对齐的内存传送,flags 参数应设置为 DDI_DMA_STREAMING。这种访问类型通常称为流访问。
在某些情况下,使用 I/O 高速缓存可以加快 I/O 传送。I/O 高速缓存最少传送一个高速缓存行。ddi_dma_mem_alloc(9F) 例程会将 size 舍入为高速缓存行的倍数,以避免数据损坏。
ddi_dma_mem_alloc(9F) 函数将返回已分配的内存对象的实际大小。由于存在填充和对齐要求,实际大小可能会大于所请求的大小。ddi_dma_addr_bind_handle(9F) 函数要求使用实际长度。
使用 ddi_dma_mem_free(9F) 函数可以释放 ddi_dma_mem_alloc(9F) 分配的内存。
驱动程序必须确保缓冲区适当对齐。要求下限 DMA 缓冲区对齐的设备的驱动程序可能需要将数据复制到满足该要求的驱动程序中间缓冲区,然后将该中间缓冲区绑定到 DMA 的 DMA 句柄。使用 ddi_dma_mem_alloc(9F) 可分配驱动程序中间缓冲区。请务必使用 ddi_dma_mem_alloc(9F) 而非 kmem_alloc(9F) 来为要进行访问的设备分配内存。
资源分配例程在处理分配故障时可为驱动程序提供若干选项。waitfp 参数用于指明分配例程是阻塞、立即返回还是安排回调,如下表所示。
表 9–1 资源分配处理
waitfp 值 |
表示的操作 |
---|---|
DDI_DMA_DONTWAIT |
驱动程序不想等到资源可用 |
DDI_DMA_SLEEP |
驱动程序愿意无限期地等到资源可用 |
其他值 |
当资源可能可用时要调用的函数的地址 |
如果资源已成功分配,则必须对设备进行编程。尽管对 DMA 引擎进行编程是特定于设备的,但所有 DMA 引擎都需要一个起始地址和一个传送计数。设备驱动程序将从 ddi_dma_addr_bind_handle(9F)、ddi_dma_buf_bind_handle(9F) 或 ddi_dma_getwin(9F) 的成功调用所返回的 DMA cookie 中检索这两个值。这些函数都会返回第一个 DMA cookie 以及指示 DMA 对象是否包含多个 cookie 的 cookie 计数。如果 cookie 计数 N 大于 1,则必须对 ddi_dma_nextcookie(9F) 调用 N-1 次,以检索其余所有 cookie。
DMA cookie 的类型为 ddi_dma_cookie(9S)。这一类型的 cookie 包含以下字段:
uint64_t _dmac_ll; /* 64-bit DMA address */ uint32_t _dmac_la[2]; /* 2 x 32-bit address */ size_t dmac_size; /* DMA cookie size */ uint_t dmac_type; /* bus specific type bits */
dmac_laddress 指定适用于对设备的 DMA 引擎进行编程的 64 位 I/O 地址。如果设备具有 64 位 DMA 地址寄存器,则驱动程序应使用此字段对 DMA 引擎进行编程。dmac_address 字段指定应该用于具有 32 位 DMA 地址寄存器的设备的 32 位 I/O 地址。dmac_size 字段包含传送计数。根据总线体系结构,驱动程序可能需要 cookie 中的 dmac_type 字段。驱动程序不应对 cookie 执行任何处理,如逻辑或算术处理。
ddi_dma_cookie_t cookie; if (ddi_dma_buf_bind_handle(xsp->handle,xsp->bp, flags, xxstart, (caddr_t)xsp, &cookie, &xsp->ccount) != DDI_DMA_MAPPED) { /* error handling */ } sglp = regp->sglist; for (cnt = 1; cnt <= SGLLEN; cnt++, sglp++) { /* store the cookie parms into the S/G list */ ddi_put32(xsp->access_hdl, &sglp->dma_size, (uint32_t)cookie.dmac_size); ddi_put32(xsp->access_hdl, &sglp->dma_addr, cookie.dmac_address); /* Check for end of cookie list */ if (cnt == xsp->ccount) break; /* Get next DMA cookie */ (void) ddi_dma_nextcookie(xsp->handle, &cookie); } /* start DMA transfer */ ddi_put8(xsp->access_hdl, ®p->csr, ENABLE_INTERRUPTS | START_TRANSFER);
DMA 传送完成后(通常在中断例程中),驱动程序可以通过调用 ddi_dma_unbind_handle(9F) 来释放 DMA 资源。
如同步内存对象中所述,ddi_dma_unbind_handle(9F) 可调用 ddi_dma_sync(9F),从而无需进行任何显式同步。调用 ddi_dma_unbind_handle(9F) 之后,DMA 资源将无效,并且对资源的进一步引用会产生无法预料的结果。以下示例说明如何使用 ddi_dma_unbind_handle(9F)。
static uint_t xxintr(caddr_t arg) { struct xxstate *xsp = (struct xxstate *)arg; uint8_t status; volatile uint8_t temp; mutex_enter(&xsp->mu); /* read status */ status = ddi_get8(xsp->access_hdl, &xsp->regp->csr); if (!(status & INTERRUPTING)) { mutex_exit(&xsp->mu); return (DDI_INTR_UNCLAIMED); } ddi_put8(xsp->access_hdl, &xsp->regp->csr, CLEAR_INTERRUPT); /* for store buffers */ temp = ddi_get8(xsp->access_hdl, &xsp->regp->csr); ddi_dma_unbind_handle(xsp->handle); /* Check for errors. */ xsp->busy = 0; mutex_exit(&xsp->mu); if ( /* pending transfers */ ) { (void) xxstart((caddr_t)xsp); } return (DDI_INTR_CLAIMED); }
应释放 DMA 资源。如果要在下一传送中使用不同对象,则应重新分配 DMA 资源。但是,如果始终使用同一个对象,则分配一次资源即可。只要保持对 ddi_dma_sync(9F) 的介入调用,随后便可重用资源。
分离驱动程序时,必须释放 DMA 句柄。ddi_dma_free_handle(9F) 函数可销毁 DMA 句柄以及系统在该句柄上高速缓存的任何剩余资源。如果再对 DMA 句柄进行任何引用,将会产生无法预料的结果。
DMA 回调不能取消。取消 DMA 回调需要在驱动程序的 detach(9E) 入口点中附加一些代码。如果存在任何未完成的回调,则 detach() 例程一定不会返回 DDI_SUCCESS。请参见示例 9–6。发生 DMA 回调时,detach() 例程必须等待回调运行。回调完成时,detach() 必须防止回调自行重新安排。通过状态结构中的附加字段可以防止重新安排回调,如以下示例所示。
static int xxdetach(dev_info_t *dip, ddi_detach_cmd_t cmd) { /* ... */ mutex_enter(&xsp->callback_mutex); xsp->cancel_callbacks = 1; while (xsp->callback_count > 0) { cv_wait(&xsp->callback_cv, &xsp->callback_mutex); } mutex_exit(&xsp->callback_mutex); /* ... */ } static int xxstrategy(struct buf *bp) { /* ... */ mutex_enter(&xsp->callback_mutex); xsp->bp = bp; error = ddi_dma_buf_bind_handle(xsp->handle, xsp->bp, flags, xxdmacallback, (caddr_t)xsp, &cookie, &ccount); if (error == DDI_DMA_NORESOURCES) xsp->callback_count++; mutex_exit(&xsp->callback_mutex); /* ... */ } static int xxdmacallback(caddr_t callbackarg) { struct xxstate *xsp = (struct xxstate *)callbackarg; /* ... */ mutex_enter(&xsp->callback_mutex); if (xsp->cancel_callbacks) { /* do not reschedule, in process of detaching */ xsp->callback_count--; if (xsp->callback_count == 0) cv_signal(&xsp->callback_cv); mutex_exit(&xsp->callback_mutex); return (DDI_DMA_CALLBACK_DONE); /* don't reschedule it */ } /* * Presumably at this point the device is still active * and will not be detached until the DMA has completed. * A return of 0 means try again later */ error = ddi_dma_buf_bind_handle(xsp->handle, xsp->bp, flags, DDI_DMA_DONTWAIT, NULL, &cookie, &ccount); if (error == DDI_DMA_MAPPED) { /* Program the DMA engine. */ xsp->callback_count--; mutex_exit(&xsp->callback_mutex); return (DDI_DMA_CALLBACK_DONE); } if (error != DDI_DMA_NORESOURCES) { xsp->callback_count--; mutex_exit(&xsp->callback_mutex); return (DDI_DMA_CALLBACK_DONE); } mutex_exit(&xsp->callback_mutex); return (DDI_DMA_CALLBACK_RUNOUT); }
在访问内存对象的过程中,驱动程序可能需要同步与各种高速缓存有关的内存对象。本节提供了有关何时以及如何同步内存对象的准则。
CPU 高速缓存是位于 CPU 和系统的主内存之间的极高速内存。I/O 高速缓存位于设备和系统的主内存之间,如下图所示。
尝试从主内存读取数据时,关联的高速缓存会对请求的数据进行检查。如果数据可用,高速缓存可快速提供这些数据。如果高速缓存中没有数据,则该高速缓存将从主内存中检索数据。然后,高速缓存会将数据传递给请求者并保存数据,以备在后续请求中使用。
类似地,在写循环中,数据会快速存储在高速缓存中。CPU 或设备可以继续执行,即传送数据。将数据存储在高速缓存中所需的时间比等待将数据写入内存所需的时间少得多。
采用此模型,在设备传送完成后,数据仍可位于 I/O 高速缓存中,而主内存中没有数据。如果 CPU 访问内存,CPU 可能会从 CPU 高速缓存中读取错误数据。驱动程序必须调用同步例程,以刷新 I/O 高速缓存中的数据,并使用新数据更新 CPU 高速缓存。此操作可确保内存的情况对于 CPU 而言保持一致。类似地,如果设备要对 CPU 修改的数据进行访问,则需要采用同步步骤。
可在设备和内存之间创建附加的高速缓存和缓冲区,如总线延伸架和桥。使用 ddi_dma_sync(9F) 可以同步所有适用的高速缓存。
一个内存对象可能有多个映射,如通过 DMA 句柄用于 CPU 和用于设备的映射。如果使用任何映射来修改内存对象,则具有多个映射的驱动程序需要调用 ddi_dma_sync(9F)。调用 ddi_dma_sync() 可以确保对内存对象的修改在通过不同映射访问该对象之前完成。如果对对象的任何高速缓存引用现在已过时,ddi_dma_sync() 函数还可以通知该对象的其他映射。此外,ddi_dma_sync() 还会根据需要刷新过时的高速缓存引用或使其无效。
通常,当 DMA 传送完成时,驱动程序必须调用 ddi_dma_sync()。此规则的例外情况是如果使用 ddi_dma_unbind_handle(9F) 取消分配 DMA 资源,则会代表驱动程序隐式执行 ddi_dma_sync()。ddi_dma_sync() 的语法如下:
int ddi_dma_sync(ddi_dma_handle_t handle, off_t off, size_t length, uint_t type);
如果设备的 DMA 引擎要读取对象,则必须通过将 type 设置为 DDI_DMA_SYNC_FORDEV 来同步该设备看到的对象信息。如果设备的 DMA 引擎已写入内存对象并且 CPU 将读取该对象,则必须通过将 type 设置为 DDI_DMA_SYNC_FORCPU 来同步该 CPU 看到的对象信息。
以下示例说明如何为 CPU 同步 DMA 对象:
if (ddi_dma_sync(xsp->handle, 0, length, DDI_DMA_SYNC_FORCPU) == DDI_SUCCESS) { /* the CPU can now access the transferred data */ /* ... */ } else { /* error handling */ }
如果唯一的映射是用于内核的,请使用标志 DDI_DMA_SYNC_FORKERNEL,类似于 ddi_dma_mem_alloc(9F) 所分配的内存中的情况。系统会尝试以比同步 CPU 看到的信息更快的速度来同步内核看到的信息。如果系统无法更快地同步内核看到的信息,则系统将按照如同已设置 DDI_DMA_SYNC_FORCPU 标志的情况执行相应的操作。
如果对象不满足 DMA 引擎的限制,则必须将传送分为一系列较小的传送。驱动程序本身即可对传送进行拆分。或者,驱动程序也可以允许系统仅为对象的一部分分配资源,从而创建一系列 DMA 窗口。允许系统分配资源是首选解决方案,因为系统管理资源的效率比驱动程序高。
DMA 窗口有两个特性。offset 特性是从对象的开头度量的。length 特性是要分配的内存的字节数。在进行部分分配后,只有一系列在 offset 开始的 length 字节分配了资源。
请求 DMA 窗口的方法是将 DDI_DMA_PARTIAL 标志指定为 ddi_dma_buf_bind_handle(9F) 或 ddi_dma_addr_bind_handle(9F) 的参数。如果可以建立窗口,则这两个函数都将返回 DDI_DMA_PARTIAL_MAP。但是,系统可能会为整个对象分配资源,此时将返回 DDI_DMA_MAPPED。驱动程序应检查返回值,以确定 DMA 窗口是否正在使用。请参见以下示例。
static int xxstart (caddr_t arg) { struct xxstate *xsp = (struct xxstate *)arg; struct device_reg *regp = xsp->reg; ddi_dma_cookie_t cookie; int status; mutex_enter(&xsp->mu); if (xsp->busy) { /* transfer in progress */ mutex_exit(&xsp->mu); return (DDI_DMA_CALLBACK_RUNOUT); } xsp->busy = 1; mutex_exit(&xsp->mu); if ( /* transfer is a read */) { flags = DDI_DMA_READ; } else { flags = DDI_DMA_WRITE; } flags |= DDI_DMA_PARTIAL; status = ddi_dma_buf_bind_handle(xsp->handle, xsp->bp, flags, xxstart, (caddr_t)xsp, &cookie, &ccount); if (status != DDI_DMA_MAPPED && status != DDI_DMA_PARTIAL_MAP) return (DDI_DMA_CALLBACK_RUNOUT); if (status == DDI_DMA_PARTIAL_MAP) { ddi_dma_numwin(xsp->handle, &xsp->nwin); xsp->partial = 1; xsp->windex = 0; } else { xsp->partial = 0; } /* Program the DMA engine. */ return (DDI_DMA_CALLBACK_DONE); }
有两个函数可对 DMA 窗口执行操作。第一个函数 ddi_dma_numwin(9F) 可为特定的 DMA 对象返回 DMA 窗口数。另一个函数 ddi_dma_getwin(9F) 允许在对象内重新定位,即重新分配系统资源。ddi_dma_getwin () 函数用于从当前窗口切换到对象中的新窗口。由于 ddi_dma_getwin() 会将系统资源重新分配给新窗口,因此前面的窗口将变为无效。
在向当前窗口的传送完成之前,请勿通过调用 ddi_dma_getwin() 来移动 DMA 窗口。请一直等待,直至向当前窗口的传送完成为止,即出现中断的时候。然后,调用 ddi_dma_getwin() 以避免数据损坏。
通常从中断例程中调用 ddi_dma_getwin() 函数,如示例 9–8 中所示。调用驱动程序会导致启动第一个 DMA 传送。后续传送将从中断例程中启动。
中断例程会检查设备的状态,以确定设备是否已成功完成传送。如果未成功完成传送,则会进行正常错误恢复。如果传送成功,例程必须确定逻辑传送是否已完成。完整的传送包括 buf(9S) 结构所指定的整个对象。在部分传送中,仅会移动一个 DMA 窗口。在部分传送中,中断例程将使用 ddi_dma_getwin(9F) 移动窗口、检索新 cookie 并启动其他 DMA 传送。
如果逻辑请求已完成,则中断例程将检查待处理的请求。如有必要,中断例程会启动传送。否则,例程将返回,而不调用其他 DMA 传送。以下示例说明了常见的流程控制。
static uint_t xxintr(caddr_t arg) { struct xxstate *xsp = (struct xxstate *)arg; uint8_t status; volatile uint8_t temp; mutex_enter(&xsp->mu); /* read status */ status = ddi_get8(xsp->access_hdl, &xsp->regp->csr); if (!(status & INTERRUPTING)) { mutex_exit(&xsp->mu); return (DDI_INTR_UNCLAIMED); } ddi_put8(xsp->access_hdl,&xsp->regp->csr, CLEAR_INTERRUPT); /* for store buffers */ temp = ddi_get8(xsp->access_hdl, &xsp->regp->csr); if ( /* an error occurred during transfer */ ) { bioerror(xsp->bp, EIO); xsp->partial = 0; } else { xsp->bp->b_resid -= /* amount transferred */ ; } if (xsp->partial && (++xsp->windex < xsp->nwin)) { /* device still marked busy to protect state */ mutex_exit(&xsp->mu); (void) ddi_dma_getwin(xsp->handle, xsp->windex, &offset, &len, &cookie, &ccount); /* Program the DMA engine with the new cookie(s). */ return (DDI_INTR_CLAIMED); } ddi_dma_unbind_handle(xsp->handle); biodone(xsp->bp); xsp->busy = 0; xsp->partial = 0; mutex_exit(&xsp->mu); if ( /* pending transfers */ ) { (void) xxstart((caddr_t)xsp); } return (DDI_INTR_CLAIMED); }
一些设备驱动程序允许应用程序通过 mmap(2) 访问设备或内核内存。例如,帧缓存器驱动程序允许将帧缓存器映射到用户线程中。另一个示例是使用共享的内核内存池与应用程序通信的伪驱动程序。本章介绍有关以下主题的信息:
驱动程序必须采取如下步骤才能导出设备或内核内存:
在 cb_ops(9S) 结构的 cb_flag 标志中设置 D_DEVMAP 标志。
定义 devmap(9E) 驱动程序入口点并视需要定义 segmap(9E) 入口点,以导出映射。
使用 devmap_devmem_setup(9F) 设置到设备的用户映射。要设置到内核内存的用户映射,请使用 devmap_umem_setup(9F)。
本节介绍如何使用 segmap(9E) 和 devmap(9E) 入口点。
segmap(9E) 入口点负责设置 mmap(2) 系统调用所请求的内存映射。许多内存映射设备的驱动程序使用 ddi_devmap_segmap(9F) 作为入口点,而不是定义自己的 segmap(9E) 例程。通过提供 segmap () 入口点,驱动程序能够在创建映射之前或之后管理常规任务。例如,驱动程序可以检查映射权限并分配专用映射资源。驱动程序还可以调整映射,以适应非按页对齐的设备缓冲区。segmap() 入口点在返回之前必须调用 ddi_devmap_segmap(9F) 函数。ddi_devmap_segmap() 函数调用驱动程序的 devmap(9E) 入口点以执行实际映射。
segmap() 函数的语法如下:
int segmap(dev_t dev, off_t off, struct as *asp, caddr_t *addrp, off_t len, unsigned int prot, unsigned int maxprot, unsigned int flags, cred_t *credp);
其中:
要映射其内存的设备。
设备内存中的偏移,映射将从此位置开始。
指向设备内存将要映射到的地址空间的指针。
请注意,此参数可以是 struct as *(如示例 10–1 中所示),也可以是 ddi_as_handle_t(如示例 10–2 中所示)。这是因为 ddidevmap.h 包括以下声明:
typedef struct as *ddi_as_handle_t
指向设备内存将要映射到的地址空间中的地址的指针。
所映射的内存的长度(以字节为单位)。
指定保护的位字段。可能的设置有 PROT_READ、PROT_WRITE、PROT_EXEC、PROT_USER 和 PROT_ALL。有关详细信息,请参见手册页。
尝试的映射可用的最大保护标志。如果用户打开只读的特殊文件,则 PROT_WRITE 位可能会被屏蔽。
指示映射类型的标志。可能的值包括 MAP_SHARED 和 MAP_PRIVATE。
指向用户凭证结构的指针。
在以下示例中,驱动程序控制允许只写映射的帧缓存器。如果应用程序尝试进行读取访问,并随后调用 ddi_devmap_segmap(9F) 来设置用户映射,则驱动程序返回 EINVAL。
static int xxsegmap(dev_t dev, off_t off, struct as *asp, caddr_t *addrp, off_t len, unsigned int prot, unsigned int maxprot, unsigned int flags, cred_t *credp) { if (prot & PROT_READ) return (EINVAL); return (ddi_devmap_segmap(dev, off, as, addrp, len, prot, maxprot, flags, cred)); }
以下示例说明如何处理其缓冲区未在寄存器空间中按页对齐的设备。本示例将映射从偏移 0x800 开始的缓冲区,因此 mmap(2) 会返回与该缓冲区起始位置对应的地址。devmap_devmem_setup(9F) 函数映射整页,要求映射按页对齐,并返回页起始位置的地址。如果此地址是通过 segmap(9E) 传递的,或者未定义 segmap() 入口点,则 mmap() 将返回对应于页起始位置的地址,而不是对应于缓冲区起始位置的地址。在本示例中,缓冲区偏移将与 devmap_devmem_setup 返回的按页对齐地址相加,因此,得到的返回地址就是所需的缓冲区起始地址。
#define BUFFER_OFFSET 0x800 int xx_segmap(dev_t dev, off_t off, ddi_as_handle_t as, caddr_t *addrp, off_t len, uint_t prot, uint_t maxprot, uint_t flags, cred_t *credp) { int rval; unsigned long pagemask = ptob(1L) - 1L; if ((rval = ddi_devmap_segmap(dev, off, as, addrp, len, prot, maxprot, flags, credp)) == DDI_SUCCESS) { /* * The address returned by ddi_devmap_segmap is the start of the page * that contains the buffer. Add the offset of the buffer to get the * final address. */ *addrp += BUFFER_OFFSET & pagemask); } return (rval); }
devmap(9E) 入口点通过 segmap(9E) 入口点内部的 ddi_devmap_segmap(9F) 函数调用。
devmap(9E) 入口点是作为 mmap(2) 系统调用的结果调用的。调用 devmap(9E) 函数可将设备内存或内核内存导出到用户应用程序。devmap() 函数用于进行以下操作:
验证用户到设备内存或内核内存的映射
将应用程序映射中的逻辑偏移转换为设备或内核内存中的对应偏移
将映射信息传递给系统以设置映射
devmap() 函数的语法如下所示:
int devmap(dev_t dev, devmap_cookie_t handle, offset_t off, size_t len, size_t *maplen, uint_t model);
其中:
要映射其内存的设备。
系统创建的设备映射句柄,用来描述到设备或内核中的连续内存的映射。
应用程序映射中的逻辑偏移,必须通过驱动程序将其转换为设备或内核内存中的对应偏移。
所映射的内存的长度(以字节为单位)。
使驱动程序可将不同的内核内存区域或多个物理上不连续的内存区域与一个连续的用户应用程序映射相关联。
当前线程的数据模型类型。
系统在一次 mmap(2) 系统调用中将创建多个映射句柄。例如,映射可能包含多个物理上不连续的内存区域。
devmap(9E) 的首次调用使用参数 off 和 len 的初始值。应用程序将这些参数传递给 mmap(2)。devmap(9E) 将 *maplen 设置为从 off 到连续内存区域结尾之间的长度。*maplen 值必须向上进位为页面大小的倍数。 *maplen 值可被设置为小于原始映射长度 len。如果这样,系统将使用调整了 off 和 len 参数的新映射句柄反复调用 devmap(9E),直到达到初始映射长度为止。
如果一个驱动程序支持多个应用程序数据模型,则必须将 model 传递给 ddi_model_convert_from(9F)。ddi_model_convert_from() 函数可以确定当前线程与设备驱动程序之间是否存在数据模型不匹配的情况。设备驱动程序可能必须调整数据结构的形状,然后才能将结构导出到支持不同数据模型的用户线程。有关更多详细信息,请参见附录 C。
如果逻辑偏移 off 超出了驱动程序导出的内存范围,则 devmap(9E) 入口点必将返回 -1。
通过驱动程序的 devmap(9E) 入口点调用 devmap_devmem_setup(9F) 可将设备内存导出到用户应用程序。
devmap_devmem_setup(9F) 函数的语法如下所示:
int devmap_devmem_setup(devmap_cookie_t handle, dev_info_t *dip, struct devmap_callback_ctl *callbackops, uint_t rnumber, offset_t roff, size_t len, uint_t maxprot, uint_t flags, ddi_device_acc_attr_t *accattrp);
其中:
系统用来标识映射的不透明的设备映射句柄。
指向设备的 dev_info 结构的指针。
指向 devmap_callback_ctl(9S) 结构的指针,此指针可在映射时向驱动程序通知用户事件。
寄存器地址空间集的索引号。
在设备内存中的偏移。
导出的长度(以字节为单位)。
允许驱动程序为导出的设备内存中的不同区域指定不同的保护。
必须设置为 DEVMAP_DEFAULTS。
指向 ddi_device_acc_attr(9S) 结构的指针。
roff 和 len 参数描述了寄存器集 rnumber 指定的设备内存中的一个范围。reg 属性用于描述 rnumber 所引用的寄存器规格。对于只有一个寄存器集的设备,将 rnumber 设置为 0 即可。范围通过 roff 和 len 定义。如果用户的应用程序映射位于通过 devmap(9E) 入口点传入的 offset 位置上,则可对此范围进行访问。驱动程序通常将 devmap(9E) 偏移直接传递给 devmap_devmem_setup(9F)。然后,mmap(2) 的返回地址将映射到寄存器集的起始地址。
通过 maxprot 参数,驱动程序可为导出的设备内存中的不同区域指定不同保护。例如,要禁止对某个区域进行写访问,可以只为该区域设置 PROT_READ 和 PROT_USER。
以下示例说明如何将设备内存导出到应用程序。驱动程序首先确定请求的映射是否位于设备内存区域之内。设备内存的大小通过使用 ddi_dev_regsize(9F) 来确定。使用 ptob(9F) 和 btopr(9F) 可将映射的长度向上舍入为页面大小的倍数。然后调用 devmap_devmem_setup(9F) 可将设备内存导出到应用程序。
static int xxdevmap(dev_t dev, devmap_cookie_t handle, offset_t off, size_t len, size_t *maplen, uint_t model) { struct xxstate *xsp; int error, rnumber; off_t regsize; /* Set up data access attribute structure */ struct ddi_device_acc_attr xx_acc_attr = { DDI_DEVICE_ATTR_V0, DDI_NEVERSWAP_ACC, DDI_STRICTORDER_ACC }; xsp = ddi_get_soft_state(statep, getminor(dev)); if (xsp == NULL) return (-1); /* use register set 0 */ rnumber = 0; /* get size of register set */ if (ddi_dev_regsize(xsp->dip, rnumber, ®size) != DDI_SUCCESS) return (-1); /* round up len to a multiple of a page size */ len = ptob(btopr(len)); if (off + len > regsize) return (-1); /* Set up the device mapping */ error = devmap_devmem_setup(handle, xsp->dip, NULL, rnumber, off, len, PROT_ALL, DEVMAP_DEFAULTS, &xx_acc_attr); /* acknowledge the entire range */ *maplen = len; return (error); }
一些设备驱动程序可能需要分配可供用户程序通过 mmap(2) 进行访问的内核内存。一个示例是为两个应用程序间的通信设置共享内存。另一个示例是在驱动程序和应用程序之间共享内存。
将内核内存导出到用户应用程序时,请执行以下步骤:
使用 ddi_umem_alloc(9F) 分配内核内存。
使用 devmap_umem_setup(9F) 导出内存。
不再需要内存时,使用 ddi_umem_free(9F) 释放内存。
使用 ddi_umem_alloc(9F) 可以分配导出到应用程序的内核内存。 ddi_umem_alloc() 的语法如下所示:
void *ddi_umem_alloc(size_t size, int flag, ddi_umem_cookie_t *cookiep);
其中:
要分配的字节数。
用于确定休眠条件和内存类型。
指向内核内存 cookie 的指针。
ddi_umem_alloc(9F) 分配按页对齐的内核内存。ddi_umem_alloc () 返回一个指向所分配内存的指针。最初,内存被零填充。分配的字节数是系统页面大小的倍数,该页面大小是通过 size 参数向上舍入得到的。分配的内存可在内核中使用。此内存也可导出到应用程序。cookiep 是指向用来描述所分配的内核内存的内核内存 cookie 的指针。驱动程序将内核内存导出到用户应用程序时,devmap_umem_setup(9F) 中会使用 cookiep。
flag 参数用于指示 ddi_umem_alloc(9F) 是立即阻塞还是返回,以及分配的内核内存是否可换页。flag 参数的值如下所示:
驱动程序无需等待内存成为可用。如果内存不可用,则返回 NULL。
驱动程序可以无限等待,直到内存可用为止。
驱动程序允许内存页被换出。如果未设置,则锁定内存。
ddi_umem_lock() 函数可以执行设备锁定内存检查。此函数针对 project.max-locked-memory 中指定的限制值进行检查。如果当前项目的锁定内存使用量低于限制,则会增加项目的锁定内存字节计数。进行限制检查后,内存将会锁定。ddi_umem_unlock() 函数可以解除锁定内存,从而减少项目的锁定内存字节计数。
其中所用的记帐方法是不严密的 "full price"(足价)模式。例如,对于同一项目中 umem_lockmemory() 的具有重叠内存区域的两个调用程序会被计数两次。
有关 project.max-locked-memory 和 zone.max-locked_memory 对安装了区域的 Solaris 系统的资源控制的信息,请参见《Solaris 10 资源管理器开发者指南》和 resource_controls(5)。
以下示例说明如何为应用程序访问分配内核内存。驱动程序会导出一页内核内存,它将被多个应用程序用作共享存储区。应用程序第一次映射共享页时,会在 segmap(9E) 中分配内存。如果驱动程序必须支持多个应用程序数据模型,则会再分配一页。例如,64 位驱动程序可能同时将内存导出到 64 位应用程序和 32 位应用程序。64 位应用程序共享第一页,32 位应用程序共享第二页。
static int xxsegmap(dev_t dev, off_t off, struct as *asp, caddr_t *addrp, off_t len, unsigned int prot, unsigned int maxprot, unsigned int flags, cred_t *credp) { int error; minor_t instance = getminor(dev); struct xxstate *xsp = ddi_get_soft_state(statep, instance); size_t mem_size; /* 64-bit driver supports 64-bit and 32-bit applications */ switch (ddi_mmap_get_model()) { case DDI_MODEL_LP64: mem_size = ptob(2); break; case DDI_MODEL_ILP32: mem_size = ptob(1); break; } mutex_enter(&xsp->mu); if (xsp->umem == NULL) { /* allocate the shared area as kernel pageable memory */ xsp->umem = ddi_umem_alloc(mem_size, DDI_UMEM_SLEEP | DDI_UMEM_PAGEABLE, &xsp->ucookie); } mutex_exit(&xsp->mu); /* Set up the user mapping */ error = devmap_setup(dev, (offset_t)off, asp, addrp, len, prot, maxprot, flags, credp); return (error); }
使用 devmap_umem_setup(9F) 可将内核内存导出到用户应用程序。devmap_umem_setup() 必须通过驱动程序的 devmap(9E) 入口点进行调用 。devmap_umem_setup() 的语法如下所示:
int devmap_umem_setup(devmap_cookie_t handle, dev_info_t *dip, struct devmap_callback_ctl *callbackops, ddi_umem_cookie_t cookie, offset_t koff, size_t len, uint_t maxprot, uint_t flags, ddi_device_acc_attr_t *accattrp);
其中:
用于描述映射的不透明结构。
指向设备的 dev_info 结构的指针。
指向 devmap_callback_ctl(9S) 结构的指针。
ddi_umem_alloc(9F) 返回的内核内存 cookie。
cookie 指定的内核内存中的偏移。
导出的长度(以字节为单位)。
用于为导出的映射指定可能的最大保护。
必须设置为 DEVMAP_DEFAULTS。
指向 ddi_device_acc_attr(9S) 结构的指针。
handle 是系统用来标识映射的设备映射句柄。handle 通过 devmap(9E) 入口点传入。dip 是指向设备的 dev_info 结构的指针。callbackops 允许向驱动程序通知有关映射的用户事件。导出内核内存时,大多数驱动程序都会将 callbackops 设置为 NULL。
koff 和 len 用于在 ddi_umem_alloc(9F) 分配的内核内存中指定一个范围。如果用户的应用程序映射位于通过 devmap(9E) 入口点传入的偏移上,则可对此范围进行访问。驱动程序通常将 devmap(9E) 偏移直接传递给 devmap_umem_setup(9F)。然后,mmap(2) 的返回地址将映射到 ddi_umem_alloc(9F) 返回的内核地址。koff 和 len 必须按页对齐。
通过 maxprot,驱动程序可为导出的内核内存中的不同区域指定不同的保护。例如,通过仅设置 PROT_READ 和 PROT_USER,一个区域可能不允许写访问。
以下示例说明如何将内核内存导出到应用程序。驱动程序首先检查请求的映射是否位于分配的内核内存区域之内。如果 64 位驱动程序收到来自 32 位应用程序的映射请求,则会将该请求重定向到内核存储区的第二页。此重定向可确保仅有编译到相同数据模型的应用程序才能共享相同的页。
static int xxdevmap(dev_t dev, devmap_cookie_t handle, offset_t off, size_t len, size_t *maplen, uint_t model) { struct xxstate *xsp; int error; /* round up len to a multiple of a page size */ len = ptob(btopr(len)); /* check if the requested range is ok */ if (off + len > ptob(1)) return (ENXIO); xsp = ddi_get_soft_state(statep, getminor(dev)); if (xsp == NULL) return (ENXIO); if (ddi_model_convert_from(model) == DDI_MODEL_ILP32) /* request from 32-bit application. Skip first page */ off += ptob(1); /* export the memory to the application */ error = devmap_umem_setup(handle, xsp->dip, NULL, xsp->ucookie, off, len, PROT_ALL, DEVMAP_DEFAULTS, NULL); *maplen = len; return (error); }
卸载驱动程序时,必须通过调用 ddi_umem_free(9F) 释放 ddi_umem_alloc(9F) 分配的内存。
void ddi_umem_free(ddi_umem_cookie_t cookie);
cookie 是 ddi_umem_alloc(9F) 返回的内核内存 cookie。
一些设备驱动程序(如用于图形硬件的驱动程序)可为用户进程提供对设备的直接访问。这些设备通常要求一次仅有一个进程访问设备。
本章介绍了可供设备驱动程序用于管理对此类设备的访问的接口集。本章提供有关以下主题的信息:
本节介绍设备上下文和上下文管理模型。
设备的上下文是指设备硬件的当前状态。设备驱动程序可代表进程管理该进程的设备上下文。驱动程序必须分别为访问设备的每个进程保留单独的设备上下文。设备驱动程序负责在进程访问设备时恢复正确的设备上下文。
帧缓存器可作为设备上下文管理的一个很好的示例。使用加速的帧缓存器,用户进程可以通过内存映射访问直接处理设备的控制寄存器。由于这些进程不使用传统的系统调用,因此访问设备的进程无需调用设备驱动程序。但是,如果进程要访问设备,则必须通知设备驱动程序。驱动程序需要恢复正确的设备上下文并且提供所需的任何同步。
要解决这一问题,可以使用设备上下文管理接口,在用户进程访问设备的内存映射区域时通知设备驱动程序,并控制对设备硬件的访问。设备驱动程序负责同步和管理各种设备上下文。用户进程访问映射时,设备驱动程序必须为该进程恢复正确的设备上下文。
每次用户进程执行以下任一操作时,都会通知设备驱动程序:
下图显示了映射到一个设备的内存中的多个用户进程。驱动程序授予了进程 B 对设备的访问权限,进程 B 不再向驱动程序通知访问情况。但是,如果进程 A 或进程 C 访问设备,仍会通知驱动程序。
在将来某一时刻,进程 A 将访问设备。设备驱动程序会得到通知,并阻止进程 B 将来对该设备的访问。然后,驱动程序会为进程 B 保存设备上下文。驱动程序恢复进程 A 的设备上下文,然后授予进程 A 访问权限,如下图所示。此时,如果进程 B 或进程 C 访问设备,则会通知设备驱动程序。
在多处理器计算机中,多个进程可能会尝试同时访问设备。此情况会引起抖动。有些设备需要较长的时间才能恢复设备上下文。要防止恢复设备上下文所需的 CPU 时间超过实际使用该设备上下文所需的时间,可以使用 devmap_set_ctx_timeout(9F) 设置进程访问设备所需的最短时间。
内核可以保证一旦设备驱动程序向某一进程授予了访问权限,便不允许其他任何进程在 devmap_set_ctx_timeout(9F) 指定的时间间隔内请求访问同一设备。
定义 devmap_callback_ctl(9S) 结构。
根据需要分配用于保存设备上下文的空间。
通过 devmap_devmem_setup(9F) 设置到设备的用户映射和驱动程序通知。
通过 devmap_load(9F) 和 devmap_unload(9F) 管理用户对设备的访问。
根据需要释放设备上下文结构。
设备驱动程序必须分配并初始化 devmap_callback_ctl(9S) 结构,以便通知系统用于设备上下文管理的入口点例程。
此结构使用以下语法:
struct devmap_callback_ctl { int devmap_rev; int (*devmap_map)(devmap_cookie_t dhp, dev_t dev, uint_t flags, offset_t off, size_t len, void **pvtp); int (*devmap_access)(devmap_cookie_t dhp, void *pvtp, offset_t off, size_t len, uint_t type, uint_t rw); int (*devmap_dup)(devmap_cookie_t dhp, void *pvtp, devmap_cookie_t new_dhp, void **new_pvtp); void (*devmap_unmap)(devmap_cookie_t dhp, void *pvtp, offset_t off, size_t len, devmap_cookie_t new_dhp1, void **new_pvtp1, devmap_cookie_t new_dhp2, void **new_pvtp2); };
devmap_callback_ctl 结构的版本号。版本号必须设置为 DEVMAP_OPS_REV。
必须设置为驱动程序的 devmap_map(9E) 入口点的地址。
必须设置为驱动程序的 devmap_access(9E) 入口点的地址。
必须设置为驱动程序的 devmap_dup(9E) 入口点的地址。
必须设置为驱动程序的 devmap_unmap(9E) 入口点的地址。
devmap(9E) 的语法如下所示:
int xxdevmap_map(devmap_cookie_t handle, dev_t dev, uint_t flags, offset_t offset, size_t len, void **new-devprivate);
驱动程序从其 devmap() 入口点返回并且系统已建立到设备内存的用户映射后,将会调用 devmap_map() 入口点。通过 devmap() 入口点,驱动程序可以执行其他处理操作或分配特定于映射的专用数据。例如,为了支持上下文切换,驱动程序必须分配上下文结构。然后,驱动程序必须将上下文结构与映射关联。
系统期望驱动程序在 *new-devprivate 中返回一个指向分配的专用数据的指针。驱动程序必须存储用于定义专用数据中的映射范围的 offset 和 len。然后当系统调用 devmap_unmap(9E) 时,驱动程序将使用此信息来确定要取消映射的映射量。
flags 指示驱动程序是否应为映射分配专用上下文。例如,如果 flags 设置为 MAP_PRIVATE,则驱动程序可以分配用于存储设备上下文的内存区域。如果设置了 MAP_SHARED,驱动程序将返回指向共享区域的指针。
以下示例说明了 devmap() 入口点。驱动程序分配了一个新的上下文结构。然后,驱动程序便可保存通过入口点传入的相关参数。接下来,将通过分配或通过将映射附加至已经存在的共享上下文来为映射指定新的上下文。映射访问设备的最短时间间隔设置为 1 毫秒。
static int int xxdevmap_map(devmap_cookie_t handle, dev_t dev, uint_t flags, offset_t offset, size_t len, void **new_devprivate) { struct xxstate *xsp = ddi_get_soft_state(statep, getminor(dev)); struct xxctx *newctx; /* create a new context structure */ newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP); newctx->xsp = xsp; newctx->handle = handle; newctx->offset = offset; newctx->flags = flags; newctx->len = len; mutex_enter(&xsp->ctx_lock); if (flags & MAP_PRIVATE) { /* allocate a private context and initialize it */ newctx->context = kmem_alloc(XXCTX_SIZE, KM_SLEEP); xxctxinit(newctx); } else { /* set a pointer to the shared context */ newctx->context = xsp->ctx_shared; } mutex_exit(&xsp->ctx_lock); /* give at least 1 ms access before context switching */ devmap_set_ctx_timeout(handle, drv_usectohz(1000)); /* return the context structure */ *new_devprivate = newctx; return(0); }
对转换无效的映射进行访问时,将会调用 devmap_access(9E) 入口点。映射转换在以下几种情况下无效:作为对 mmap(2) 的响应通过 devmap_devmem_setup(9F) 创建映射;通过 fork(2) 复制映射或通过调用 devmap_unload(9F) 显式使映射无效。
devmap_access() 的语法如下所示:
int xxdevmap_access(devmap_cookie_t handle, void *devprivate, offset_t offset, size_t len, uint_t type, uint_t rw);
其中:
用户进程所访问的映射的映射句柄。
指向与映射关联的驱动程序专用数据的指针。
所访问映射内的偏移。
所访问内存的长度(以字节为单位)。
访问操作的类型。
用于指定访问的方向。
系统期望 devmap_access(9E) 调用 devmap_do_ctxmgt(9F) 或 devmap_default_access(9F) 以便在 devmap_access() 返回前装入内存地址转换。对于支持上下文切换的映射,设备驱动程序应调用 devmap_do_ctxmgt()。系统会通过 devmap_access(9E) 向此例程传递所有参数以及指向驱动程序入口点 devmap_contextmgt(9E) 的指针,该指针用来处理上下文切换。对于不支持上下文切换的映射,驱动程序应调用 devmap_default_access(9F)。devmap_default_access() 的用途是调用 devmap_load(9F) 以装入用户转换。
以下示例说明了 devmap_access(9E) 入口点。该映射分为两个区域。在偏移 OFF_CTXMG 上开始并且长度为 CTXMGT_SIZE 字节的区域支持上下文管理。其余映射支持缺省访问。
#define OFF_CTXMG 0 #define CTXMGT_SIZE 0x20000 static int xxdevmap_access(devmap_cookie_t handle, void *devprivate, offset_t off, size_t len, uint_t type, uint_t rw) { offset_t diff; int error; if ((diff = off - OFF_CTXMG) >= 0 && diff < CTXMGT_SIZE) { error = devmap_do_ctxmgt(handle, devprivate, off, len, type, rw, xxdevmap_contextmgt); } else { error = devmap_default_access(handle, devprivate, off, len, type, rw); } return (error); }
devmap_contextmgt(9E) 的语法如下所示:
int xxdevmap_contextmgt(devmap_cookie_t handle, void *devprivate, offset_t offset, size_t len, uint_t type, uint_t rw);
devmap_contextmgt() 应使用当前对设备具有访问权限的映射的句柄调用 devmap_unload(9F)。此方法可使对于该映射的转换无效。通过此方法,可确保下次访问当前映射时针对该映射调用 devmap_access(9E)。对于引起访问事件发生的映射,需要验证其映射转换。相应地,驱动程序必须为进程请求访问恢复设备上下文。并且,驱动程序必须针对映射的 handle 调用 devmap_load(9F),该映射生成了对此入口点的调用。
访问已通过调用 devmap_load() 对映射转换进行验证的部分映射时不会导致调用 devmap_access()。对 devmap_unload() 的后续调用将使映射转换无效。通过此调用,可再次调用 devmap_access()。
如果 devmap_load() 或 devmap_unload() 返回错误,devmap_contextmgt() 应立即返回该错误。如果设备驱动程序在恢复设备上下文时遇到硬件故障,则应返回 -1。否则,成功处理访问请求后,devmap_contextmgt() 应返回零。如果从 devmap_contextmgt() 返回非零值,则会向进程发送 SIGBUS 或 SIGSEGV。
以下示例说明如何管理单页设备上下文。
xxctxsave() 和 xxctxrestore() 是与设备相关的上下文保存和恢复函数。xxctxsave() 从寄存器中读取数据并将数据保存在软状态结构中。xxctxrestore() 提取软状态结构中保存的数据并将数据写入设备寄存器。请注意,执行读取、写入和保存都需要使用 DDI/DKI 数据访问例程。
static int xxdevmap_contextmgt(devmap_cookie_t handle, void *devprivate, offset_t off, size_t len, uint_t type, uint_t rw) { int error; struct xxctx *ctxp = devprivate; struct xxstate *xsp = ctxp->xsp; mutex_enter(&xsp->ctx_lock); /* unload mapping for current context */ if (xsp->current_ctx != NULL) { if ((error = devmap_unload(xsp->current_ctx->handle, off, len)) != 0) { xsp->current_ctx = NULL; mutex_exit(&xsp->ctx_lock); return (error); } } /* Switch device context - device dependent */ if (xxctxsave(xsp->current_ctx, off, len) < 0) { xsp->current_ctx = NULL; mutex_exit(&xsp->ctx_lock); return (-1); } if (xxctxrestore(ctxp, off, len) < 0){ xsp->current_ctx = NULL; mutex_exit(&xsp->ctx_lock); return (-1); } xsp->current_ctx = ctxp; /* establish mapping for new context and return */ error = devmap_load(handle, off, len, type, rw); if (error) xsp->current_ctx = NULL; mutex_exit(&xsp->ctx_lock); return (error); }
复制设备映射(例如,由调用 fork(2) 的用户进程进行复制)时,将会调用 devmap_dup(9E) 入口点。驱动程序预期会为新映射生成新的驱动程序专用数据。
devmap_dup() 的语法如下所示:
int xxdevmap_dup(devmap_cookie_t handle, void *devprivate, devmap_cookie_t new-handle, void **new-devprivate);
其中:
正在复制的映射的映射句柄。
已复制的映射的映射句柄。
指向与正在复制的映射关联的驱动程序专用数据的指针。
应设置为指向用于新映射的新驱动程序专用数据的指针。
缺省情况下使用 devmap_dup() 所创建的映射会使其映射转换无效。第一次访问映射时,无效的映射转换会强制调用 devmap_access(9E) 入口点。
以下示例说明了一个典型的 devmap_dup() 例程。
static int xxdevmap_dup(devmap_cookie_t handle, void *devprivate, devmap_cookie_t new_handle, void **new_devprivate) { struct xxctx *ctxp = devprivate; struct xxstate *xsp = ctxp->xsp; struct xxctx *newctx; /* Create a new context for the duplicated mapping */ newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP); newctx->xsp = xsp; newctx->handle = new_handle; newctx->offset = ctxp->offset; newctx->flags = ctxp->flags; newctx->len = ctxp->len; mutex_enter(&xsp->ctx_lock); if (ctxp->flags & MAP_PRIVATE) { newctx->context = kmem_alloc(XXCTX_SIZE, KM_SLEEP); bcopy(ctxp->context, newctx->context, XXCTX_SIZE); } else { newctx->context = xsp->ctx_shared; } mutex_exit(&xsp->ctx_lock); *new_devprivate = newctx; return(0); }
对映射取消映射时,将会调用 devmap_unmap(9E) 入口点。用户进程退出或调用 munmap(2) 系统调用会导致取消映射。
devmap_unmap() 的语法如下所示:
void xxdevmap_unmap(devmap_cookie_t handle, void *devprivate, offset_t off, size_t len, devmap_cookie_t new-handle1, void **new-devprivate1, devmap_cookie_t new-handle2, void **new-devprivate2);
其中:
正在释放的映射的映射句柄。
指向与映射关联的驱动程序专用数据的指针。
逻辑设备内存中取消映射开始处的偏移。
所取消映射的内存的长度(以字节为单位)。
系统用来描述新区域的句柄,该新区域在 off - 1 位置结束。new-handle1 的值可以为 NULL。
要由驱动程序通过用于新区域的专用驱动程序映射数据进行填充的指针,该新区域在 off -1 位置结束。如果 new-handle1 为 NULL,则会忽略 new-devprivate1。
系统用来描述新区域的句柄,该新区域在 off + len 位置开始。new-handle2 的值可以为 NULL。
要由驱动程序通过用于新区域的驱动程序专用映射数据进行填充的指针,该新区域在 off + len 位置开始。如果 new-handle2 为 NULL,则会忽略 new-devprivate2。
devmap_unmap() 例程预期会释放通过 devmap_map(9E) 或 devmap_dup(9E) 创建此映射时分配的任何驱动程序专用资源。如果只是取消映射部分映射,则驱动程序必须在释放旧的专用数据之前为其余映射分配新的专用数据。不必针对已释放的映射的句柄调用 devmap_unload(9F),即使此句柄指向具有有效转换的映射时也是如此。不过,为了避免将来出现 devmap_access(9E) 问题,设备驱动程序应确保当前的映射表示形式设置为“无当前映射”。
以下示例说明了一个典型的 devmap_unmap() 例程。
static void xxdevmap_unmap(devmap_cookie_t handle, void *devprivate, offset_t off, size_t len, devmap_cookie_t new_handle1, void **new_devprivate1, devmap_cookie_t new_handle2, void **new_devprivate2) { struct xxctx *ctxp = devprivate; struct xxstate *xsp = ctxp->xsp; mutex_enter(&xsp->ctx_lock); /* * If new_handle1 is not NULL, we are unmapping * at the end of the mapping. */ if (new_handle1 != NULL) { /* Create a new context structure for the mapping */ newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP); newctx->xsp = xsp; if (ctxp->flags & MAP_PRIVATE) { /* allocate memory for the private context and copy it */ newctx->context = kmem_alloc(XXCTX_SIZE, KM_SLEEP); bcopy(ctxp->context, newctx->context, XXCTX_SIZE); } else { /* point to the shared context */ newctx->context = xsp->ctx_shared; } newctx->handle = new_handle1; newctx->offset = ctxp->offset; newctx->len = off - ctxp->offset; *new_devprivate1 = newctx; } /* * If new_handle2 is not NULL, we are unmapping * at the beginning of the mapping. */ if (new_handle2 != NULL) { /* Create a new context for the mapping */ newctx = kmem_alloc(sizeof (struct xxctx), KM_SLEEP); newctx->xsp = xsp; if (ctxp->flags & MAP_PRIVATE) { newctx->context = kmem_alloc(XXCTX_SIZE, KM_SLEEP); bcopy(ctxp->context, newctx->context, XXCTX_SIZE); } else { newctx->context = xsp->ctx_shared; } newctx->handle = new_handle2; newctx->offset = off + len; newctx->flags = ctxp->flags; newctx->len = ctxp->len - (off + len - ctxp->off); *new_devprivate2 = newctx; } if (xsp->current_ctx == ctxp) xsp->current_ctx = NULL; mutex_exit(&xsp->ctx_lock); if (ctxp->flags & MAP_PRIVATE) kmem_free(ctxp->context, XXCTX_SIZE); kmem_free(ctxp, sizeof (struct xxctx)); }
用户进程通过 mmap(2) 请求到设备的映射时,将会调用驱动程序的 segmap(9E) 入口点。如果驱动程序需要管理设备上下文,则在设置内存映射时,驱动程序必须使用 ddi_devmap_segmap(9F) 或 devmap_setup(9F)。这两个函数都会调用驱动程序的 devmap(9E) 入口点,该入口点使用 devmap_devmem_setup(9F) 将设备内存与用户映射关联。有关如何映射设备内存的详细信息,请参见第 10 章。
驱动程序必须向系统通知 devmap_callback_ctl(9S) 入口点才能获取对用户映射的访问通知。驱动程序通过向 devmap_devmem_setup(9F) 提供一个指向 devmap_callback_ctl(9S) 结构的指针来通知系统。devmap_callback_ctl(9S) 结构描述了一组用于上下文管理的入口点。系统通过调用这些入口点来通知设备驱动程序管理有关设备映射的事件。
系统会将每个映射与一个映射句柄关联。此句柄会传递给每个用于上下文管理的入口点。该映射句柄可用来使映射转换无效和对映射转换进行验证。如果驱动程序使映射转换无效,则会向该驱动程序通知将来对映射的任何访问。如果驱动程序对映射转换进行验证,则不再向该驱动程序通知对映射的访问。映射总是在映射转换无效的情况下创建,以便第一次访问映射时将会通知驱动程序。
以下示例说明如何使用设备上下文管理接口设置映射。
static struct devmap_callback_ctl xx_callback_ctl = { DEVMAP_OPS_REV, xxdevmap_map, xxdevmap_access, xxdevmap_dup, xxdevmap_unmap }; static int xxdevmap(dev_t dev, devmap_cookie_t handle, offset_t off, size_t len, size_t *maplen, uint_t model) { struct xxstate *xsp; uint_t rnumber; int error; /* Setup data access attribute structure */ struct ddi_device_acc_attr xx_acc_attr = { DDI_DEVICE_ATTR_V0, DDI_NEVERSWAP_ACC, DDI_STRICTORDER_ACC }; xsp = ddi_get_soft_state(statep, getminor(dev)); if (xsp == NULL) return (ENXIO); len = ptob(btopr(len)); rnumber = 0; /* Set up the device mapping */ error = devmap_devmem_setup(handle, xsp->dip, &xx_callback_ctl, rnumber, off, len, PROT_ALL, 0, &xx_acc_attr); *maplen = len; return (error); }
用户进程访问没有有效的映射转换的内存映射区域中的地址时,将会通知设备驱动程序。访问事件发生时,必须使当前对设备具有访问权限的进程的映射转换无效。必须恢复请求访问设备的进程的设备上下文。并且,必须对请求访问的进程的映射转换进行验证。
函数 devmap_load(9F) 和 devmap_unload(9F) 用于验证映射转换和使其无效。
devmap_load(9F) 的语法如下所示:
int devmap_load(devmap_cookie_t handle, offset_t offset, size_t len, uint_t type, uint_t rw);
devmap_load() 可以验证对于 handle、offset 和 len 指定的映射页的映射转换。通过验证对这些页的映射转换,驱动程序将告知系统不要拦截对这些映射页的访问。并且,系统不得在未通知设备驱动程序的情况下允许继续进行访问。
必须通过映射的偏移和句柄调用 devmap_load(),该映射可生成访问事件以便完成访问。如果不针对此句柄调用 devmap_load(9F),则不会验证映射转换,并且进程将收到 SIGBUS。
devmap_unload(9F) 的语法如下所示:
int devmap_unload(devmap_cookie_t handle, offset_t offset, size_t len);
devmap_unload() 可使对 handle、offset 和 len 指定的映射页的映射转换无效。通过使对这些页的映射转换无效,设备驱动程序将告知系统拦截对这些映射页的访问。并且,下次通过调用 devmap_access(9E) 入口点访问这些映射页时,系统必须通知设备驱动程序。
对于这两个函数而言,请求会影响包含 offset 的整页,直到包含由 offset + len 所表示的最后一个字节的整页(包含该页)。设备驱动程序必须确保对于所映射的每页设备内存而言,在任意时刻仅有一个进程具有有效转换。
如果成功,两个函数都将返回零。但是,如果在对映射转换进行验证或使其无效时出现错误,则该错误将返回给设备驱动程序。设备驱动程序必须将此错误返回给系统。
电源管理提供控制和管理计算机系统或设备的电源使用情况的功能。使用电源管理,可使系统在空闲时消耗较少的电量,在未使用时完全关闭电源,从而节省能源。例如,桌面计算机系统耗电量很大,但经常处于空闲状态,尤其是在夜间。电源管理软件可以检测到系统未被使用的情况。因此,电源管理可以关闭系统或其中某些组件的电源。
本章介绍有关以下主题的信息:
Solaris 电源管理框架依靠设备驱动程序来实现特定于设备的电源管理功能。该框架分两部分实现:
设备电源管理-自动关闭未使用的设备,以减少能耗
系统电源管理-当整个系统处于空闲状态时,自动关闭计算机
该框架使设备可在经过指定的空闲时间间隔后降低能耗。在电源管理过程中,系统软件会检查空闲设备。电源管理框架会导出接口,通过这些接口,可以在系统软件与设备驱动程序之间进行通信。
Solaris 电源管理框架提供了下列设备电源管理功能:
适用于电源可管理设备且与设备无关的模型。
dtpower(1M ),一种用于配置工作站电源管理的工具。电源管理也可以通过 power.conf(4) 和 /etc/default/power 文件来实现。
一组 DDI 接口,用于通知框架电源管理兼容性和空闲状态。
系统电源管理可在关闭系统电源之前保存系统状态。因此,系统可以在重新打开时立即返回到相同状态。
要关闭整个系统并返回到关闭前的状态,请执行以下步骤:
停止内核线程和用户进程。以后再重新启动这些线程和进程。
将系统中所有设备的硬件状态保存到磁盘。以后再恢复该状态。
当前,仅在 Solaris OS 支持的某些 SPARC 系统上实现了系统电源管理。有关更多信息,请参见 power.conf(4) 手册页。
Solaris OS 中的系统电源管理框架提供了下列系统电源管理功能:
与平台无关的系统空闲模型。
pmconfig(1M),一种用于配置工作站电源管理的工具。电源管理也可以通过 power.conf(4) 和 /etc/default/power 文件来实现。
一组接口,使设备驱动程序可以覆盖用于确定哪些驱动程序具有硬件状态的方法。
一组允许框架对驱动程序进行调用以便保存和恢复设备状态的接口。
用于通知进程已执行恢复操作的机制。
组件
空闲
电源级别
相关性
策略
设备电源管理接口
电源管理入口点
如果在设备处于空闲状态时可以减少设备能耗,则该设备是电源可管理设备。从概念上讲,电源可管理设备由许多电源可管理硬件单元组成,这些硬件单元称为组件。
设备驱动程序通知系统有关设备组件及其相关电源级别的信息。因此,在驱动程序初始化期间,驱动程序会在其 attach(9E) 入口点中创建 pm-components(9P) 属性。
大多数电源可管理设备仅实现单个组件。例如,磁盘就是一个电源可管理的单组件设备,当磁盘处于空闲状态时,可以停止磁盘主轴马达以节省电能。
如果一个设备具有多个可单独控制的电源可管理单元,则该设备应实现多个组件。
例如,配有监视器的帧缓存器卡就是一个电源可管理的双组件设备。帧缓存器电子设备是第一个组件 [组件 0]。未使用帧缓存器电子设备时,其能耗将会降低。监视器是第二个组件 [组件 1]。未使用监视器时,监视器也可以进入低能耗模式。系统将帧缓存器电子设备和监视器视为一个由两个组件组成的设备。
对于电源管理框架而言,所有组件均“一视同仁”,并且组件之间完全无关。如果组件状态不完全兼容,则设备驱动程序必须确保不会出现不需要的状态组合。例如,帧缓存器/监视器卡具有以下几种可能状态: D0、D1、D2 和 D3。与卡连接的监视器具有以下几种可能状态: On、Standby、Suspend 和 Off。这些状态并不一定相互兼容。例如,如果监视器处于 On 状态,则帧缓存器必须处于 D0 状态(即完全打开)。如果在帧缓存器处于 D3 状态时,其驱动程序收到一个请求,要求打开监视器电源使监视器处于 On 状态,则在将监视器设置为 On 之前,驱动程序必须调用 pm_raise_power(9F) 才能启动帧缓存器。如果系统在监视器处于 On 状态时请求降低帧缓存器的电能供给,则驱动程序必须拒绝该请求。
每个设备组件都可处于以下两种状态之一:繁忙或空闲。设备驱动程序通过调用 pm_busy_component(9F) 和 pm_idle_component(9F) 通知框架设备状态的更改。最初创建组件时,组件被视为空闲状态。
通过设备导出的 pm-components 属性,设备电源管理框架可了解设备支持的电源级别。电源级别值必须是正整数。对电源级别的解释由设备驱动程序编写者确定。在 pm-components 属性中必须按单一递增顺序列出电源级别。该框架将 0 电源级别解释为关闭。如果框架由于相关性必须打开设备电源,则它会将每个组件都设置为其最高电源级别。
以下示例给出了某驱动程序的 .conf 文件中的 pm-components 项,该驱动程序实现了一个电源管理组件(即磁盘主轴马达)。磁盘轴马达是组件 0。该轴马达支持两个电源级别。这两个级别表示“停止”和“全速旋转”。
pm-components="NAME=Spindle Motor", "0=Stopped", "1=Full Speed";
以下示例说明如何在驱动程序的 attach() 例程中实现示例 12–1。
static char *pmcomps[] = { "NAME=Spindle Motor", "0=Stopped", "1=Full Speed" }; /* ... */ xxattach(dev_info_t *dip, ddi_attach_cmd_t cmd) { /* ... */ if (ddi_prop_update_string_array(DDI_DEV_T_NONE, dip, "pm-components", &pmcomp[0], sizeof (pmcomps) / sizeof (char *)) != DDI_PROP_SUCCESS) goto failed; /* ... */
以下示例给出了实现两个组件的帧缓存器。组件 0 是支持四个不同电源级别的帧缓存器电子设备。组件 1 表示所连接的监视器的电源管理状态。
pm-components="NAME=Frame Buffer", "0=Off", "1=Suspend", \ "2=Standby", "3=On", "NAME=Monitor", "0=Off", "1=Suspend", "2=Standby", "3=On";
首次连接设备驱动程序时,框架并不了解设备的电源级别。在以下情况下,会进行电源转换:
驱动程序调用 pm_raise_power(9F) 或 pm_lower_power(9F)。
由于超出时间阈值,框架降低了组件的电源级别。
另一个设备更换了电源,而这两个设备之间存在相关性。请参见电源管理相关性。
进行电源转换后,框架将开始跟踪每个设备组件的电源级别。如果驱动程序已通知框架电源级别,则也会进行跟踪。驱动程序通过调用 pm_power_has_changed(9F) 通知框架电源级别的更改。
系统将计算每个可能的电源转换的缺省阈值。这些阈值基于系统空闲阈值。可以使用 pmconfig 或 power.conf(4) 覆盖缺省阈值。当组件电源级别未知时,将使用基于系统空闲阈值的其他缺省阈值。
某些设备的电源应仅在关闭其他设备的电源时关闭。例如,如果允许关闭 CD-ROM 驱动器的电源,则可能会丢失一些必需功能,如弹出 CD 的功能。
为了防止设备独立关闭电源,可以使该设备依赖于电源可能保持打开的其他设备。通常,设备依赖于帧缓存器,因为在用户使用系统时监视器通常处于打开状态。
power.conf(4) 文件指定设备之间的相关性。(设备树中的父节点隐式依赖于其子节点。电源管理框架会自动处理此相关性。)可以使用以下格式的 power.conf(4) 项指定特定的相关性:
device-dependency dependent-phys-path phys-path
其中,dependent-phys-path 是电源保持打开状态的设备,如 CD-ROM 驱动器。phys-path 表示要依赖于其电源状态的设备,如帧缓存器。
在 power.conf 中为插入系统的每个新设备添加一个项将非常麻烦。可以使用以下语法,以一种更通用的方式指明相关性:
device-dependency-property property phys-path
这种项要求任何导出属性 property 的设备都必须依赖于 phys-path 指定的设备。由于此相关性尤其适用于可移除介质设备,因此缺省情况下 /etc/power.conf 包含以下行:
device_dependent-property removable-media /dev/fb
使用此语法,除非关闭控制台帧缓存器的电源,否则无法关闭导出 removable-media 属性的设备的电源。
有关更多信息,请参见 power.conf(4) 和 removable-media(9P) 手册页。
如果 pmconfig 或 power.conf(4) 启用了自动电源管理,则具有 pm-components(9P) 属性的所有设备都将自动使用电源管理。当组件空闲一段缺省时间后,组件将自动降低到下一个最低电源级别。缺省时间段由电源管理框架计算,用于在系统空闲阈值内将整个设备设置为其最低能耗状态。
缺省情况下,1999 年 7 月 1 日后首次发布的所有 SPARC 桌面系统上都启用了自动电源管理。对于所有其他系统,缺省情况下禁用此功能。要确定您的计算机上是否启用了自动电源管理,请参阅 power.conf(4) 手册页中的说明。
可以使用 power.conf(4) 覆盖框架计算的缺省值。
支持包含电源可管理组件的设备的设备驱动程序必须创建 pm-components(9P) 属性。该属性向系统指明设备包含电源可管理组件。pm-components 还向系统指明可用的电源级别。通常,驱动程序通过从其 attach(9E) 入口点调用 ddi_prop_update_string_array(9F) 来通知系统。另一种通知系统的方法是使用 driver.conf(4) 文件。有关详细信息,请参见 pm-components(9P) 手册页。
驱动程序必须始终使框架了解从空闲到繁忙或从繁忙到空闲的设备状态转换。进行这些转换的位置完全特定于设备。繁忙与空闲状态之间的转换取决于设备的性质以及特定组件具备的状态转换特性。例如,SCSI 磁盘目标驱动程序通常导出单个组件,该组件表示 SCSI 目标磁盘驱动器是启动状态还是停止状态。当驱动器有未解决的请求时,该组件将标记为繁忙。完成最后一个排队请求后,该组件将标记为空闲。某些组件创建后从未标记为繁忙。例如,pm-components(9P) 创建的组件就一直处于空闲状态。
pm_busy_component(9F) 和 pm_idle_component(9F) 接口将通知电源管理框架繁忙/空闲状态转换。pm_busy_component(9F) 调用的语法如下所示:
int pm_busy_component(dev_info_t *dip, int component);
pm_busy_component(9F) 将 component 标记为繁忙。当组件为繁忙状态时,不应关闭该组件的电源。如果已关闭组件电源,则将该组件标记为繁忙不会更改电源级别。为此,驱动程序需要调用 pm_raise_power(9F)。对 pm_busy_component(9F) 的调用具有累积性,因此要使组件处于空闲状态,需要调用相应次数的 pm_idle_component(9F)。
pm_idle_component(9F) 例程的语法如下所示:
int pm_idle_component(dev_info_t *dip, int component);
pm_idle_component(9F) 将 component 标记为空闲。可以关闭空闲组件的电源。要使组件处于空闲状态,必须针对 pm_busy_component(9F) 的每次调用调用 pm_idle_component(9F) 一次。
设备驱动程序可以调用 pm_raise_power(9F) 来请求将组件至少设置为给定的电源级别。使用已关闭电源的组件之前,必须采用这种方式设置电源级别。例如,如果已关闭磁盘电源,则 SCSI 磁盘目标驱动程序的 read(9E) 例程可能需要启动磁盘。pm_raise_power(9F) 函数请求电源管理框架将设备能耗状态转换为较高的电源级别。通常,由框架来降低组件的电源级别。但是,设备驱动程序在分离时应调用 pm_lower_power(9F),以便尽可能地降低未使用设备的能耗。
对于某些设备来说,关闭电源可能会产生风险。例如,某些磁带机在关闭电源时会损坏磁带。同样,某些磁盘驱动器在开关电源过程中的容错能力有限,因为每次开关电源都会导致磁头停放。应使用 no-involuntary-power-cycles(9P) 属性通知系统,设备驱动程序应控制设备的所有关开机循环。此方法可防止在分离设备驱动程序时切断设备电源,除非驱动程序已从其 detach(9E) 入口点调用 pm_lower_power(9F) 关闭设备电源。
驱动程序发现某个操作所需组件的电源级别不够高时,会调用 pm_raise_power(9F) 函数。该接口会使驱动程序将组件的当前电源级别提高到所需级别。该调用还会将依赖于该设备的所有设备恢复到全功率。
如果在不再需要访问某个设备后分离该设备,则将调用 pm_lower_power(9F)。调用 pm_lower_power(9F) 可将每个组件设置为最低电源级别,从而使设备在未使用时尽可能少地消耗电量。必须从 detach() 入口点调用 pm_lower_power() 函数。如果从驱动程序的任何其他部分调用 pm_lower_power() 函数,该函数不会起作用。
调用 pm_power_has_changed(9F) 函数可通知框架有关电源转换的情况。转换可能是由于设备更改了自己的电源级别而导致。转换也可能是由于暂停/恢复等操作而导致。pm_power_has_changed(9F) 的语法与 pm_raise_power(9F) 的语法相同。
电源管理框架使用 power(9E) 入口点。
power() 使用以下语法:
int power(dev_info_t *dip, int component, int level);
需要更改组件的电源级别时,系统将调用 power(9E) 入口点。该入口点执行的操作特定于设备驱动程序。在上面提到的 SCSI 目标磁盘驱动程序示例中,如果将电源级别设置为 0,则会发送 SCSI 命令停止磁盘运转,而如果将电源级别设置为全电源级别,则会发送 SCSI 命令启动磁盘。
如果电源转换导致设备丢失状态,则驱动程序必须将任何必需的状态保存在内存中,以便将来恢复。如果电源转换要求先恢复保存的状态,然后才能再次使用设备,则驱动程序必须恢复该状态。该框架并未对哪些电源事务会导致丢失自动电源管理设备的状态作出假设,也未对哪些电源事务会要求恢复自动电源管理设备的状态作出假设。以下示例给出了一个 power() 例程样例。
int xxpower(dev_info_t *dip, int component, int level) { struct xxstate *xsp; int instance; instance = ddi_get_instance(dip); xsp = ddi_get_soft_state(statep, instance); /* * Make sure the request is valid */ if (!xx_valid_power_level(component, level)) return (DDI_FAILURE); mutex_enter(&xsp->mu); /* * If the device is busy, don't lower its power level */ if (xsp->xx_busy[component] && xsp->xx_power_level[component] > level) { mutex_exit(&xsp->mu); return (DDI_FAILURE); } if (xsp->xx_power_level[component] != level) { /* * device- and component-specific setting of power level * goes here */ xsp->xx_power_level[component] = level; } mutex_exit(&xsp->mu); return (DDI_SUCCESS); }
以下示例是包含两个组件的设备的 power() 例程,其中,组件 1 为打开状态时,组件 0 必须也为打开状态。
int xxpower(dev_info_t *dip, int component, int level) { struct xxstate *xsp; int instance; instance = ddi_get_instance(dip); xsp = ddi_get_soft_state(statep, instance); /* * Make sure the request is valid */ if (!xx_valid_power_level(component, level)) return (DDI_FAILURE); mutex_enter(&xsp->mu); /* * If the device is busy, don't lower its power level */ if (xsp->xx_busy[component] && xsp->xx_power_level[component] > level) { mutex_exit(&xsp->mu); return (DDI_FAILURE); } /* * This code implements inter-component dependencies: * If we are bringing up component 1 and component 0 * is off, we must bring component 0 up first, and if * we are asked to shut down component 0 while component * 1 is up we must refuse */ if (component == 1 && level > 0 && xsp->xx_power_level[0] == 0) { xsp->xx_busy[0]++; if (pm_busy_component(dip, 0) != DDI_SUCCESS) { /* * This can only happen if the args to * pm_busy_component() * are wrong, or pm-components property was not * exported by the driver. */ xsp->xx_busy[0]--; mutex_exit(&xsp->mu); cmn_err(CE_WARN, "xxpower pm_busy_component() failed"); return (DDI_FAILURE); } mutex_exit(&xsp->mu); if (pm_raise_power(dip, 0, XX_FULL_POWER_0) != DDI_SUCCESS) return (DDI_FAILURE); mutex_enter(&xsp->mu); } if (component == 0 && level == 0 && xsp->xx_power_level[1] != 0) { mutex_exit(&xsp->mu); return (DDI_FAILURE); } if (xsp->xx_power_level[component] != level) { /* * device- and component-specific setting of power level * goes here */ xsp->xx_power_level[component] = level; } mutex_exit(&xsp->mu); return (DDI_SUCCESS); }
自动关闭阈值
繁忙状态
硬件状态
策略
电源管理入口点
经过一段可配置空闲时间后,系统可以自动关闭(即关闭电源)。该时间段称为自动关闭阈值。缺省情况下,将对在 1995 年 10 月 1 日到 1999 年 7 月 1 日间首次发布的 SPARC 桌面系统启用此行为。有关更多信息,请参见 power.conf(4) 手册页。可以使用 dtpower(1M) 或 power.conf(4) 覆盖自动关闭。
可以采用几种方法度量系统的繁忙状态。当前支持的内置度量标准项包括键盘字符、鼠标活动、tty 字符、平均负荷值、磁盘读取和 NFS 请求。其中任何一项都可使系统处于繁忙状态。除内置度量标准外,还定义了一个接口,用于运行用户指定的可以表明系统处于繁忙状态的进程。
导出 reg 属性的设备被视为具有硬件状态,关闭系统之前,必须保存该硬件状态。没有 reg 属性的设备被视为无状态设备。但是,设备驱动程序可以另外一种方式处理这种情况。
如果驱动程序导出值为 needs-suspend-resume 的 pm-hardware-state 属性,则必须调用具有硬件状态但没有 reg 属性的设备(如 SCSI 驱动程序),才能保存并恢复状态。否则,缺少 reg 属性即表示设备没有硬件状态。有关设备属性的信息,请参见第 4 章。
具有 reg 属性但没有硬件状态的设备可以导出值为 no-suspend-resume 的 pm-hardware-state 属性。将 no-suspend-resume 与 pm-hardware-state 属性配合使用,可防止框架调用驱动程序来保存并恢复该状态。有关电源管理属性的更多信息,请参见 pm-components(9P) 手册页。
dtpower(1M) 或 power.conf(4) 启用了自动关闭。
系统空闲的时间长度已达到自动关闭阈值(分钟)。
满足 power.conf 中指定的所有度量标准。
系统电源管理将命令DDI_SUSPEND 传递给 detach(9E) 驱动程序入口点,以请求驱动程序保存设备硬件状态。系统电源管理将命令 DDI_RESUME 传递给 attach(9E) 驱动程序入口点,以请求驱动程序恢复设备硬件状态。
detach(9E) 的语法如下所示:
int detach(dev_info_t *dip, ddi_detach_cmd_t cmd);
具有 reg 属性或 pm-hardware-state 属性设置为 needs-suspend-resume 的设备必须能够保存设备的硬件状态。框架调用驱动程序的 detach(9E) 入口点使驱动程序保存状态,以便在系统电源重新打开后进行恢复。要处理 DDI_SUSPEND 命令,detach(9E) 必须执行以下任务:
在设备恢复之前,阻止启动进一步操作,但 dump(9E) 请求除外。
一直等到未完成的操作完成为止。如果可以重新启动未完成的操作,则可以中止该操作。
取消待处理的任何超时和回调。
将任何易失的硬件状态保存到内存。该状态包含设备寄存器的内容,此外还可以包含下载的固件。
如果驱动程序无法暂停设备并将其状态保存到内存,则驱动程序必须返回 DDI_FAILURE。然后,框架将异常中止系统电源管理操作。
在某些情况下,关闭设备电源存在一定风险。例如,如果关闭内含磁带的磁带机电源,则该磁带可能会损坏。在这种情况下,attach(9E) 应执行以下操作:
调用 ddi_removing_power(9F) 以确定 DDI_SUSPEND 命令是否会关闭设备的电源。
确定关闭电源是否会产生问题。
如果上述两种操作的结果都是肯定的,则应拒绝 DDI_SUSPEND 请求。示例 12–6 给出了使用 ddi_removing_power(9F) 检查 DDI_SUSPEND 命令是否会产生问题的 attach(9E) 例程。
必须接受转储请求。框架使用 dump(9E) 入口点写出包含内存内容的状态文件。有关使用该入口点时对设备驱动程序强加的限制,请参见 dump(9E) 手册页。
使用 DDI_SUSPEND 命令调用电源可管理组件的 detach(9E) 入口点时,应保存关闭设备电源时的状态。驱动程序应取消待处理的超时。驱动程序还应禁止对 pm_raise_power(9F) 的任何调用,但 dump(9E) 请求除外。通过使用 DDI_RESUME 命令调用 attach(9E) 来恢复设备时,可以恢复超时以及对 pm_raise_power () 的调用。驱动程序必须掌握其足够的状态信息,才能够正确处理这种可能发生的情况。以下示例给出了实现 DDI_SUSPEND 命令的 detach(9E) 例程。
int xxdetach(dev_info_t *dip, ddi_detach_cmd_t cmd) { struct xxstate *xsp; int instance; instance = ddi_get_instance(dip); xsp = ddi_get_soft_state(statep, instance); switch (cmd) { case DDI_DETACH: /* ... */ case DDI_SUSPEND: /* * We do not allow DDI_SUSPEND if power will be removed and * we have a device that damages tape when power is removed * We do support DDI_SUSPEND for Device Reconfiguration. */ if (ddi_removing_power(dip) && xxdamages_tape(dip)) return (DDI_FAILURE); mutex_enter(&xsp->mu); xsp->xx_suspended = 1; /* stop new operations */ /* * Sleep waiting for all the commands to be completed * * If a callback is outstanding which cannot be cancelled * then either wait for the callback to complete or fail the * suspend request * * This section is only needed if the driver maintains a * running timeout */ if (xsp->xx_timeout_id) { timeout_id_t temp_timeout_id = xsp->xx_timeout_id; xsp->xx_timeout_id = 0; mutex_exit(&xsp->mu); untimeout(temp_timeout_id); mutex_enter(&xsp->mu); } if (!xsp->xx_state_saved) { /* * Save device register contents into * xsp->xx_device_state */ } mutex_exit(&xsp->mu); return (DDI_SUCCESS); default: return (DDI_FAILURE); }
attach(9E) 的语法如下所示:
int attach(dev_info_t *dip, ddi_attach_cmd_t cmd);
恢复系统电源后,每个具有 reg 属性或具有值为 needs-suspend-resume 的 pm-hardware-state 属性的设备都会使用 DDI_RESUME 命令值调用其 attach(9E) 入口点。如果系统关闭异常中止,则即使尚未关闭电源,也会调用每个暂停的驱动程序以进行恢复。因此,attach(9E) 中的恢复代码不能对系统是否已实际断电作出任何假设。
电源管理框架认为组件的电源级别在执行 DDI_RESUME 时未知。根据设备性质,驱动程序编写者有两种选择:
如果驱动程序无需打开组件电源即可确定设备组件的实际电源级别(如通过读取寄存器),则驱动程序应通过调用 pm_power_has_changed(9F) 来通知框架每个组件的电源级别。
如果驱动程序无法确定组件的电源级别,则驱动程序应在首次访问各个组件之前,在内部将每个组件标记为未知并调用 pm_raise_power(9F)。
以下示例给出了使用 DDI_RESUME 命令的 attach(9E) 例程。
int xxattach(devinfo_t *dip, ddi_attach_cmd_t cmd) { struct xxstate *xsp; int instance; instance = ddi_get_instance(dip); xsp = ddi_get_soft_state(statep, instance); switch (cmd) { case DDI_ATTACH: /* ... */ case DDI_RESUME: mutex_enter(&xsp->mu); if (xsp->xx_pm_state_saved) { /* * Restore device register contents from * xsp->xx_device_state */ } /* * This section is optional and only needed if the * driver maintains a running timeout */ xsp->xx_timeout_id = timeout( /* ... */ ); xsp->xx_suspended = 0; /* allow new operations */ cv_broadcast(&xsp->xx_suspend_cv); /* If it is possible to determine in a device-specific * way what the power levels of components are without * powering the components up, * then the following code is recommended */ for (i = 0; i < num_components; i++) { xsp->xx_power_level[i] = xx_get_power_level(dip, i); if (xsp->xx_power_level[i] != XX_LEVEL_UNKNOWN) (void) pm_power_has_changed(dip, i, xsp->xx_power_level[i]); } mutex_exit(&xsp->mu); return(DDI_SUCCESS); default: return(DDI_FAILURE); } }
detach(9E) 和 attach(9E) 接口也可用于恢复处于静止状态的系统。
如果支持电源管理,并且按示例 12–6 和 示例 12–7 中的方式使用 detach(9E) 和 attach(9E),则可以从用户上下文(例如,read(2)、write(2) 和 ioctl(2))访问该设备。
以下示例演示了该方法。该示例假定要执行的操作需要以电源级别 level 运行的组件 component。
mutex_enter(&xsp->mu); /* * Block command while device is suspended by DDI_SUSPEND */ while (xsp->xx_suspended) cv_wait(&xsp->xx_suspend_cv, &xsp->mu); /* * Mark component busy so xx_power() will reject attempt to lower power */ xsp->xx_busy[component]++; if (pm_busy_component(dip, component) != DDI_SUCCESS) { xsp->xx_busy[component]--; /* * Log error and abort */ } if (xsp->xx_power_level[component] < level) { mutex_exit(&xsp->mu); if (pm_raise_power(dip, component, level) != DDI_SUCCESS) { /* * Log error and abort */ } mutex_enter(&xsp->mu); }
当设备操作(例如,设备的中断处理程序执行的操作)完成时,可以使用以下示例中的代码段。
/* * For each command completion, decrement the busy count and unstack * the pm_busy_component() call by calling pm_idle_component(). This * will allow device power to be lowered when all commands complete * (all pm_busy_component() counts are unstacked) */ xsp->xx_busy[component]--; if (pm_idle_component(dip, component) != DDI_SUCCESS) { xsp->xx_busy[component]++; /* * Log error and abort */ } /* * If no more outstanding commands, wake up anyone (like DDI_SUSPEND) * waiting for all commands to be completed */
图 12–1 说明了电源管理框架中的控制流程。
完成组件活动后,驱动程序可以调用 pm_idle_component(9F) 将该组件标记为空闲。如果组件在其阈值时间内一直处于空闲状态,则框架可以将该组件的能耗降低到下一个较低级别。框架调用 power(9E) 函数将组件的能耗设置为支持的下一个较低电源级别(如果存在较低级别)。当组件处于繁忙状态时,驱动程序的 power(9E) 函数应拒绝任何降低该组件电源级别的尝试。在转换到较低级别之前 power(9E) 函数应保存可能在转换过程中丢失的任何状态。
需要较高级别的组件时,驱动程序将调用 pm_busy_component(9F)。此调用将阻止框架进一步降低能耗,然后针对组件调用 pm_raise_power(9F)。在对 pm_raise_power(9F) 的调用返回之前,框架接着调用 power(9E) 以提高组件的能耗。驱动程序的 power(9E) 代码必须恢复在较低级别中丢失、但在较高级别中需要的任何状态。
分离某个驱动程序时,该驱动程序应针对每个组件调用 pm_lower_power(9F),以将其能耗降低到最低级别。在对 pm_lower_power(9F) 的调用返回之前,框架可以随后调用驱动程序的 power(9E) 例程以降低组件的能耗。
在 Solaris 8 发行版之前,设备的电源管理不是自动的。开发者必须为要管理其电源的每个设备在 /etc/power.conf 中添加一项。框架假定所有设备都只支持两个电源级别: 0 和标准能耗。
电源假定所有其他组件对组件 0 具有隐式相关性。当组件 0 更改为级别 0 时,使用 DDI_PM_SUSPEND 命令调用驱动程序的 detach(9E) 来保存硬件状态。当组件 0 的级别 0 发生更改时,使用命令 DDI_PM_RESUME 调用 attach(9E) 例程来恢复硬件状态。
以下接口和命令已过时,现在仍支持它们是为了适于采用二进制的场合:
ddi_dev_is_needed(9F)
pm_create_components(9F)
pm_destroy_components(9F)
pm_get_normal_power(9F)
pm_set_normal_power(9F)
DDI_PM_SUSPEND
DDI_PM_RESUME
从 Solaris 8 发行版开始,如果启用了 autopm,则导出 pm-components 属性的设备将自动使用电源管理。
现在,框架可以通过 pm-components 属性了解每个设备支持的电源级别。
框架对设备不同组件之间的相关性不会作出任何假设。更改电源级别时,设备驱动程序负责根据需要保存并恢复硬件状态。
通过这些更改,电源管理框架可以处理新兴的设备技术。现在,电源管理可以节省更多的电。框架可以自动检测哪些设备能够省电。框架可以使用设备的中间能耗状态。现在,系统可以实现节能目标,而无需关闭整个系统的电源,也无需使用任何功能。
表 12–1 电源管理接口
已删除的接口 |
等效接口 |
---|---|
pm_create_components(9F) | |
pm_set_normal_power(9F) | |
pm_destroy_components(9F) |
无 |
pm_get_normal_power(9F) |
无 |
ddi_dev_is_needed(9F) | |
无 | |
无 | |
DDI_PM_SUSPEND |
无 |
DDI_PM_RESUME |
无 |
借助故障管理体系结构 (Fault Management Architecture, FMA) I/O 故障服务,驱动程序开发者可将故障管理功能集成到 I/O 设备驱动程序中。Solaris I/O 故障服务框架定义了一组接口,使得所有驱动程序可以协调工作,并执行基本的错误处理任务和活动。总体上,Solaris FMA 除了可进行响应和恢复外,还可进行错误处理和故障诊断。FMA 是 Sun 的预测性自我修复策略的一个组成部分。
当驱动程序除了将 I/O 故障服务框架用于错误处理和诊断外,还使用本文档中介绍的防御性编程做法时,认为该驱动程序已经过强化。驱动程序强化测试工具测试是否已正确实现 I/O 故障服务和防御性编程要求。
本文档包含以下各节:
Sun 故障管理体系结构 I/O 故障服务为希望将故障管理功能集成到 I/O 设备驱动程序中的驱动程序开发者提供参考。
用于 Solaris 设备驱动程序的防御性编程方法提供有关如何防御性地编写 Solaris 设备驱动程序的一般信息。
驱动程序强化测试工具是一种驱动程序开发工具,当处于开发阶段的驱动程序访问其硬件时,该工具可注入仿真的硬件故障。
本节介绍如何为 I/O 设备驱动程序集成故障管理错误报告、错误处理和诊断。本节深入探讨了 I/O 故障服务框架以及如何在设备驱动程序内利用 I/O 故障服务 API。
本节讨论以下主题:
什么是预测性自我修复?提供 Sun 故障管理体系结构的背景信息和概述。
Solaris Fault Manager介绍了更多背景信息,重点介绍 Solaris Fault Manager fmd(1M) 的较高层面的概述。
错误处理是面向驱动程序开发者的主要章节。本节重点介绍用于获得高可用性的最佳编码方法,以及如何在驱动程序代码中使用 I/O 故障服务来与 FMA 交互。
诊断故障介绍如何根据驱动程序检测到的错误来诊断故障。
事件注册表提供有关 Sun 的事件注册表的信息。
传统上,系统会将硬件和软件错误信息以系统日志消息的形式直接导出给管理员以及管理软件。错误检测、诊断、报告和处理通常会嵌入到每个驱动程序的代码中。
Solaris OS 预测性自我修复系统这样的系统是最早的也是最重要的自我诊断系统。自我诊断意味着系统提供根据观察到的症状自动诊断问题的技术,然后将诊断结果用于触发自动响应和恢复。硬件故障或软件缺陷可与一组可能观察到的、称为错误的症状相关联。系统检测到错误后所生成的数据称为错误报告或 ereport。
在具有自我修复功能的系统中,ereport 由系统捕获,并编码为由可扩展事件协议描述的一组名称-值对,从而形成 ereport 事件。收集 ereport 事件和其他数据是为了便于进行自我修复,还会将其分发给名为诊断引擎的软件组件,诊断引擎设计用来诊断与系统检测到的错误症状对应的底层问题。诊断引擎在后台运行,并以无提示方式使用错误遥测,直到它可以生成诊断或预测故障为止。
在处理足够的遥测从而得出结论之后,诊断引擎会生成名为故障事件的另一个事件。然后,会向对特定故障事件感兴趣的所有代理广播该故障事件。代理是可对特定故障事件启动恢复和做出响应的软件组件。被称为 Solaris Fault Manager 的软件组件 fmd(1M) 可在 ereport 生成器、诊断引擎和代理软件之间管理事件的多路复用。
Solaris Fault Manager fmd(1M) 负责将传入错误遥测事件分发给相应的诊断引擎。诊断引擎负责识别产生错误症状的基础硬件故障或软件缺陷。fmd(1M) 守护进程是故障管理器的 Solaris OS 实现。它在引导时启动,并且会装入系统中可用的所有诊断引擎和代理。Solaris Fault Manager 还为系统管理员和服务人员提供用于观察故障管理活动的界面。
进行诊断后,会以 list.suspect 事件的形式输出诊断。list.suspect 事件是由一个或多个可能的故障或缺陷事件构成的事件。有时候,诊断无法将错误原因的范围缩小至单个故障或缺陷。例如,底层问题可能是连接控制器与主系统总线的线路中断。问题也可能在于总线上的某个组件或总线本身。在这种特定情况下,list.suspect 事件将包含多个故障事件:一个是连接到总线的每个控制器的事件,另一个是总线本身的事件。
除了介绍诊断的故障外,故障事件还包含四个可应用诊断的有效负荷成员。
资源是被诊断为有故障的组件。fmdump(1M) 命令将此有效负荷成员显示为 "Problem in"。
自动系统恢复单元 (Automated System Recovery Unit, ASRU) 是必须禁用以防止出现更多错误症状的硬件或软件组件。fmdump(1M) 命令将此有效负荷成员显示为 "Affects"。
现场可更换单元 (Field Replaceable Unit, FRU) 是必须更换或维修以便修复底层问题的组件。
标签有效负荷是一个字符串,用于按照 FRU 在机箱或主板上的印刷形式给出其位置,例如,在 DIMM 插槽或 PCI 卡插槽旁边。fmdump 命令将此有效负荷成员显示为 "Location"。
例如,在给定时间内针对特定内存位置收到特定数量的 ECC 可纠正错误后,CPU 和内存诊断引擎会对有故障的 DIMM 发出诊断(list.suspect 事件)。
# fmdump -v -u 38bd6f1b-a4de-4c21-db4e-ccd26fa8573c TIME UUID SUNW-MSG-ID Oct 31 13:40:18.1864 38bd6f1b-a4de-4c21-db4e-ccd26fa8573c AMD-8000-8L 100% fault.cpu.amd.icachetag Problem in: hc:///motherboard=0/chip=0/cpu=0 Affects: cpu:///cpuid=0 FRU: hc:///motherboard=0/chip=0 Location: SLOT 2 |
在此示例中,fmd(1M) 识别到资源中的一个问题,具体而言是 CPU (hc:///motherboard=0/chip=0/cpu=0 )。为了抑制出现更多错误症状并防止发生无法纠正的错误,对一个 ASRU (cpu:///cpuid=0) 进行了标识,以便弃用 (retirement)。需要更换的组件是 FRU (hc:///motherboard=0/chip=0)。
代理是可为了响应诊断或修复而执行操作的软件组件。例如,CPU 和内存弃用代理设计为对包含 fault.cpu.* 事件的 list.suspect 执行操作。cpumem-retire 代理将尝试在服务中使 CPU 脱机或弃用物理内存页。如果该代理成功,则会在故障管理器的 ASRU 缓存中为成功弃用的页或 CPU 添加一项。以下示例所示的 fmadm(1M) 实用程序显示了被诊断为有故障的内存等级的一项。系统无法使其脱机、将其弃用或禁用的 ASRU 也会在 ASRU 缓存中存在一项,但会将其视为被降级。降级意味着与 ASRU 关联的资源有故障,但无法从服务中将该 ASRU 删除。目前,Solaris 代理软件不能对 I/O ASRU(设备实例)执行操作。缓存中所有有故障的 I/O 资源项都处于降级状态。
# fmadm faulty STATE RESOURCE / UUID -------- ---------------------------------------------------------------------- degraded mem:///motherboard=0/chip=1/memory-controller=0/dimm=3/rank=0 ccae89df-2217-4f5c-add4-d920f78b4faf -------- ---------------------------------------------------------------------- |
弃用代理的主要用途是隔离(从服务中安全删除)被诊断为有故障的硬件或软件部分。
代理还可以执行其他重要操作,例如以下操作:
通过 SNMP 陷阱发送警报。这样便可将诊断转换为插入现有软件机制中的 SNMP 警报。
发布系统日志消息。特定于消息的诊断(例如,系统日志消息代理)可以提取诊断的结果,并将其转换为管理员可用来执行特定操作的系统日志消息。
其他代理操作,如更新 FRUID。响应代理可以特定于平台。
系统日志消息代理会提取诊断的输出(list.suspect 事件),并将特定消息写入控制台或 /var/adm/messages。控制台消息通常可能会难以理解。FMA 提供一个每当将 list.suspect 事件发送至系统日志消息时都会生成的已定义故障消息结构,从而对此问题进行了修正。
系统日志代理会生成一个消息标识符 (message identifier, MSG ID)。事件注册表生成字典文件(.dict 文件),这些文件可将 list.suspect 事件映射到将用来标识和查看关联知识文章的结构化消息标识符。消息文件(.po 文件)则将消息 ID 映射到诊断引擎可以生成的每个可能的可疑故障列表中的本地化消息。下面是一个测试系统中发出的故障消息示例。
SUNW-MSG-ID: AMD-8000-7U, TYPE: Fault, VER: 1, SEVERITY: Major EVENT-TIME: Fri Jul 28 04:26:51 PDT 2006 PLATFORM: Sun Fire V40z, CSN: XG051535088, HOSTNAME: parity SOURCE: eft, REV: 1.16 EVENT-ID: add96f65-5473-69e6-dbe1-8b3d00d5c47b DESC: The number of errors associated with this CPU has exceeded acceptable levels. Refer to http://sun.com/msg/AMD-8000-7U for more information. AUTO-RESPONSE: An attempt will be made to remove this CPU from service. IMPACT: Performance of this system may be affected. REC-ACTION: Schedule a repair procedure to replace the affected CPU. Use fmdump -v -u <EVENT_ID> to identify the module. |
为了确定可能发生故障的位置,诊断引擎需要表示出给定软件或硬件系统的拓扑。fmd(1M) 守护进程为诊断引擎提供了一个可在诊断期间使用的拓扑快照句柄。拓扑信息用来表示在每个故障事件中找到的资源、ASRU 和 FRU。拓扑也可以用来存储平台标签、FRUID 和序列号标识。
故障事件中的资源有效负荷成员始终由平台机箱周围组件的物理路径位置来表示。例如,从主系统总线桥接至 PCI 本地总线的 PCI 控制器功能由其 hc 模式路径名来表示:
hc:///motherboard=0/hostbridge=1/pcibus=0/pcidev=13/pcifn=0
故障事件中的 ASRU 有效负荷成员通常由绑定到硬件控制器、设备或功能的 Solaris 设备树实例名称来表示。对于可能由专门为 I/O 设备设计的弃用代理的将来实现执行的操作,FMA 使用 dev 模式以其本地格式表示 ASRU:
dev:////pci@1e,600000/ide@d
故障事件中的 FRU 有效负荷表示形式随距离被诊断为有故障的 I/O 资源最近的可更换组件而异。例如,中断的嵌入式 PCI 控制器的故障事件可能会将系统的主板命名为需要更换的 FRU:
hc:///motherboard=0
标签有效负荷是一个字符串,用于按照 FRU 在机箱或主板上的印刷形式给出其位置,例如,在 DIMM 插槽或 PCI 卡插槽旁边。
Label: SLOT 2
本节介绍如何使用 I/O 故障服务 API 来处理驱动程序内的错误。本节讨论驱动程序应如何指示和初始化其故障管理功能、生成错误报告以及注册驱动程序的错误处理程序例程。
摘录内容来自源代码示例,这些示例演示如何从 Broadcom 1Gb NIC 驱动程序 bge 中使用 I/O 故障服务 API。以这些示例为模型,了解如何将故障管理功能集成到您自己的驱动程序中。请按照以下步骤研究完整的 bge 驱动程序代码:
在 "File Path"(文件路径)字段中输入 bge。
单击 "Search"(搜索)按钮。
被指示提供 FMA 错误报告遥测的驱动程序将检测错误,并确定这些错误对驱动程序所提供服务的影响。检测到错误后,驱动程序应确定其服务受影响的时间以及程度。
I/O 驱动程序必须立即响应检测到的错误。相应的响应包括:
尝试恢复
重试 I/O 事务
尝试故障转移技术
向调用应用程序/堆栈报告错误
如果以任何其他方式均无法约束错误,则进入紧急状态
驱动程序检测到的错误以 ereport 的形式传递给故障管理守护进程。ereport 是由 FMA 事件协议定义的结构化事件。该事件协议是一组常用数据字段的规范,除了可疑故障列表外,这些字段还必须用于描述所有可能的错误和故障事件。Ereport 被收集为错误遥测流,并分发给诊断引擎。
强化的设备驱动程序必须向 I/O 故障管理框架声明其故障管理功能。使用 ddi_fm_init(9F) 函数声明驱动程序的故障管理功能。
void ddi_fm_init(dev_info_t *dip, int *fmcap, ddi_iblock_cookie_t *ibcp)
可从驱动程序 attach(9E) 或 detach(9E) 入口点的内核上下文中调用 ddi_fm_init() 函数。通常会从 attach() 入口点调用 ddi_fm_init() 函数。ddi_fm_init() 函数根据 fmcap 来分配和初始化资源。fmcap 参数必须设置为以下故障管理功能的按位或:
DDI_FM_EREPORT_CAPABLE
-在检测到错误状态时,驱动程序负责并且能够生成 FMA 协议错误事件 (ereport)。
DDI_FM_ACCCHK_CAPABLE
-完成对 I/O 事务的一次或多次访问后,驱动程序负责并且能够检查错误。
DDI_FM_DMACHK_CAPABLE
-完成一个或多个 DMA I/O 事务后,驱动程序负责并且能够检查错误。
DDI_FM_ERRCB_CAPABLE
-驱动程序具有错误回调功能。
强化的叶驱动程序通常设置上述所有功能。但是,如果父结点不能支持任何一项请求的功能,则关联的位会被清除并按此情况返回给驱动程序。在从 ddi_fm_init(9F) 返回之前,I/O 故障服务框架会创建一组故障管理功能属性: fm-ereport-capable、fm-accchk-capable、fm-dmachk-capable 和 fm-errcb-capable。可使用 prtconf(1M) 命令来观察当前支持的故障管理功能级别。
为了使驱动程序支持故障管理功能的管理选择,请导出故障管理功能级别属性并将其设置为上面 driver.conf(4) 文件中描述的值。在使用所需功能列表调用 ddi_fm_init() 之前,必须设置并读取 fm-capable 属性。
来自 bge 驱动程序的以下示例显示了 bge_fm_init() 函数,该函数调用 ddi_fm_init(9F) 函数的包装。可在 bge_attach() 函数中调用 bge_fm_init() 函数。
static void bge_fm_init(bge_t *bgep) { ddi_iblock_cookie_t iblk; /* Only register with IO Fault Services if we have some capability */ if (bgep->fm_capabilities) { bge_reg_accattr.devacc_attr_access = DDI_FLAGERR_ACC; dma_attr.dma_attr_flags = DDI_DMA_FLAGERR; /* * Register capabilities with IO Fault Services */ ddi_fm_init(bgep->devinfo, &bgep->fm_capabilities, &iblk); /* * Initialize pci ereport capabilities if ereport capable */ if (DDI_FM_EREPORT_CAP(bgep->fm_capabilities) || DDI_FM_ERRCB_CAP(bgep->fm_capabilities)) pci_ereport_setup(bgep->devinfo); /* * Register error callback if error callback capable */ if (DDI_FM_ERRCB_CAP(bgep->fm_capabilities)) ddi_fm_handler_register(bgep->devinfo, bge_fm_error_cb, (void*) bgep); } else { /* * These fields have to be cleared of FMA if there are no * FMA capabilities at runtime. */ bge_reg_accattr.devacc_attr_access = DDI_DEFAULT_ACC; dma_attr.dma_attr_flags = 0; } }
ddi_fm_fini(9F) 函数清除为支持 dip 的故障管理而分配的资源。
void ddi_fm_fini(dev_info_t *dip)
可从驱动程序 attach(9E) 或 detach(9E) 入口点的内核上下文中调用 ddi_fm_fini() 函数。
来自 bge 驱动程序的以下示例显示了 bge_fm_fini() 函数,该函数调用 ddi_fm_fini(9F) 函数的包装。可在 bge_unattach() 函数中调用 bge_fm_fini() 函数,而在 bge_attach() 和 bge_detach() 函数中调用 bge_unattach 函数。
static void bge_fm_fini(bge_t *bgep) { /* Only unregister FMA capabilities if we registered some */ if (bgep->fm_capabilities) { /* * Release any resources allocated by pci_ereport_setup() */ if (DDI_FM_EREPORT_CAP(bgep->fm_capabilities) || DDI_FM_ERRCB_CAP(bgep->fm_capabilities)) pci_ereport_teardown(bgep->devinfo); /* * Un-register error callback if error callback capable */ if (DDI_FM_ERRCB_CAP(bgep->fm_capabilities)) ddi_fm_handler_unregister(bgep->devinfo); /* * Unregister from IO Fault Services */ ddi_fm_fini(bgep->devinfo); } }
ddi_fm_capable(9F) 函数返回当前为 dip 设置的功能位掩码。
void ddi_fm_capable(dev_info_t *dip)
本节提供有关以下主题的信息:
对错误事件排队讨论如何对错误事件排队。
检测和报告与 PCI 相关的错误介绍如何报告与 PCI 相关的错误。
报告标准 I/O 控制器错误介绍如何报告标准 I/O 控制器错误。
服务影响函数讨论如何报告错误是否对设备提供的服务产生了影响。
ddi_fm_ereport_post(9F) 函数对 ereport 事件排队,以便传送给故障管理器守护进程 fmd(1M)。
void ddi_fm_ereport_post(dev_info_t *dip, const char *error_class, uint64_t ena, int sflag, ...)
sflag 参数指示调用方是否愿意等待系统内存和事件通道资源变为可用。
ENA 指示此错误报告的错误编号关联 (Error Numeric Association, ENA)。ENA 可能已初始化,并且是从其他错误检测软件模块(如总线结点驱动程序)中获得的。如果 ENA 设置为 0,它将被 ddi_fm_ereport_post() 初始化。
名称-值对 (nvpair) 变量参数列表包含非数组 data_type_t 类型的一个或多个名称、类型、值指针 nvpair 元组,或者包含 data_type_t 数组类型的一个或多个名称、类型、元素数、值指针元组。nvpair 元组补足诊断所需要的 ereport 事件有效负荷。参数列表的结尾由 NULL 指定。
报告标准 I/O 控制器错误中介绍的用于 I/O 控制器的 ereport 类名和有效负荷可适用于 error_class。可以定义其他 ereport 类名和有效负荷,但必须在 Sun 事件注册表中进行注册,并伴有特定于驱动程序的诊断引擎软件或 Eversholt 故障树 (Eversholt fault tree, eft) 规则。有关 Sun 事件注册表和 Eversholt 故障树规则的更多信息,请参见 OpenSolaris 项目的故障管理社区。
void bge_fm_ereport(bge_t *bgep, char *detail) { uint64_t ena; char buf[FM_MAX_CLASS]; (void) snprintf(buf, FM_MAX_CLASS, "%s.%s", DDI_FM_DEVICE, detail); ena = fm_ena_generate(0, FM_ENA_FMT1); if (DDI_FM_EREPORT_CAP(bgep->fm_capabilities)) { ddi_fm_ereport_post(bgep->devinfo, buf, ena, DDI_NOSLEEP, FM_VERSION, DATA_TYPE_UINT8, FM_EREPORT_VERS0, NULL); } }
使用 pci_ereport_post(9F) 时,会自动检测和报告与 PCI(包括 PCI、PCI-X 和 PCI-E)相关的错误。
void pci_ereport_post(dev_info_t *dip, ddi_fm_error_t *derr, uint16_t *xx_status)
驱动程序不需要为 PCI 本地总线配置状态寄存器中发生的错误生成特定于驱动程序的 ereport。pci_ereport_post() 函数可以报告数据奇偶校验错误、主机异常中止、目标异常中止、发出信号的系统错误等。
如果 pci_ereport_post() 将由驱动程序使用,则此前 pci_ereport_setup(9F) 必须已经在驱动程序的 attach(9E) 例程中调用,pci_ereport_teardown(9F) 随后必须在驱动程序的 detach(9E) 例程中调用。
下面的 bge 代码样例显示了从驱动程序的错误处理程序中调用 pci_ereport_post() 函数的 bge 驱动程序。另请参见注册错误处理程序。
/* * The I/O fault service error handling callback function */ /*ARGSUSED*/ static int bge_fm_error_cb(dev_info_t *dip, ddi_fm_error_t *err, const void *impl_data) { /* * as the driver can always deal with an error * in any dma or access handle, we can just return * the fme_status value. */ pci_ereport_post(dip, err, NULL); return (err->fme_status); }
针对 I/O 控制器的常见错误定义了一组标准的设备 ereport。只要检测到本节中所述的错误症状之一,便应生成这些 ereport。
本节中所述的 ereport 将分发给 eft 诊断引擎以进行诊断,eft 诊断引擎使用一组常用的标准规则来诊断这些 ereport。设备驱动程序检测的其他任何错误都必须在 Sun 事件注册表中定义为 ereport 事件,并必须伴有特定于设备的诊断软件或 eft 规则。
驱动程序已检测到设备处于无效状态。
当驱动程序检测到所传送或接收的数据看起来无效时,该驱动程序应发布错误。例如,在 bge 代码中,当 bge_chip_reset() 和 bge_receive_ring() 例程检测到无效数据时,这些例程将生成 ereport.io.device.inval_state 错误。
/* * The SEND INDEX registers should be reset to zero by the * global chip reset; if they're not, there'll be trouble * later on. */ sx0 = bge_reg_get32(bgep, NIC_DIAG_SEND_INDEX_REG(0)); if (sx0 != 0) { BGE_REPORT((bgep, "SEND INDEX - device didn't RESET")); bge_fm_ereport(bgep, DDI_FM_DEVICE_INVAL_STATE); return (DDI_FAILURE); } /* ... */ /* * Sync (all) the receive ring descriptors * before accepting the packets they describe */ DMA_SYNC(rrp->desc, DDI_DMA_SYNC_FORKERNEL); if (*rrp->prod_index_p >= rrp->desc.nslots) { bgep->bge_chip_state = BGE_CHIP_ERROR; bge_fm_ereport(bgep, DDI_FM_DEVICE_INVAL_STATE); return (NULL); }
设备已报告自我纠正的内部错误。例如,设备的内部缓冲区中的硬件已检测到可纠正的 ECC 错误。
bge 驱动程序中未使用此错误标志。有关使用此错误的示例,请参见 OpenSolaris 中的 nxge_fm.c 文件。执行以下步骤来研究 nxge 驱动程序代码:
转到 OpenSolaris。
单击页面右上角的 "Source Browser"(源代码浏览器)。
在 "File Path"(文件路径)字段中输入 nxge。
单击 "Search"(搜索)按钮。
设备已报告无法纠正的内部错误。例如,设备的内部缓冲区中的硬件已检测到不可纠正的 ECC 错误。
bge 驱动程序中未使用此错误标志。有关使用此错误的示例,请参见 OpenSolaris 中的 nxge_fm.c 文件。
驱动程序检测到数据传输已意外停顿。
bge_factotum_stall_check() 例程提供了停顿检测的示例。
dogval = bge_atomic_shl32(&bgep->watchdog, 1); if (dogval < bge_watchdog_count) return (B_FALSE); BGE_REPORT((bgep, "Tx stall detected, watchdog code 0x%x", dogval)); bge_fm_ereport(bgep, DDI_FM_DEVICE_STALL); return (B_TRUE);
设备未对驱动程序命令进行响应。
bge_chip_poll_engine(bge_t *bgep, bge_regno_t regno, uint32_t mask, uint32_t val) { uint32_t regval; uint32_t n; for (n = 200; n; --n) { regval = bge_reg_get32(bgep, regno); if ((regval & mask) == val) return (B_TRUE); drv_usecwait(100); } bge_fm_ereport(bgep, DDI_FM_DEVICE_NO_RESPONSE); return (B_FALSE); }
设备引发了过多的连续性无效中断。
bge() 驱动程序内的 bge_intr 例程提供了有问题的中断检测的示例。bge_fm_ereport() 函数是 ddi_fm_ereport_post(9F) 函数的包装。请参见对错误事件排队中的 bge_fm_ereport() 示例。
if (bgep->missed_dmas >= bge_dma_miss_limit) { /* * If this happens multiple times in a row, * it means DMA is just not working. Maybe * the chip has failed, or maybe there's a * problem on the PCI bus or in the host-PCI * bridge (Tomatillo). * * At all events, we want to stop further * interrupts and let the recovery code take * over to see whether anything can be done * about it ... */ bge_fm_ereport(bgep, DDI_FM_DEVICE_BADINT_LIMIT); goto chip_stop; }
具有故障管理功能的驱动程序必须指示错误是否影响了设备所提供的服务。检测错误并在必要时关闭服务之后,驱动程序应调用 ddi_fm_service_impact(9F) 例程来反映设备实例的当前服务状态。诊断和恢复软件可以使用该服务状态来帮助确定问题或对问题做出反应。
当驱动程序本身检测到错误时以及框架检测到错误并将访问或 DMA 句柄标记为有故障时,均应调用 ddi_fm_service_impact() 例程。
void ddi_fm_service_impact(dev_info_t *dip, int svc_impact)
ddi_fm_service_impact() 接受以下服务影响值 (svc_impact):
由于设备故障或软件缺陷,设备提供的服务不可用。
驱动程序无法提供正常服务,但驱动程序可以提供部分服务或降级的服务。例如,驱动程序可能必须重复尝试执行操作才能取得成功,或者它至少要以配置的速度运行。
驱动程序已检测到错误,但设备实例提供的服务不会受到影响。
设备提供的所有服务都已恢复。
调用 ddi_fm_service_impact() 时会根据服务影响例程的服务影响参数代表驱动程序生成以下 ereport:
ereport.io.service.lost
ereport.io.service.degraded
ereport.io.service.unaffected
ereport.io.service.restored
在以下 bge 代码中,驱动程序确定由于出现错误,它无法成功地重新开始传送或接收数据包。设备的服务状态转换为 DDI_SERVICE_LOST。
/* * All OK, reinitialize hardware and kick off GLD scheduling */ mutex_enter(bgep->genlock); if (bge_restart(bgep, B_TRUE) != DDI_SUCCESS) { (void) bge_check_acc_handle(bgep, bgep->cfg_handle); (void) bge_check_acc_handle(bgep, bgep->io_handle); ddi_fm_service_impact(bgep->devinfo, DDI_SERVICE_LOST); mutex_exit(bgep->genlock); return (DDI_FAILURE); }
不应从已注册的回调例程中调用 ddi_fm_service_impact() 函数。
DDI_FM_ACCCHK_CAPABLE
设备驱动程序必须设置其访问属性,以指示它能够处理寄存器读取或写入期间发生的程控 I/O (programmed I/O, PIO) 访问错误。应将 ddi_device_acc_attr(9S) 结构中的 devacc_attr_access 字段设置为驱动程序可以检查和处理数据路径错误的系统的指示器。ddi_device_acc_attr 结构包含以下成员:
ushort_t devacc_attr_version; uchar_t devacc_attr_endian_flags; uchar_t devacc_attr_dataorder; uchar_t devacc_attr_access; /* access error protection */
在到设备或来自设备的数据路径中检测到的错误可由设备驱动程序的一个或多个父结点来处理。
devacc_attr_version 字段必须至少设置为 DDI_DEVICE_ATTR_V1。如果 devacc_attr_version 字段未设置为 DDI_DEVICE_ATTR_V1,则将忽略 devacc_attr_access 字段。
可将 devacc_attr_access 字段设置为以下值:
此标志指示当出现错误时系统将采取缺省操作(如果合适,则进入紧急状态)。DDI_FM_ACCCHK_CAPABLE 驱动程序不能使用此属性。
此标志指示系统将尝试处理与访问句柄关联的错误并从该错误中恢复。驱动程序应使用用于 Solaris 设备驱动程序的防御性编程方法中介绍的技术,并应使用 ddi_fm_acc_err_get(9F) 定期检查错误,之后才能允许数据回传给调用应用程序。
DDI_FLAGERR_ACC 标志可提供:
通过驱动程序回调收到的错误通知
通过 ddi_fm_acc_err_get(9F) 注册的驱动程序回调获得的错误通知
DDI_CAUTIOUS_ACC 标志可为驱动程序进行的每个程控 I/O 访问提供高级别的保护。
DDI_CAUTIOUS_ACC 标志指示访问驱动程序可以预见错误。系统尝试尽可能正常地处理与此句柄关联的错误并从该错误中恢复。最终不会生成错误报告,但句柄的 fme_status 标志将设置为 DDI_FM_NONFATAL。此标志在功能上与 ddi_peek(9F) 和 ddi_poke(9F) 等效。
对总线的独占访问
陷阱 (On trap) 保护-(ddi_peek() 和 ddi_poke())
通过使用 ddi_fm_handler_register(9F) 注册的驱动程序回调获得的错误通知
通过 ddi_fm_acc_err_get(9F) 注册的驱动程序回调获得的错误通知
通常,驱动程序应在代码路径中的适当接合点处检查数据路径错误,以确保数据一致并确保 I/O 软件堆栈中显示正确的错误状态。
DDI_FM_ACCCHK_CAPABLE 设备驱动程序必须将其 devacc_attr_access 字段设置为 DDI_FLAGERR_ACC 或 DDI_CAUTIOUS_ACC。
与访问句柄设置一样,DDI_FM_DMACHK_CAPABLE 设备驱动程序必须将其 ddi_dma_attr(9S) 结构的 dma_attr_flag 字段设置为 DDI_DMA_FLAGERR 标志。系统将尝试从与设置了 DDI_DMA_FLAGERR 的句柄关联的错误中恢复。ddi_dma_attr 结构包含以下成员:
uint_t dma_attr_version; /* version number */ uint64_t dma_attr_addr_lo; /* low DMA address range */ uint64_t dma_attr_addr_hi; /* high DMA address range */ uint64_t dma_attr_count_max; /* DMA counter register */ uint64_t dma_attr_align; /* DMA address alignment */ uint_t dma_attr_burstsizes; /* DMA burstsizes */ uint32_t dma_attr_minxfer; /* min effective DMA size */ uint64_t dma_attr_maxxfer; /* max DMA xfer size */ uint64_t dma_attr_seg; /* segment boundary */ int dma_attr_sgllen; /* s/g length */ uint32_t dma_attr_granular; /* granularity of device */ uint_t dma_attr_flags; /* Bus specific DMA flags */
设置 DDI_DMA_FLAGERR 标志的驱动程序应使用用于 Solaris 设备驱动程序的防御性编程方法中介绍的技术,并且应该在 DMA 事务完成时或者代码路径的重要点处使用 ddi_fm_dma_err_get(9F) 检查数据路径错误。这样可以确保数据一致并且 I/O 软件堆栈中显示正确的错误状态。
使用 DDI_DMA_FLAGERR 可提供:
通过使用 ddi_fm_handler_register() 注册的驱动程序回调获得的错误通知
通过调用 ddi_fm_dma_err_get() 观测到的错误状态
如果发生的故障影响到通过句柄映射的资源,则会更新错误状态结构,以反映在总线或 I/O 数据路径中的其他设备驱动程序在处理错误期间捕获的错误信息。
void ddi_fm_dma_err_get(ddi_dma_handle_t handle, ddi_fm_error_t *de, int version) void ddi_fm_acc_err_get(ddi_acc_handle_t handle, ddi_fm_error_t *de, int version)
在 ddi_fm_dma_err_get(9F) 和 ddi_fm_acc_err_get(9F) 函数分别为 DMA 或访问句柄返回错误状态。应将版本字段设置为 DDI_FME_VERSION。
访问句柄错误意味着已检测到一种错误,该错误影响到达使用该访问句柄的设备或来自该设备的 PIO 事务。该驱动程序接收到的任何数据(例如,通过最新的 ddi_get8(9F) 调用)均应被视为可能已损坏。发送到设备的任何数据(例如,通过最新的 ddi_put32(9F) 调用)也都可能已损坏,或根本未被接收。然而,基本故障可能是瞬态的,因而驱动程序可以通过调用 ddi_fm_acc_err_clear(9F)、将设备重置为已知状态、重试任何可能出错的事务来尝试进行恢复。
如果指示 DMA 句柄出现错误,则意味着检测到错误已经(或将要)影响设备和当前绑定到句柄(如果句柄当前未绑定,则为最近绑定)的内存之间的 DMA 事务。可能的原因包括 DMA 数据路径中的组件出现故障,或设备尝试进行无效的 DMA 访问。驱动程序通过重试和重新分配内存可能能够继续。应将当前(或以前)绑定到句柄的内存的内容视为不确定的,并应将其释放回系统。一旦绑定或重新绑定句柄,与当前事务关联的故障指示便会丢失,但由于故障可能持续存在,因此将来的 DMA 操作可能不会成功。
在句柄检测到错误后,驱动程序希望在无需释放和重新分配句柄的前提下重试请求时,应调用 ddi_fm_acc_err_clear() 和 ddi_fm_dma_err_clear(9F) 例程。
void ddi_fm_acc_err_clear(ddi_acc_handle_t handle, int version) void ddi_fm_dma_err_clear(ddi_dma_handle_t handle, int version)
当操作系统通过陷阱或错误中断检测到错误时,错误处理活动可能会开始。如果负责处理错误的软件(错误处理程序)无法立即隔离出现故障的 I/O 操作中涉及的设备,它必须尝试在设备树内查找可以执行错误隔离的软件模块。Solaris 设备树提供了向子级传播结点驱动程序错误处理活动的结构化方法,这些子级可能对错误具有更详细的了解,并可捕获错误状态和隔离问题设备。
驱动程序可以使用 I/O 故障服务框架注册错误处理程序回调。错误处理程序应特定于错误的类型以及进行错误检测的子系统。调用驱动程序的错误处理程序例程时,驱动程序必须检查与设备事务关联的任何未解决的错误并生成 ereport 事件。驱动程序还必须在其 ddi_fm_error(9S) 结构中返回错误处理程序状态。例如,如果已经确定系统的完整性受到威胁,则错误处理程序可能采取的最合适的操作是使系统进入紧急状态。
当错误可能与特定的设备实例关联时,父结点驱动程序会调用回调。注册错误处理程序的设备驱动程序必须为 DDI_FM_ERRCB_CAPABLE。
void ddi_fm_handler_register(dev_info_t *dip, ddi_err_func_t handler, void *impl_data)
ddi_fm_handler_register(9F) 例程向 I/O 故障服务框架注册错误处理程序回调。应在驱动程序故障管理初始化 (ddi_fm_init()) 之后在驱动程序的 attach(9E) 入口点中调用 ddi_fm_handler_register() 函数,以便进行回调注册。
检查与设备事务关联的任何未解决的硬件错误,并生成 ereport 事件以便进行诊断。对于 PCI、PCI-x 或 PCI Express 设备,通常使用 pci_ereport_post() 执行此操作,如检测和报告与 PCI 相关的错误中所述。
在其 ddi_fm_error 结构中返回错误处理程序状态:
DDI_FM_OK
DDI_FM_FATAL
DDI_FM_NONFATAL
DDI_FM_UNKNOWN
驱动程序错误处理程序会接收以下内容:
在驱动程序的控制下接收指向设备实例 (dip) 的指针
包含常见故障管理数据和错误处理状态的数据结构 (ddi_fm_error)
指向在处理程序注册时指定的任何特定于实现的数据 (impl_data) 的指针
必须在驱动程序的 attach(9E) 或 detach(9E) 入口点的内核上下文中调用 ddi_fm_handler_register() 和 ddi_fm_handler_unregister(9F) 例 程。可以从内核、中断或高级别中断上下文中调用注册的错误处理程序回调。因此,错误处理程序:
不得持有锁
等待资源时不得处于休眠状态
设备驱动程序负责:
隔离可能已导致错误的设备实例
恢复与错误关联的事务
报告错误对服务的影响
针对视为致命的错误调度设备关闭
可在错误处理程序函数内执行这些操作。但是,由于对锁定的限制以及错误处理程序函数并非始终了解故障发生时驱动程序所执行操作的上下文,因此,更通常的做法是,如前所述在驱动程序的正常路径内内联调用 ddi_fm_acc_err_get(9F) 和 ddi_fm_dma_err_get(9F) 之后执行这些操作。
/* * The I/O fault service error handling callback function */ /*ARGSUSED*/ static int bge_fm_error_cb(dev_info_t *dip, ddi_fm_error_t *err, const void *impl_data) { /* * as the driver can always deal with an error * in any dma or access handle, we can just return * the fme_status value. */ pci_ereport_post(dip, err, NULL); return (err->fme_status); }
驱动程序错误处理回调会被传递一个指向数据结构的指针,该数据结构包含常见的故障管理数据和错误处理状态。
数据结构 ddi_fm_error 包含用于当前错误的 FMA 协议 ENA、错误处理程序回调的状态、错误预期标志以及与父结点检测到的错误关联的任何潜在访问或 DMA 句柄。
此字段通过调用父结点来进行初始化,并可能在达到驱动程序的已注册回调例程之前随着错误处理传播链不断增大。如果驱动程序检测到自身的相关错误,它应在调用 ddi_fm_ereport_post() 之前使此 ENA 增大。
如果父级能够将在其级别上检测到的错误与设备驱动程序映射或绑定的句柄相关联,则这些字段中包含有效的访问或 DMA 句柄。
如果调用父级确定错误是由于 DDI_CAUTIOUS_ACC 受保护的操作引起的,fme_flag 将设置为 DDI_FM_ERR_EXPECTED。在这种情况下,fme_acc_handle 有效,并且驱动程序应只检查并报告不与 DDI_CAUTIOUS_ACC 受保护操作关联的错误。否则,fme_flag 将设置为 DDI_FM_ERR_UNEXPECTED,并且驱动程序必须执行完整的错误处理任务。
从其错误处理程序回调返回后,驱动程序必须立即将 fme_status 设置为以下值之一:
故障管理守护进程 fmd(1M) 为诊断引擎 (diagnosis engine, DE) 插件模块的开发提供编程接口。可通过编写 DE 来使用和诊断任何错误遥测或特定错误遥测。eft DE 设计为根据以 Eversholt 语言指定的诊断规则来诊断任意数量的 ereport 类。
大多数 I/O 子系统都使用 eft DE 和规则集来诊断与设备和设备驱动程序相关的问题。已为 PCI 叶设备指定了一组报告标准 I/O 控制器错误中列出的标准 ereport。除了这些 ereport 外,同时还提供了提取遥测并确定关联设备故障的 eft 诊断规则。生成这些 ereport 的驱动程序不需要交付其他任何诊断软件或 eft 规则。
检测和生成这些 ereport 时将产生以下故障事件:
PCI 总线上的硬件故障
设备内的硬件故障
设备中的硬件故障或驱动程序的缺陷,导致设备发送无效请求
设备中的硬件故障,导致驱动程序不对有效请求做出响应
链路中的硬件故障
链路关闭,导致设备无法对有效请求做出响应
设备内的硬件故障
设备中的硬件故障或驱动程序的缺陷,导致设备发送无效请求
设备中的硬件故障,导致设备无法对有效请求做出响应
要生成其他 ereport 或提供更专门的诊断软件或 eft 规则的驱动程序开发者可以通过编写基于 C 的 DE 或 eft 诊断规则集来实现此目标。有关信息,请参见 OpenSolaris 项目的故障管理社区。
Sun 事件注册表是所有类名、ereport、故障、缺陷、混乱和可疑列表 (list.suspect) 事件的中央信息库。该事件注册表中还包含所有事件成员有效负荷的当前定义以及重要的非有效负荷信息,例如内部文档、可疑列表、字典和知识文章。例如,ereport.io 和 fault.io 是对 I/O 驱动程序开发者特别重要的两个基本类名。
FMA 事件协议定义随每个注册事件提供的有效负荷成员的基本集合。开发者还可以定义其他事件,以帮助诊断引擎(或 eft 规则)将可疑列表范围缩小至特定故障。
本节使用以下术语:
用于描述订阅 fault.* 或 list.* 事件的故障管理器模块的通用术语。代理用于弃用有故障的资源、将诊断结果告知管理员并桥接至更高级别的管理框架。
ASRU 是可由软件或硬件禁用以便隔离系统中的问题并抑制生成更多错误报告的资源。
一个故障管理模块,其用途是通过订阅传入错误事件的一个或多个类并使用这些事件来解决与系统中每个问题关联的案例来诊断问题。
错误编号关联 (Error Numeric Association, ENA) 是一个编码的整数,用于唯一标识给定故障区域和时间段内的错误报告。ENA 还指示错误与以前的错误之间的关系,以作为辅助影响。
意外的情况、结果、信号或数据。错误是问题在系统中的症状。每个问题通常都会产生许多不同种类的错误。
随特定错误捕获的数据。错误报告格式通过创建命名错误报告的类和通过定义使用 Sun 事件注册表的模式提前定义。
表示错误报告实例的数据结构。错误事件表示为名称-值对列表。
硬件组件的故障行为。
可为其枚举一组特定故障的硬件或软件元素的逻辑分区。
在协议中编码的故障诊断的实例。
负责通过一个或多个诊断引擎进行故障诊断以及状态管理的软件组件。
FMRI 是类似于 URL 的标识符,它在故障管理系统中充当特定资源的规范名称。每个 FMRI 中都包括一个标识资源类型的模式,以及特定于该模式的一个或多个值。FMRI 可以表示为类似于 URL 的字符串或名称-值对列表数据结构。
FRU 是可在现场由客户或服务提供商更换的资源。可为硬件(例如,系统板)或软件(例如,软件包或修补程序)定义 FRU。
以下资源可提供附加信息:
FMA Messaging web site(FMA 消息传送 Web 站点)
本节针对设备驱动程序提供了一些方法,可用于避免系统出现紧急情况并挂起、浪费系统资源以及扩大数据损坏范围。如果除了 I/O 故障服务框架之外,驱动程序还将这些防御性编程做法用于错误处理和诊断,则认为该驱动程序已进行强化。
所有 Solaris 驱动程序都应遵循以下编码做法:
每个硬件都应由设备驱动程序的单独实例来控制。请参见设备配置概念。
程控 I/O (Programmed I/O, PIO) 只能使用适当的数据访问句柄借助 DDI 访问函数来执行。请参见第 7 章。
设备驱动程序必须假定从该设备接收的数据可能已损坏。在使用数据之前驱动程序必须检查数据的完整性。
驱动程序必须避免向系统的其余部分释放错误数据。
在驱动程序中仅使用记录的 DDI 函数和接口。
驱动程序必须确保设备仅向完全由该驱动程序控制的 DMA 缓冲区 (DDI_DMA_READ) 的内存页中写入内容。此方法可以防止 DMA 故障损坏系统主内存的任意部分。
如果设备已锁定,设备驱动程序必须不能无限使用系统资源。如果设备声明连续处于忙状态,该驱动程序应超时。驱动程序还应检测异常(有问题的)中断请求,并执行适当操作。
在 Solaris OS 中,设备驱动程序必须支持热插拔。
设备驱动程序必须使用回调,而非等待资源。
发生故障后,驱动程序必须释放资源。例如,即使在硬件出现故障后,系统也必须能够关闭所有次要设备并分离驱动程序实例。
Solaris 内核允许一个驱动程序具有多个实例。每个实例都有自己的数据空间,但与其他实例共享文本和某些全局数据。设备是基于每个实例进行管理的。除非将驱动程序设计用于在内部处理任何故障转移,否则驱动程序应针对每个硬件使用单独的实例。一个插槽可能具有一个驱动程序的多个实例,例如,具有多功能卡。
驱动程序进行的所有 PIO 访问都必须使用以下系列例程中的 Solaris DDI 访问函数:
ddi_getX
ddi_putX
ddi_rep_getX
ddi_rep_putX
驱动程序不应根据 ddi_regs_map_setup(9F) 返回的地址直接访问已映射的寄存器。请避免使用 ddi_peek(9F) 和 ddi_poke(9F) 例程,因为这些例程不使用访问句柄。
由于 DDI 访问提供了对数据读入内核的方式进行控制的机会,因此 DDI 访问机制很重要。
以下各节介绍可能发生数据损坏的位置以及如何检测损坏。
驱动程序应假定,从设备获取的任何数据(无论通过 PIO 还是 DMA)都可能已被损坏。需要特别指出的是,对于基于设备数据的指针、内存偏移以及数组索引要格外小心。此类值可以是恶性的,因为取消引用这些值时会导致内核出现紧急情况。在使用之前,应针对所有此类值执行范围和对齐检查(如果需要)。
即使是非恶性指针,仍然可能具有误导性。例如,指针可能指向某个对象的有效但错误的实例。驱动程序应尽量交叉检查指针以及该指针所指向的对象,或者对通过该指针获得的数据进行验证。
其他类型的数据也可能具有误导性,如包长度、状态字或通道 ID。应尽可能地对这些数据类型进行检查。可对包长度进行范围检查,以确保该长度既不为负,也不比包含缓冲区大。可针对“不可能”的位对状态字进行检查。可将通道 ID 与有效 ID 的列表进行匹配。
其中,一个值标识一个流,驱动程序必须确保该流仍然存在。处理 STREAMS 的异步性质意味着可在设备中断仍未完成时中断流。
驱动程序不应从设备中重新读取数据。数据应只读取一次,然后进行验证并以驱动程序的本地状态进行存储。此方法可避免数据在初始读取时正确但以后重新读取时错误的风险。
驱动程序还应确保已限制所有循环。例如,返回连续 BUSY 状态的设备不能锁定整个系统。
设备错误可能导致将损坏的数据放置在接收缓冲区中。此类损坏与设备域之外(例如,网络中)发生的损坏几乎没有区别。通常可利用现有软件处理此类问题。例如,在协议栈的传输层进行完整性检查,或者在使用该设备的应用程序内进行完整性检查。
如果不打算在较高层对已接收的数据进行完整性检查,则可在驱动程序自身内对数据进行完整性检查。对已接收数据中的损坏进行检测的方法通常特定于设备。例如,校验和与 CRC 即是可执行的检查种类。
有缺陷的设备可能通过总线启动错误的 DMA 传输。此类数据传输可能会损坏以前传送的正常数据。发生故障的设备可能会生成损坏的地址,该地址可能会污染甚至不属于自己的驱动器的内存。
在具有 IOMMU 的系统中,设备只能写入映射为对于 DMA 可写入的页。因此,此类页只应归一个驱动程序实例所有。不应将这些页与其他任何内核结构共享。尽管该页被映射为对于 DMA 可写入的页,但驱动程序应怀疑该页中的数据。在将页传递到驱动程序之外以及对数据进行验证之前,必须从 IOMMU 取消映射页。
可以使用 ddi_umem_alloc(9F) 来保证已分配整个对齐的页,或分配多页并忽略第一个页边界之下的内存。可以使用 ddi_ptob(9F) 确定 IOMMU 页的大小。
或者,驱动程序可以选择在处理数据之前将其复制到内存中较安全的部分。如果已执行此操作,则必须先使用 ddi_dma_sync(9F) 同步数据。
对 ddi_dma_sync() 的调用应在使用 DMA 向设备传送数据之前指定 SYNC_FOR_DEV,并在使用 DMA 从设备向内存传送数据之后指定 SYNC_FOR_CPU。
在某些基于 PCI 且具有 IOMMU 的系统中,设备可以使用 PCI 双地址循环(64 位地址)绕过 IOMMU。此功能使设备可能损坏主内存的任何区域。设备驱动程序不得尝试使用此类模式,并应禁用此类模式。
驱动程序必须标识有问题的中断,因为不断声明中断会严重影响系统性能,几乎一定会使单处理器计算机产生延迟。
有时,驱动程序可能很难将特定中断标识为无效。对于网络驱动程序,如果指示接收中断,但没有新缓冲区可用,则无需执行任何操作。当此情况为孤立事件时,这并不是一个问题,因为另一个例程(如读取服务)可能已完成实际工作。
另一方面,出现连续中断但不需要驱动程序处理任何工作,这表示一个有问题的中断行。因此,平台允许在执行防御性操作之前发生许多明显无效的中断。
当显示有工作需要处理时,挂起的设备可能无法更新其缓冲区描述符。驱动程序应阻止此类重复请求。
在某些情况下,平台特定总线驱动程序可能能够识别一直未请求的中断,并且可以禁用违例设备。但是,这依赖于驱动程序识别有效中断并返回相应值的能力。除非驱动程序检测到设备合法声明了中断,否则驱动程序应返回 DDI_INTR_UNCLAIMED 结果。仅当设备实际要求驱动程序执行一些有用的工作时,中断才是合法的。
其他更偶然的中断的合法性更难认证。预期中断标志是评估中断是否有效的有用工具。请考虑一个中断,如描述符释放,如果先前已分配设备的所有描述符,则可生成该中断。如果驱动程序检测到它已从卡中获取最后一个描述符,便可以设置一个预期中断标志。如果传送关联的中断时未设置此标志,则该中断为可疑中断。
有些提示性中断可能无法预测,例如指示介质已断开连接或帧同步已丢失的中断。检测此类中断是否有问题的最简单方法是:第一次出现中断时屏蔽此特定源,直到下一个轮询周期。
如果在禁用期间再次发生中断,则该中断无效。有些设备具有即使掩码寄存器已禁用关联源并且不可能引起中断时仍可读取的中断状态位。您可以设计更适合的、特定于设备的算法。
应避免对中断状态位进行无限循环。如果传送开始时设置的状态位都不要求任何实际工作,请中断此类循环。
除了前面各节中讨论的要求外,还请考虑以下问题:
线程交互
自上而下请求的威胁
自适应策略
设备驱动程序中内核出现紧急情况通常是由设备发生故障后内核线程的意外交互引起的。设备出现故障时,线程可能会以您意想不到的方式进行交互。
如果处理例程较早终止,则条件变量等待程序将由于从未给定预期信号而被阻塞。尝试向其他模块通知故障或处理意外回调会导致不需要的线程交互。请考虑设备发生故障期间获取和释放互斥锁的顺序。
如果源自上游 STREAMS 模块的线程被用来意外返回该模块,则这些线程可能得出自相矛盾的结果。请考虑使用备用线程来处理异常消息。例如,过程可以使用读端服务例程与 M_ERROR 进行通信,而不直接使用读端 putnext(9F) 处理错误。
在关闭期间由于故障而无法处于静态的发生故障的 STREAMS 设备会在流终止后生成中断。中断处理程序不得尝试使用过时的流指针来处理消息。
针对有缺陷的硬件为系统提供保护的同时,您还需要针对驱动程序误用提供保护。尽管驱动程序可以假定内核基础结构始终正确(受信任的核心),但传递给它的用户请求可能具有破坏性。
例如,用户可以请求对用户提供的数据块 (M_IOCTL) 执行某一操作,该数据块小于消息的控制部分所指示的块大小。驱动程序绝不应该信任用户应用程序。
请考虑您的驱动程序可以接收的每种类型的 ioctl 的构造以及 ioctl 可能引起的潜在危害。驱动程序应执行检查,以确保它不处理格式错误的 ioctl。
驱动程序可以使用有故障的硬件继续提供服务。驱动程序可以尝试使用用于访问设备的备用策略来解决已确定的问题。假定损坏的硬件不可预测并且已知与其他设计复杂性关联的风险,则自适应策略并不总是明智的选择。这些策略最多应限制为定期中断轮询和重试尝试。定期重试设备可使驱动程序了解设备恢复的时间。强制驱动程序禁用中断后,定期轮询可以控制中断机制。
理论上,系统始终有一个备用设备来提供重要的系统服务。内核或用户空间中的服务多路复用程序可在设备出现时提供维护系统服务的最佳方法。此类做法将不在本节中进行介绍。
驱动程序强化测试工具测试是否已正确实现 I/O 故障服务和防御性编程要求。强化的设备驱动程序可从潜在的硬件故障中复原。必须在驱动程序开发过程中测试设备驱动程序的复原能力。此类测试要求驱动程序以受控制并且可重复的方式处理多种典型硬件故障。通过驱动程序测试工具可在软件中仿真此类硬件故障。
驱动程序强化测试工具是一种 Solaris 设备驱动程序开发工具。该测试工具可在处于开发阶段的驱动程序访问其硬件时注入多种仿真硬件故障。本节介绍如何配置测试工具、创建错误注入规范(称为 errdef)以及对设备驱动程序执行测试。
测试工具可截获从驱动程序到各种 DDI 例程的调用,然后损坏调用的结果,就像硬件引起损坏一样。此外,该工具还允许损坏对特定寄存器的访问以及定义更多随机类型的损坏。
测试工具可以在运行指定的工作负荷期间通过跟踪所有寄存器访问以及直接内存访问 (direct memory access, DMA) 和中断使用情况自动生成测试脚本。生成的脚本将在向每个访问中注入一组故障的同时,重新运行该工作负荷。
驱动程序测试器应从生成的脚本中删除重复的测试案例。
测试工具作为名为 bofi(表示 bus_ops 故障注入)的设备驱动程序和两个用户级实用程序(th_define(1M) 和 th_manage(1M))来实现。
测试工具可执行以下任务:
验证 Solaris DDI 服务的使用是否符合规则
促进程控 I/O (programmed I/O, PIO) 和 DMA 请求的受控损坏以及对中断的干扰,从而仿真在驱动程序所管理的硬件中发生的故障
促进在 CPU 与设备之间的数据路径中的故障仿真,这些故障从父结点驱动程序中进行报告
在运行指定的工作负荷期间监视驱动程序的访问并生成故障注入脚本
驱动程序强化测试工具截获并在请求时损坏驱动程序对其硬件进行的每次访问。本节提供您在创建故障以测试驱动程序的可复原性时应了解的信息。
Solaris 设备在名为设备树(devinfo 树)的类似树的结构内进行管理。devinfo 树的每个节点都存储着与系统中某一设备的特定实例相关的信息。每个叶节点对应于一个设备驱动程序,而其他所有节点都称为子树节点。通常,结点 (nexus) 表示总线。总线节点将叶驱动程序与总线相关项隔离,从而可以生成在体系结构上独立的驱动程序。
许多 DDI 函数(特别是数据访问函数)都会导致向上调用总线结点驱动程序。当叶驱动程序访问其硬件时,它会将句柄传递给访问例程。总线结点了解如何处理句柄和实现请求。符合 DDI 标准的驱动程序只通过使用这些 DDI 访问例程来访问硬件。在这些向上调用到达指定的总线结点之前,测试工具会截获这些向上调用。如果数据访问与驱动程序测试器指定的标准相符,访问将被损坏。如果数据访问与该标准不符,则将其提供给总线结点,以便通过常规方式进行处理。
驱动程序通过使用 ddi_regs_map_setup(9F) 函数获取访问句柄:
ddi_regs_map_setup(dip, rset, ma, offset, size, handle)
参数指定要映射哪个“板外”内存。驱动程序在引用映射的 I/O 地址时必须使用返回的句柄,因为句柄用于将驱动程序与总线层次结构的详细信息隔离开来。因此,请不要直接使用返回的映射地址 ma。直接使用映射地址会导致当前以及将来无法使用数据访问函数机制。
I/O 到主机:
ddi_getX(handle, ma) ddi_rep_getX(handle, buf, ma, repcnt, flag)
主机到 I/O:
ddi_putX(handle, ma, value) ddi_rep_putX()
X 和 repcnt 是要传送的字节数。X 是总线传送大小,为 8、16、32 或 64 字节。
DMA 具有与之类似但更为丰富的一组数据访问函数。
驱动程序强化测试工具属于 Solaris Developer Cluster 的一部分。如果尚未安装此 Solaris 簇,则必须手动安装适用于您平台的测试工具软件包。
要安装测试工具软件包(SUNWftduu 和 SUNWftdur),请使用 pkgadd(1M) 命令。
以超级用户身份转到软件包所在目录,并键入:
# pkgadd -d . SUNWftduu SUNWftdur |
安装测试工具后,请在 /kernel/drv/bofi.conf 文件中设置属性,以将工具配置为与驱动程序交互。完成工具配置后,重新引导系统以装入工具驱动程序。
测试工具行为由 /kernel/drv/bofi.conf 配置文件中设置的引导时属性控制。
首次安装工具时,可通过设置以下属性来使工具可以截获对驱动程序的 DDI 访问:
总线结点类型,如 PCI 总线
所测试的驱动程序的名称
例如,要测试名为 xyznetdrv 的 PCI 总线网络驱动程序,请设置以下属性值:
bofi-nexus="pci" bofi-to-test="xyznetdrv"
其他属性与用于从使用 PIO 的外围设备读取和写入以及与使用 DMA 的外围设备进行双向数据传送的 Solaris DDI 数据访问机制的使用和工具检查相关。
设置此属性时,测试工具将检查传递给 PIO 数据访问函数的参数的一致性。
设置此属性时,测试工具将验证 ddi_map_regs_setup(9F) 返回的映射地址未在数据访问函数的上下文之外使用。
设置此属性时,测试工具将验证 DMA 函数的使用是否正确并确保驱动程序对 ddi_dma_sync(9F) 的使用符合规范。
本节介绍如何使用 th_define(1M) 和 th_manage(1M) 命令创建并注入故障。
th_define 实用程序为 bofi 设备驱动程序提供了一个接口,以用于定义 errdef。errdef 对应于有关如何损坏设备驱动程序对其硬件的访问的规范。th_define 命令行参数确定要注入的故障的确切性质。如果提供的参数定义了一致的 errdef,th_define 进程将使用 bofi 驱动程序存储 errdef。该进程将使自身暂停,直至 errdef 给定的条件得到满足为止。实际上,当访问计数达到零 (0) 时,暂停将结束。
测试工具在数据访问级别运行。数据访问具有以下特征:
正在访问的硬件类型(驱动程序名称)
正在访问的硬件实例(驱动程序实例)
正在测试的寄存器集
作为目标的寄存器集的子集
传送的方向(读取或写入)
访问类型(PIO 或 DMA)
测试工具截获数据访问并将适当的故障注入驱动程序。th_define(1M) 命令指定的 errdef 可对以下信息编码:
正在测试的驱动程序实例和寄存器集(-n name、-i instance 和 -r reg_number)。
满足损坏资格的寄存器集的子集。通过提供进入寄存器集的偏移和距离该偏移的长度 (-l offset [len]) 来指示该子集。
要截获的访问的种类: log、pio、dma、pio_r、pio_w、dma_r、dma_w、intr (-a acc_types)。
应视为有故障的访问数量 (-c count [failcount ])。
应对合格的访问应用的损坏种类 (-o operator [operand])。
使用固定值替换数据 (EQUAL)
对数据执行按位操作 (AND, OR, XOR)
忽略传送(对于主机到 I/O 访问,为 NO_TRANSFER)
丢失、延迟或注入虚假中断 (LOSE, DELAY, EXTRA)
使用 -a acc_chk 选项可仿真 errdef 中的框架故障。
注入故障的过程涉及两个阶段:
使用 th_define(1M) 命令创建 errdef。
通过向 bofi 驱动程序传递测试定义来创建 errdef,该驱动程序会存储这些定义,因此可以使用 th_manage(1M) 命令来访问它们。
创建工作负荷,然后使用 th_manage 命令激活和管理 errdef。
th_manage 命令是到 bofi 工具驱动程序可以识别的各种 ioctl 的用户接口。th_manage 命令在驱动程序名称和实例级别运行并且包含以下命令: get_handles 用于列出访问句柄,start 用于激活 errdef,stop 用于取消激活 errdef。
激活 errdef 将导致合格的数据访问出现故障。th_manage 实用程序支持以下命令: broadcast 用于提供 errdef 的当前状态,clear_errors 用于清除 errdef。
有关更多信息,请参见 th_define(1M) 和 th_manage(1M) 手册页。
可对测试工具进行配置,以便通过以下方法来处理警告消息:
将警告消息写入控制台
将警告消息写入控制台,然后使系统进入紧急状态
使用第二种方法有助于确定问题的根本原因。
如果将 bofi-range-check 属性值设置为 warn,当工具检测到驱动程序违反 DDI 函数的范围时,该工具将列显以下消息(或者,如果设置为 panic,则会进入紧急状态):
ddi_getX() out of range addr %x not in %x ddi_putX() out of range addr %x not in %x ddi_rep_getX() out of range addr %x not in %x ddi_rep_putX() out of range addr %x not in %x
X 为 8、16、32 或 64。
当工具已请求插入 1000 个以上额外中断时,如果驱动程序未检测到中断逾限 (jabber),则会列显以下消息:
undetected interrupt jabber - %s %d
可以使用日志记录访问类型 th_define(1M) 实用程序来创建故障注入测试脚本:
# th_define -n name -i instance -a log [-e fixup_script] |
th_define 命令使实例脱机,然后再使其恢复联机。然后,th_define 运行 fixup_script 描述的工作负荷并记录驱动程序实例进行的 I/O 访问。
将会使用可选参数的集合调用 fixup_script 两次。会在实例脱机前调用该脚本一次,然后在实例恢复联机后再次调用该脚本。
以下变量将被传递到调用的可执行文件的环境中:
实例的设备路径
驱动程序的实例编号
当实例即将脱机时设置为 1
当实例已恢复联机时设置为 1
通常,fixup_script 可确保所测试的设备处于适合脱机的状态(未配置)或处于适合注入错误的状态(例如,已配置、无错误并为工作负荷服务)。以下脚本是用于网络驱动程序的最小脚本:
#!/bin/ksh driver=xyznetdrv ifnum=$driver$DRIVER_INSTANCE if [[ $DRIVER_CONFIGURE = 1 ]]; then ifconfig $ifnum plumb ifconfig $ifnum ... ifworkload start $ifnum elif [[ $DRIVER_UNCONFIGURE = 1 ]]; then ifworkload stop $ifnum ifconfig $ifnum down ifconfig $ifnum unplumb fi exit $?
ifworkload 命令应将工作负荷作为一项后台任务来启动。故障注入发生在 fixup_script 配置所测试的驱动程序并使其联机(DRIVER_CONFIGURE 设置为 1)之后。
如果存在 -e fixup_script 选项,它必须是命令行中的最后一个选项。如果不存在 -e 选项,则使用缺省脚本。缺省脚本会反复尝试使所测试的设备脱机和联机。因此,工作负荷由驱动程序的 attach() 和 detach() 路径构成。
生成的日志将转换为一组适合运行独立的 (unassisted) 故障注入测试的可执行脚本。这些脚本创建在当前目录的子目录中,名称为 driver.test.id。脚本将在运行 fixup_script 描述的工作负荷的同时向驱动程序中注入故障,一次一个。
驱动程序测试器可对测试自动化过程生成的 errdef 进行实质性控制。请参见 th_define(1M) 手册页。
如果测试器为测试脚本选择了合适的工作负荷范围,则工具可为驱动程序各方面的强化提供良好的覆盖率。但是,要取得满覆盖率,测试器可能需要手动创建其他测试案例。请将这些案例添加至测试脚本。为确保测试可及时完成,您可能需要手动删除重复的测试案例。
以下过程介绍了自动化测试:
确定要测试的驱动程序的各个方面。
测试驱动程序中与硬件交互的所有方面:
连接和分离
在堆栈下检测和取消检测
正常数据传送
记录的调试模式
必须为每种使用模式生成单独的工作负荷脚本 (fixup_script)。
对于每种使用模式,准备可执行程序 ( fixup_script),该可执行程序可对设备进行配置和取消配置,并可创建和终止工作负荷。
使用 errdef 以及访问类型 -a log 运行 th_define(1M) 命令。
等待日志填充。
日志中包含 bofi 驱动程序的内部缓冲区的转储。此数据包含在脚本的前面。
由于创建日志可能需要几秒钟到几分钟的时间,因此可使用 th_manage broadcast 命令检查进度。
转到已创建的测试目录并运行主测试脚本。
主脚本将按顺序运行每个生成的测试脚本。每个寄存器集会生成单独的测试脚本。
存储结果,以用于分析。
成功的测试结果(如 success (corruption reported) 和 success (corruption undetected))表明所测试的驱动程序工作正常。如果工具检测到驱动程序在报告故障后无法报告服务影响或者驱动程序无法检测到访问或 DMA 句柄已被标记为有故障,则结果将报告为 failure (no service impact reported)。
输出中出现几个 test not triggered 故障并不碍事。但是,若干个此类故障将表明测试没有正常工作。当驱动程序访问的寄存器与生成测试脚本时的寄存器不同时,会出现这些故障。
同时对驱动程序的多个实例运行测试,以测试错误路径的多线程。
例如,每个 th_define 命令都会创建一个单独的目录,其中包含测试脚本和主脚本:
# th_define -n xyznetdrv -i 0 -a log -e script # th_define -n xyznetdrv -i 1 -a log -e script |
创建后,并行运行主脚本。
生成的脚本只生成仿真的故障注入,这些故障注入基于日志记录 errdef 处于活动状态期间记录的内容。定义工作负荷时,请确保记录所需结果。此外,还要分析生成的日志和故障注入规范。请验证生成的测试脚本所创建的硬件访问覆盖率是否满足需要。
LDI 是一组 DDI/DKI,内核模块可以使用它来访问系统中的其他设备。另外使用 LDI 还可以确定内核模块当前使用的设备。
本章包含以下主题:
LDI 包括以下两类接口:
内核接口。用户应用程序使用系统调用来打开、读取和写入由内核中的设备驱动程序管理的设备。内核模块可以使用 LDI 内核接口来打开、读取和写入由内核中的另一设备驱动程序管理的设备。例如,用户应用程序可使用 read(2),而内核模块可使用 ldi_read(9F) 来读取同一设备。 请参见内核接口。
用户接口。LDI 用户接口可为用户进程提供有关内核中其他设备当前使用哪些设备的信息。请参见用户接口。
Target Device(目标设备)。目标设备是内核中的设备,由设备驱动程序管理并由设备使用方访问。
Device Consumer(设备使用方)。设备使用方是打开并访问目标设备的用户进程或内核模块。设备使用方通常对目标设备执行 open、read、write 或 ioctl 之类的操作。
Kernel Device Consumer(内核设备使用方)。内核设备使用方是一种特定类型的设备使用方,它是访问目标设备的内核模块。通常情况下,内核设备使用方不是用于管理要访问的目标设备的设备驱动程序。相反,内核设备使用方通过管理目标设备的设备驱动程序间接访问目标设备。
Layered Driver(分层驱动程序)。分层驱动程序是一种特定类型的内核设备使用方。分层驱动程序是一种不直接管理任何硬件的内核驱动程序。相反,分层驱动程序通过管理目标设备的设备驱动程序间接访问这些目标设备中的一个或多个设备。例如,卷管理器和 STREAMS 多路复用器就是比较典型的分层驱动程序。
通过某些 LDI 内核接口,LDI 可以跟踪和报告内核设备使用信息。请参见分层标识符-内核设备使用方。
通过其他 LDI 内核接口,内核模块可以对目标设备执行 open、read 和 write 之类的访问操作。 另外,通过这些 LDI 内核接口,内核设备使用方可以查询有关目标设备的属性和事件信息。请参见分层驱动程序句柄-目标设备。
LDI 内核接口示例介绍了使用其中多个 LDI 接口的驱动程序示例。
通过分层标识符,LDI 可以跟踪和报告内核设备使用信息。分层标识符 (ldi_ident_t) 用于标识内核设备使用方。内核设备使用方必须先获取分层标识符,然后才能使用 LDI 打开目标设备。
分层驱动程序是唯一受支持的内核设备使用方类型。因此,分层驱动程序必须获取与设备编号、设备信息节点或分层驱动程序流关联的分层标识符。分层标识符与分层驱动程序关联。分层标识符与目标设备没有关联。
可以通过 libdevinfo(3LIB) 接口、fuser(1M) 命令或 prtconf(1M) 命令,检索通过 LDI 收集的内核设备使用信息。例如,使用 prtconf(1M) 命令可以显示分层驱动程序正在访问哪些目标设备,或者哪些分层驱动程序正在访问特定目标设备。要了解有关如何检索设备使用情况的更多信息,请参见用户接口。
下面介绍了 LDI 分层标识符接口:
分层标识符。属于不透明类型。
分配和检索与 dev_t 设备编号关联的分层标识符。
分配和检索与 dev_info_t 设备信息节点关联的分层标识符。
分配和检索与流关联的分层标识符。
释放使用 ldi_ident_from_dev(9F)、ldi_ident_from_dip(9F) 或 ldi_ident_from_stream(9F) 分配的分层标识符。
内核设备使用方必须使用分层驱动程序句柄 (ldi_handle_t) 来通过 LDI 接口访问目标设备。ldi_handle_t 类型仅对 LDI 接口有效。当 LDI 成功打开某个设备时,将分配并返回此句柄。然后,内核设备使用方可使用此句柄通过 LDI 接口访问目标设备。LDI 在关闭设备时会取消分配该句柄。有关示例,请参见LDI 内核接口示例。
本节讨论内核设备使用方如何访问目标设备并检索不同类型的信息。要了解内核设备使用方如何打开和关闭目标设备,请参见打开和关闭目标设备。要了解内核设备使用方如何对目标设备执行 read、write、strategy 和 ioctl 之类的操作,请参见访问目标设备。检索目标设备信息介绍了用于检索目标设备信息(如设备打开类型和设备次要名称)的接口。检索目标设备属性值介绍了用于检索目标设备属性的值和地址的接口。要了解内核设备使用方如何接收来自目标设备的事件通知,请参见接收异步设备事件通知。
本节介绍用于打开和关闭目标设备的 LDI 内核接口。打开接口采用指向分层驱动程序句柄的指针。打开接口会尝试打开由设备编号、设备 ID 或路径名指定的目标设备。如果打开操作成功,则打开接口将分配并返回可用于访问目标设备的分层驱动程序句柄。关闭接口用于关闭与指定分层驱动程序句柄关联的目标设备,然后释放该分层驱动程序句柄。
用于访问目标设备的分层驱动程序句柄。一种成功打开设备时返回的不透明数据结构。
打开由 dev_t 设备编号参数指定的设备。
打开由 ddi_devid_t 设备 ID 参数指定的设备。另外,还必须指定要打开的次要节点名称。
根据路径名打开设备。路径名是内核地址空间中以 NULL 结尾的字符串。路径名必须是以正斜杠字符 (/) 开头的绝对路径。
关闭使用 ldi_open_by_dev(9F)、ldi_open_by_devid(9F) 或 ldi_open_by_name (9F) 打开的设备。在 ldi_close(9F) 返回之后,已关闭的设备的分层驱动程序句柄不再有效。
本节介绍用于访问目标设备的 LDI 内核接口。通过这些接口,内核设备使用方可以对由分层驱动程序句柄指定的目标设备执行操作。内核设备使用方可以对目标设备执行 read、write、strategy 和 ioctl 之类的操作。
用于访问目标设备的分层驱动程序句柄。属于不透明数据结构。
将读取请求传递到目标设备的设备入口点。块设备、字符设备和 STREAMS 设备支持此操作。
将异步读取请求传递到目标设备的设备入口点。块设备和字符设备支持此操作。
将写入请求传递到目标设备的设备入口点。块设备、字符设备和 STREAMS 设备支持此操作。
将异步写入请求传递到目标设备的设备入口点。块设备和字符设备支持此操作。
将策略请求传递到目标设备的设备入口点。块设备和字符设备支持此操作。
将转储请求传递到目标设备的设备入口点。块设备和字符设备支持此操作。
将轮询请求传递到目标设备的设备入口点。块设备、字符设备和 STREAMS 设备支持此操作。
将 ioctl 请求传递到目标设备的设备入口点。块设备、字符设备和 STREAMS 设备支持此操作。LDI 支持 STREAMS 链接和 STREAMS ioctl 命令。请参见 ldi_ioctl(9F) 手册页的 "STREAM IOCTLS" 一节。另请参见 streamio(7I) 手册页中的 ioctl 命令。
将 devmap 请求传递到目标设备的设备入口点。块设备和字符设备支持此操作。
从流中获取消息块。
将消息块放在流中。
本节介绍内核设备使用方可用于检索有关指定目标设备的设备信息的 LDI 接口。目标设备由分层驱动程序句柄指定。内核设备使用方可以接收设备编号、设备打开类型、设备 ID、设备次要名称和设备大小之类的信息。
获取由分层驱动程序句柄指定的目标设备的 dev_t 设备编号。
获取用于打开由分层驱动程序句柄指定的目标设备的打开标志。此标志指示目标设备是字符设备还是块设备。
获取由分层驱动程序句柄指定的目标设备的 ddi_devid_t 设备 ID。使用完设备 ID 后,应使用 ddi_devid_free(9F) 释放 ddi_devid_t。
检索包含为目标设备打开的次要节点的名称的缓冲区。使用完次要节点名称后,应使用 kmem_free(9F) 释放该缓冲区。
检索由分层驱动程序句柄指定的目标设备的分区大小。
本节介绍内核设备使用方可用于检索有关指定目标设备的属性信息的 LDI 接口。目标设备由分层驱动程序句柄指定。内核设备使用方可以接收属性的值和地址,以及确定某属性是否存在。
如果由分层驱动程序句柄指定的目标设备的属性存在,则返回 1。如果指定目标设备的属性不存在,则返回 0。
搜索与由分层驱动程序句柄指定的目标设备关联的 int 整数属性。如果找到整数属性,则返回属性值。
搜索与由分层驱动程序句柄指定的目标设备关联的 int64_t 整数属性。如果找到整数属性,则返回属性值。
检索由分层驱动程序句柄指定的目标设备的 int 整数数组属性值的地址。
检索由分层驱动程序句柄指定的目标设备的 int64_t 整数数组属性值的地址。
检索由分层驱动程序句柄指定的目标设备的以 null 结尾的字符串属性值的地址。
检索字符串数组的地址。字符串数组是一个指针数组,指向由分层驱动程序句柄指定的目标设备的以 null 结尾的字符串属性值。
检索字节数组的地址。字节数组是由分层驱动程序句柄指定的目标设备的属性值。
通过 LDI,内核设备使用方可以注册事件通知以及接收来自目标设备的事件通知。内核设备使用方可以注册发生事件时将会调用的事件处理程序。内核设备使用方必须先打开设备并接收分层驱动程序句柄,然后才能通过 LDI 事件通知接口注册事件通知。
通过 LDI 事件通知接口,内核设备使用方可以指定事件名称以及检索关联的内核事件 cookie。然后,内核设备使用方可以将分层驱动程序句柄 (ldi_handle_t)、cookie (ddi_eventcookie_t) 及事件处理程序传递到 ldi_add_event_handler(9F) 以注册事件通知。成功完成注册后,内核设备使用方会收到一个唯一的 LDI 事件处理程序标识符 (ldi_callback_id_t)。LDI 事件处理程序标识符属于不透明类型,只能用于 LDI 事件通知接口。
LDI 提供了一个框架,以用于注册其他设备生成的事件。LDI 本身并不定义任何事件类型,也不提供用于生成事件的接口。
下面介绍了 LDI 异步事件通知接口:
事件处理程序标识符。属于不透明类型。
检索由分层驱动程序句柄指定的目标设备的事件服务 cookie。
添加由 ldi_callback_id_t 注册标识符指定的回调处理程序。发生由 ddi_eventcookie_t cookie 指定的事件时,将会调用该回调处理程序。
删除由 ldi_callback_id_t 注册标识符指定的回调处理程序。
本节介绍了一个使用本章前面几节中讨论的一些 LDI 调用的内核设备使用方示例。本节讨论此示例模块的下列几个方面:
此内核设备使用方示例名为 lyr。lyr 模块是一个分层驱动程序,它使用 LDI 调用向目标设备发送数据。在其 open(9E) 入口点中,lyr 驱动程序将打开由 lyr.conf 配置文件中的 lyr_targ 属性指定的设备。在其 write(9E) 入口点中,lyr 驱动程序将其所有传入数据写入由 lyr_targ 属性指定的设备。
在下面所示的配置文件中,lyr 驱动程序向其中写入数据的目标设备为控制台。
# # Copyright 2004 Sun Microsystems, Inc. All rights reserved. # Use is subject to license terms. # #pragma ident "%Z%%M% %I% %E% SMI" name="lyr" parent="pseudo" instance=1; lyr_targ="/dev/console";
在下面所示的驱动程序源文件中,lyr_state_t 结构保存 lyr 驱动程序的软状态。该软状态包括 lyr_targ 设备的分层驱动程序句柄 (lh) 以及 lyr 设备的分层标识符 (li)。有关软状态的更多信息,请参见检索驱动程序软状态信息。
在 lyr_open() 入口点中,ddi_prop_lookup_string(9F) 将从 lyr_targ 属性中检索要打开的 lyr 设备的目标设备的名称。ldi_ident_from_dev(9F) 函数用于获取 lyr 设备的 LDI 分层标识符。ldi_open_by_name(9F) 函数用于打开 lyr_targ 设备并获取 lyr_targ 设备的分层驱动程序句柄。
请注意,如果 lyr_open() 中发生任何故障,ldi_close(9F)、ldi_ident_release(9F) 和 ddi_prop_free(9F) 调用将会撤消所执行的所有操作。ldi_close(9F) 函数用于关闭 lyr_targ 设备。ldi_ident_release(9F) 函数用于释放 lyr 分层标识符。ddi_prop_free(9F) 函数用于释放检索 lyr_targ 设备名称时分配的资源。如果未发生故障,则会在 lyr_close() 入口点中调用 ldi_close(9F) 和 ldi_ident_release(9F) 函数。
在驱动程序模块的最后一行中,调用了 ldi_write(9F) 函数。ldi_write(9F) 函数先获取在 lyr_write() 入口点中写入 lyr 设备的数据,然后将该数据写入 lyr_targ 设备。ldi_write(9F) 函数使用 lyr_targ 设备的分层驱动程序句柄将数据写入 lyr_targ 设备。
#include <sys/types.h> #include <sys/file.h> #include <sys/errno.h> #include <sys/open.h> #include <sys/cred.h> #include <sys/cmn_err.h> #include <sys/modctl.h> #include <sys/conf.h> #include <sys/stat.h> #include <sys/ddi.h> #include <sys/sunddi.h> #include <sys/sunldi.h> typedef struct lyr_state { ldi_handle_t lh; ldi_ident_t li; dev_info_t *dip; minor_t minor; int flags; kmutex_t lock; } lyr_state_t; #define LYR_OPENED 0x1 /* lh is valid */ #define LYR_IDENTED 0x2 /* li is valid */ static int lyr_info(dev_info_t *, ddi_info_cmd_t, void *, void **); static int lyr_attach(dev_info_t *, ddi_attach_cmd_t); static int lyr_detach(dev_info_t *, ddi_detach_cmd_t); static int lyr_open(dev_t *, int, int, cred_t *); static int lyr_close(dev_t, int, int, cred_t *); static int lyr_write(dev_t, struct uio *, cred_t *); static void *lyr_statep; static struct cb_ops lyr_cb_ops = { lyr_open, /* open */ lyr_close, /* close */ nodev, /* strategy */ nodev, /* print */ nodev, /* dump */ nodev, /* read */ lyr_write, /* write */ nodev, /* ioctl */ nodev, /* devmap */ nodev, /* mmap */ nodev, /* segmap */ nochpoll, /* poll */ ddi_prop_op, /* prop_op */ NULL, /* streamtab */ D_NEW | D_MP, /* cb_flag */ CB_REV, /* cb_rev */ nodev, /* aread */ nodev /* awrite */ }; static struct dev_ops lyr_dev_ops = { DEVO_REV, /* devo_rev, */ 0, /* refcnt */ lyr_info, /* getinfo */ nulldev, /* identify */ nulldev, /* probe */ lyr_attach, /* attach */ lyr_detach, /* detach */ nodev, /* reset */ &lyr_cb_ops, /* cb_ops */ NULL, /* bus_ops */ NULL /* power */ }; static struct modldrv modldrv = { &mod_driverops, "LDI example driver", &lyr_dev_ops }; static struct modlinkage modlinkage = { MODREV_1, &modldrv, NULL }; int _init(void) { int rv; if ((rv = ddi_soft_state_init(&lyr_statep, sizeof (lyr_state_t), 0)) != 0) { cmn_err(CE_WARN, "lyr _init: soft state init failed\n"); return (rv); } if ((rv = mod_install(&modlinkage)) != 0) { cmn_err(CE_WARN, "lyr _init: mod_install failed\n"); goto FAIL; } return (rv); /*NOTEREACHED*/ FAIL: ddi_soft_state_fini(&lyr_statep); return (rv); } int _info(struct modinfo *modinfop) { return (mod_info(&modlinkage, modinfop)); } int _fini(void) { int rv; if ((rv = mod_remove(&modlinkage)) != 0) { return(rv); } ddi_soft_state_fini(&lyr_statep); return (rv); } /* * 1:1 mapping between minor number and instance */ static int lyr_info(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result) { int inst; minor_t minor; lyr_state_t *statep; char *myname = "lyr_info"; minor = getminor((dev_t)arg); inst = minor; switch (infocmd) { case DDI_INFO_DEVT2DEVINFO: statep = ddi_get_soft_state(lyr_statep, inst); if (statep == NULL) { cmn_err(CE_WARN, "%s: get soft state " "failed on inst %d\n", myname, inst); return (DDI_FAILURE); } *result = (void *)statep->dip; break; case DDI_INFO_DEVT2INSTANCE: *result = (void *)inst; break; default: break; } return (DDI_SUCCESS); } static int lyr_attach(dev_info_t *dip, ddi_attach_cmd_t cmd) { int inst; lyr_state_t *statep; char *myname = "lyr_attach"; switch (cmd) { case DDI_ATTACH: inst = ddi_get_instance(dip); if (ddi_soft_state_zalloc(lyr_statep, inst) != DDI_SUCCESS) { cmn_err(CE_WARN, "%s: ddi_soft_state_zallac failed " "on inst %d\n", myname, inst); goto FAIL; } statep = (lyr_state_t *)ddi_get_soft_state(lyr_statep, inst); if (statep == NULL) { cmn_err(CE_WARN, "%s: ddi_get_soft_state failed on " "inst %d\n", myname, inst); goto FAIL; } statep->dip = dip; statep->minor = inst; if (ddi_create_minor_node(dip, "node", S_IFCHR, statep->minor, DDI_PSEUDO, 0) != DDI_SUCCESS) { cmn_err(CE_WARN, "%s: ddi_create_minor_node failed on " "inst %d\n", myname, inst); goto FAIL; } mutex_init(&statep->lock, NULL, MUTEX_DRIVER, NULL); return (DDI_SUCCESS); case DDI_RESUME: case DDI_PM_RESUME: default: break; } return (DDI_FAILURE); /*NOTREACHED*/ FAIL: ddi_soft_state_free(lyr_statep, inst); ddi_remove_minor_node(dip, NULL); return (DDI_FAILURE); } static int lyr_detach(dev_info_t *dip, ddi_detach_cmd_t cmd) { int inst; lyr_state_t *statep; char *myname = "lyr_detach"; inst = ddi_get_instance(dip); statep = ddi_get_soft_state(lyr_statep, inst); if (statep == NULL) { cmn_err(CE_WARN, "%s: get soft state failed on " "inst %d\n", myname, inst); return (DDI_FAILURE); } if (statep->dip != dip) { cmn_err(CE_WARN, "%s: soft state does not match devinfo " "on inst %d\n", myname, inst); return (DDI_FAILURE); } switch (cmd) { case DDI_DETACH: mutex_destroy(&statep->lock); ddi_soft_state_free(lyr_statep, inst); ddi_remove_minor_node(dip, NULL); return (DDI_SUCCESS); case DDI_SUSPEND: case DDI_PM_SUSPEND: default: break; } return (DDI_FAILURE); } /* * on this driver's open, we open the target specified by a property and store * the layered handle and ident in our soft state. a good target would be * "/dev/console" or more interestingly, a pseudo terminal as specified by the * tty command */ /*ARGSUSED*/ static int lyr_open(dev_t *devtp, int oflag, int otyp, cred_t *credp) { int rv, inst = getminor(*devtp); lyr_state_t *statep; char *myname = "lyr_open"; dev_info_t *dip; char *lyr_targ = NULL; statep = (lyr_state_t *)ddi_get_soft_state(lyr_statep, inst); if (statep == NULL) { cmn_err(CE_WARN, "%s: ddi_get_soft_state failed on " "inst %d\n", myname, inst); return (EIO); } dip = statep->dip; /* * our target device to open should be specified by the "lyr_targ" * string property, which should be set in this driver's .conf file */ if (ddi_prop_lookup_string(DDI_DEV_T_ANY, dip, DDI_PROP_NOTPROM, "lyr_targ", &lyr_targ) != DDI_PROP_SUCCESS) { cmn_err(CE_WARN, "%s: ddi_prop_lookup_string failed on " "inst %d\n", myname, inst); return (EIO); } /* * since we only have one pair of lh's and li's available, we don't * allow multiple on the same instance */ mutex_enter(&statep->lock); if (statep->flags & (LYR_OPENED | LYR_IDENTED)) { cmn_err(CE_WARN, "%s: multiple layered opens or idents " "from inst %d not allowed\n", myname, inst); mutex_exit(&statep->lock); ddi_prop_free(lyr_targ); return (EIO); } rv = ldi_ident_from_dev(*devtp, &statep->li); if (rv != 0) { cmn_err(CE_WARN, "%s: ldi_ident_from_dev failed on inst %d\n", myname, inst); goto FAIL; } statep->flags |= LYR_IDENTED; rv = ldi_open_by_name(lyr_targ, FREAD | FWRITE, credp, &statep->lh, statep->li); if (rv != 0) { cmn_err(CE_WARN, "%s: ldi_open_by_name failed on inst %d\n", myname, inst); goto FAIL; } statep->flags |= LYR_OPENED; cmn_err(CE_CONT, "\n%s: opened target '%s' successfully on inst %d\n", myname, lyr_targ, inst); rv = 0; FAIL: /* cleanup on error */ if (rv != 0) { if (statep->flags & LYR_OPENED) (void)ldi_close(statep->lh, FREAD | FWRITE, credp); if (statep->flags & LYR_IDENTED) ldi_ident_release(statep->li); statep->flags &= ~(LYR_OPENED | LYR_IDENTED); } mutex_exit(&statep->lock); if (lyr_targ != NULL) ddi_prop_free(lyr_targ); return (rv); } /* * on this driver's close, we close the target indicated by the lh member * in our soft state and release the ident, li as well. in fact, we MUST do * both of these at all times even if close yields an error because the * device framework effectively closes the device, releasing all data * associated with it and simply returning whatever value the target's * close(9E) returned. therefore, we must as well. */ /*ARGSUSED*/ static int lyr_close(dev_t devt, int oflag, int otyp, cred_t *credp) { int rv, inst = getminor(devt); lyr_state_t *statep; char *myname = "lyr_close"; statep = (lyr_state_t *)ddi_get_soft_state(lyr_statep, inst); if (statep == NULL) { cmn_err(CE_WARN, "%s: ddi_get_soft_state failed on " "inst %d\n", myname, inst); return (EIO); } mutex_enter(&statep->lock); rv = ldi_close(statep->lh, FREAD | FWRITE, credp); if (rv != 0) { cmn_err(CE_WARN, "%s: ldi_close failed on inst %d, but will ", "continue to release ident\n", myname, inst); } ldi_ident_release(statep->li); if (rv == 0) { cmn_err(CE_CONT, "\n%s: closed target successfully on " "inst %d\n", myname, inst); } statep->flags &= ~(LYR_OPENED | LYR_IDENTED); mutex_exit(&statep->lock); return (rv); } /* * echo the data we receive to the target */ /*ARGSUSED*/ static int lyr_write(dev_t devt, struct uio *uiop, cred_t *credp) { int rv, inst = getminor(devt); lyr_state_t *statep; char *myname = "lyr_write"; statep = (lyr_state_t *)ddi_get_soft_state(lyr_statep, inst); if (statep == NULL) { cmn_err(CE_WARN, "%s: ddi_get_soft_state failed on " "inst %d\n", myname, inst); return (EIO); } return (ldi_write(statep->lh, uiop, credp)); }
编译驱动程序。
使用 -D_KERNEL 选项指示这是一个内核模块。
如果要针对 SPARC 体系结构进行编译,请使用 -xarch=v9 选项:
% cc -c -D_KERNEL -xarch=v9 lyr.c |
如果要针对 32 位 x86 体系结构进行编译,请使用以下命令:
% cc -c -D_KERNEL lyr.c |
链接驱动程序。
% ld -r -o lyr lyr.o |
安装配置文件。
以 root 用户身份,将配置文件复制到计算机的内核驱动程序区域:
# cp lyr.conf /usr/kernel/drv |
安装驱动程序二进制文件。
以 root 用户身份,将驱动程序二进制文件复制到 SPARC 体系结构的 sparcv9 驱动程序区域:
# cp lyr /usr/kernel/drv/sparcv9 |
以 root用户身份,将驱动程序二进制文件复制到 32 位 x86 体系结构的 drv 驱动程序区域:
# cp lyr /usr/kernel/drv |
以 root 用户身份,使用 add_drv(1M) 命令装入驱动程序。
# add_drv lyr |
列出伪设备,确认目前是否存在 lyr 设备:
# ls /devices/pseudo | grep lyr lyr@1 lyr@1:node |
要测试 lyr 驱动程序,请向 lyr 设备写入一条消息,并验证该消息是否显示在 lyr_targ 设备上。
在本示例中,lyr_targ 设备是安装了 lyr 设备的系统的控制台。
如果要查看的显示屏幕也是安装了 lyr 设备的系统的控制台设备的显示屏幕,请注意,向控制台写入将会破坏显示屏幕上的信息。控制台消息将显示在窗口系统范围以外。测试 lyr 驱动程序之后,需要重画或刷新显示器。
如果要查看的显示屏幕不是安装了 lyr 设备的系统的控制台设备的显示屏幕,请登录或以其他方式查看目标控制台设备的显示屏幕上的信息。
以下命令将一条很短的消息写入 lyr 设备:
# echo "\n\n\t===> Hello World!! <===\n" > /devices/pseudo/lyr@1:node |
目标控制台上将会显示以下消息:
console login: ===> Hello World!! <=== lyr: lyr_open: opened target '/dev/console' successfully on inst 1 lyr: lyr_close: closed target successfully on inst 1
执行 lyr_open() 和 lyr_close() 时所显示的信息来自在 lyr_open() 和 lyr_close() 入口点中执行的 cmn_err(9F) 调用。
以下命令将一条较长的消息写入 lyr 设备:
# cat lyr.conf > /devices/pseudo/lyr@1:node |
目标控制台上将会显示以下消息:
lyr: lyr_open: opened target '/dev/console' successfully on inst 1 # # Copyright 2004 Sun Microsystems, Inc. All rights reserved. # Use is subject to license terms. # #pragma ident "%Z%%M% %I% %E% SMI" name="lyr" parent="pseudo" instance=1; lyr_targ="/dev/console"; lyr: lyr_close: closed target successfully on inst 1
要更改目标设备,请编辑 /usr/kernel/drv/lyr.conf,并将 lyr_targ 属性的值更改为指向其他目标设备的路径。例如,该目标设备可以是在本地终端执行 tty 命令后的输出结果。例如,此类设备路径可以是 /dev/pts/4。
在将驱动程序更新为使用新目标设备之前,应确保 lyr 设备未被使用。
# modinfo -c | grep lyr 174 3 lyr UNLOADED/UNINSTALLED |
使用 update_drv(1M) 命令重新装入 lyr.conf 配置文件:
# update_drv lyr |
再次向 lyr 设备写入一条消息,并验证该消息是否显示在新的 lyr_targ 设备上。
LDI 中包括用户级库和命令接口,用于报告设备分层和使用信息。设备信息库接口介绍了用于报告设备分层信息的 libdevinfo(3LIB) 接口。列显系统配置命令接口介绍了用于报告内核设备使用信息的 prtconf(1M) 接口。设备用户命令接口介绍了用于报告设备使用方信息的 fuser(1M) 接口。
LDI 中包括用于报告设备分层信息快照的 libdevinfo(3LIB) 接口。如果系统中的一个设备是同一系统中另一个设备的使用方,则会发生设备分层。仅当使用方和目标都绑定到快照中包含的设备节点时,才会报告设备分层信息。
libdevinfo(3LIB) 接口以有向图的形式报告设备分层信息。lnode 是一个抽象术语,在图中表示顶点,并被绑定到设备节点。可以使用 libdevinfo(3LIB) 接口来访问 lnode 的属性,如节点的名称和设备编号。
图中的边表示链接。链接既有表示设备使用方的源 lnode,也有表示目标设备的目标 lnode。
下面介绍了 libdevinfo(3LIB) 设备分层信息接口:
通过它来捕获设备分层信息的快照标志。
两个端点之间的有向链接。每个端点都是一个 di_lnode_t。属于不透明结构。
链接的端点。属于不透明结构。di_lnode_t 绑定到 di_node_t。
表示设备节点。属于不透明结构。di_node_t 不一定绑定到 di_lnode_t。
遍历快照中的所有链接。
遍历快照中的所有 lnode。
获取下一个其中指定的 di_node_t 节点是源节点或目标节点的链接的句柄。
获取下一个其中指定的 di_lnode_t lnode 是源 lnode 或目标 lnode 的链接的句柄。
获取与 di_link_t 链接的指定端点对应的 lnode。
获取链接的规范类型。规范类型指示如何访问目标设备。目标设备由目标 lnode 表示。
获取下一个与指定的 di_node_t 设备节点关联的指定 di_lnode_t lnode 的句柄。
获取与指定 lnode 关联的名称。
获取与指定 lnode 关联的设备节点的句柄。
获取与指定 lnode 关联的设备节点的设备编号。
LDI 返回的设备分层信息可能十分复杂。因此,LDI 提供了一些接口来协助遍历设备树和设备使用情况图。通过这些接口,设备树快照的使用方可以将自定义数据指针与快照中的不同结构关联。例如,应用程序遍历 lnode 时,它可以更新与每个 lnode 关联的自定义指针,以标记已经识别的 lnode。
下面介绍了 libdevinfo(3LIB) 节点和链接标记接口:
将指定的数据与指定的 lnode 关联。通过此关联,可以遍历快照中的 lnode。
检索指向通过调用 di_lnode_private_set(3DEVINFO) 而与 lnode 关联的数据的指针。
将指定的数据与指定的链接关联。通过此关联,可以遍历快照中的链接。
检索指向通过调用 di_link_private_set(3DEVINFO) 而与链接关联的数据的指针。
prtconf(1M) 命令已得到增强,可以显示内核设备使用信息。缺省的 prtconf( 1M) 输出没有变化。如果在 prtconf(1M) 命令中指定详细选项 (-v),则会显示设备使用信息。如果在 prtconf(1M) 命令行上指定特定设备的路径,则会显示有关该设备的使用信息。
显示设备次要节点和设备使用信息。显示内核使用方和每个内核使用方当前打开的次要节点。
显示由 path 指定的设备的设备使用信息。
显示由 path 指定的设备的设备使用信息,以及作为 path 的祖先的所有设备节点。
显示由 path 指定的设备的设备使用信息,以及作为 path 的子节点的所有设备节点。
如果需要有关特定设备的使用信息,path 参数的值可以是任何有效的设备路径。
% prtconf /dev/cfg/c0 SUNW,isptwo, instance #0 |
要显示有关特定设备的使用信息以及作为其祖先的所有设备节点,请在 prtconf(1M) 命令中指定 -a 标志。祖先包括直到设备树的根的所有节点。如果在 prtconf(1M) 命令中指定 -a 标志,则还必须指定设备的 path 名称。
% prtconf -a /dev/cfg/c0 SUNW,Sun-Fire ssm, instance #0 pci, instance #0 pci, instance #0 SUNW,isptwo, instance #0 |
要显示有关特定设备的使用信息以及作为其子节点的所有设备节点,请在 prtconf(1M) 命令中指定 -c 标志。如果在 prtconf(1M) 命令中指定 -c 标志,则还必须指定设备的 path 名称。
% prtconf -c /dev/cfg/c0 SUNW,isptwo, instance #0 sd (driver not attached) st (driver not attached) sd, instance #1 sd, instance #0 sd, instance #6 st, instance #1 (driver not attached) st, instance #0 (driver not attached) st, instance #2 (driver not attached) st, instance #3 (driver not attached) st, instance #4 (driver not attached) st, instance #5 (driver not attached) st, instance #6 (driver not attached) ses, instance #0 (driver not attached) ... |
要显示有关特定设备的设备分层和设备次要节点信息,请在 prtconf(1M) 命令中指定 -v 标志。
% prtconf -v /dev/kbd conskbd, instance #0 System properties: ... Device Layered Over: mod=kb8042 dev=(101,0) dev_path=/isa/i8042@1,60/keyboard@0 Device Minor Nodes: dev=(103,0) dev_path=/pseudo/conskbd@0:kbd spectype=chr type=minor dev_link=/dev/kbd dev=(103,1) dev_path=/pseudo/conskbd@0:conskbd spectype=chr type=internal Device Minor Layered Under: mod=wc accesstype=chr dev_path=/pseudo/wc@0 |
本示例中,/dev/kbd 设备所在层位于硬件键盘设备 (/isa/i8042@1,60/keyboard@0) 之上。另外,本示例中,/dev/kbd 设备具有两个设备次要节点。第一个次要节点具有可用于访问该节点的 /dev 链接。第二个次要节点是一个无法通过文件系统访问的内部节点。wc 驱动程序(即工作站控制台)已经打开了第二个次要节点。请将本示例的输出与示例 14–12 的输出进行比较。
本示例说明哪些设备正在使用当前检测到的网络设备。
% prtconf -v /dev/iprb0 pci1028,145, instance #0 Hardware properties: ... Interrupt Specifications: ... Device Minor Nodes: dev=(27,1) dev_path=/pci@0,0/pci8086,244e@1e/pci1028,145@c:iprb0 spectype=chr type=minor alias=/dev/iprb0 dev=(27,4098) dev_path=<clone> Device Minor Layered Under: mod=udp6 accesstype=chr dev_path=/pseudo/udp6@0 dev=(27,4097) dev_path=<clone> Device Minor Layered Under: mod=udp accesstype=chr dev_path=/pseudo/udp@0 dev=(27,4096) dev_path=<clone> Device Minor Layered Under: mod=udp accesstype=chr dev_path=/pseudo/udp@0 |
本示例中,在采用 udp 和 udp6 的情况下链接了 iprb0 设备。请注意,此处并未显示指向采用 udp 和 udp6 的次要节点的任何路径。本示例中未显示任何路径是因为次要节点是通过对 iprb 驱动程序执行 clone 打开操作创建的,因此不存在可以访问这些节点的文件系统路径。请将本示例的输出与示例 14–11 的输出进行比较。
fuser(1M) 命令已得到增强,可以显示设备使用信息。仅当 path 表示设备次要节点时,fuser(1M) 命令才会显示设备使用信息。仅当指定了表示设备次要节点的 path 时,在 fuser(1M) 命令中使用 -d 标志才会有效。
显示有关应用程序设备使用方和内核设备使用方的信息(如果 path 表示设备次要节点)。
显示与 path 表示的设备次要节点关联的基础设备的所有用户。
报告内核设备使用方时采用以下四种格式之一。内核设备使用方始终用方括号 ([]) 括起来。
[kernel_module_name] [kernel_module_name,dev_path=path] [kernel_module_name,dev=(major,minor)] [kernel_module_name,dev=(major,minor),dev_path=path] |
如果 fuser(1M) 命令显示的是文件或设备用户,则输出由 stdout 中的进程 ID 后跟 stderr 中的字符组成。stderr 中的字符描述如何使用文件或设备。stderr 中会显示所有内核使用方信息。而 stdout 中不会显示任何内核使用方信息。
如果不使用 -d 标志,则 fuser(1M) 命令仅报告由 path 指定的设备次要节点的使用方。如果使用 -d 标志,则 fuser(1M) 命令会报告由 path 指定的次要节点的基础设备节点的使用方。以下示例说明了这两种情况下报告输出的差别。
大多数网络设备在打开时都会克隆其次要节点。如果请求克隆次要节点的设备使用信息,则该使用信息可能会表明没有任何进程在使用该设备。而如果请求基础设备节点的设备使用信息,则该使用信息可能会表明某个进程正在使用该设备。在本示例中,如果仅将设备 path 传递到 fuser(1M) 命令,则不会报告任何设备使用方。如果使用 -d 标志,则输出表明正在采用 udp 和 udp6 \uc2\u26469 来访问该设备。
% fuser /dev/iprb0 /dev/iprb0: % fuser -d /dev/iprb0 /dev/iprb0: [udp,dev_path=/pseudo/udp@0] [udp6,dev_path=/pseudo/udp6@0] |
请将本示例的输出与示例 14–10 的输出进行比较。
在本示例中,某个内核使用方正在访问 /dev/kbd。正在访问 /dev/kbd 设备的内核使用方是工作站控制台驱动程序。
% fuser -d /dev/kbd /dev/kbd: [genunix] [wc,dev_path=/pseudo/wc@0] |
请将本示例的输出与示例 14–9 的输出进行比较。