编写设备驱动程序

用于 Solaris 设备驱动程序的防御性编程方法

本节针对设备驱动程序提供了一些方法,可用于避免系统出现紧急情况并挂起、浪费系统资源以及扩大数据损坏范围。如果除了 I/O 故障服务框架之外,驱动程序还将这些防御性编程做法用于错误处理和诊断,则认为该驱动程序已进行强化。

所有 Solaris 驱动程序都应遵循以下编码做法:

使用单独的设备驱动程序实例

Solaris 内核允许一个驱动程序具有多个实例。每个实例都有自己的数据空间,但与其他实例共享文本和某些全局数据。设备是基于每个实例进行管理的。除非将驱动程序设计用于在内部处理任何故障转移,否则驱动程序应针对每个硬件使用单独的实例。一个插槽可能具有一个驱动程序的多个实例,例如,具有多功能卡。

独占使用 DDI 访问句柄

驱动程序进行的所有 PIO 访问都必须使用以下系列例程中的 Solaris DDI 访问函数:

驱动程序不应根据 ddi_regs_map_setup(9F) 返回的地址直接访问已映射的寄存器。请避免使用 ddi_peek(9F)ddi_poke(9F) 例程,因为这些例程不使用访问句柄。

由于 DDI 访问提供了对数据读入内核的方式进行控制的机会,因此 DDI 访问机制很重要。

检测已损坏的数据

以下各节介绍可能发生数据损坏的位置以及如何检测损坏。

设备管理和控制数据的损坏

驱动程序应假定,从设备获取的任何数据(无论通过 PIO 还是 DMA)都可能已被损坏。需要特别指出的是,对于基于设备数据的指针、内存偏移以及数组索引要格外小心。此类值可以是恶性的,因为取消引用这些值时会导致内核出现紧急情况。在使用之前,应针对所有此类值执行范围和对齐检查(如果需要)。

即使是非恶性指针,仍然可能具有误导性。例如,指针可能指向某个对象的有效但错误的实例。驱动程序应尽量交叉检查指针以及该指针所指向的对象,或者对通过该指针获得的数据进行验证。

其他类型的数据也可能具有误导性,如包长度、状态字或通道 ID。应尽可能地对这些数据类型进行检查。可对包长度进行范围检查,以确保该长度既不为负,也不比包含缓冲区大。可针对“不可能”的位对状态字进行检查。可将通道 ID 与有效 ID 的列表进行匹配。

其中,一个值标识一个流,驱动程序必须确保该流仍然存在。处理 STREAMS 的异步性质意味着可在设备中断仍未完成时中断流。

驱动程序不应从设备中重新读取数据。数据应只读取一次,然后进行验证并以驱动程序的本地状态进行存储。此方法可避免数据在初始读取时正确但以后重新读取时错误的风险。

驱动程序还应确保已限制所有循环。例如,返回连续 BUSY 状态的设备不能锁定整个系统。

已接收数据的损坏

设备错误可能导致将损坏的数据放置在接收缓冲区中。此类损坏与设备域之外(例如,网络中)发生的损坏几乎没有区别。通常可利用现有软件处理此类问题。例如,在协议栈的传输层进行完整性检查,或者在使用该设备的应用程序内进行完整性检查。

如果不打算在较高层对已接收的数据进行完整性检查,则可在驱动程序自身内对数据进行完整性检查。对已接收数据中的损坏进行检测的方法通常特定于设备。例如,校验和与 CRC 即是可执行的检查种类。

DMA 隔离

有缺陷的设备可能通过总线启动错误的 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

自适应策略

驱动程序可以使用有故障的硬件继续提供服务。驱动程序可以尝试使用用于访问设备的备用策略来解决已确定的问题。假定损坏的硬件不可预测并且已知与其他设计复杂性关联的风险,则自适应策略并不总是明智的选择。这些策略最多应限制为定期中断轮询和重试尝试。定期重试设备可使驱动程序了解设备恢复的时间。强制驱动程序禁用中断后,定期轮询可以控制中断机制。

理论上,系统始终有一个备用设备来提供重要的系统服务。内核或用户空间中的服务多路复用程序可在设备出现时提供维护系统服务的最佳方法。此类做法将不在本节中进行介绍。