本章介绍如何编写强健的驱动程序。根据本章中所讨论的原则编写的驱动程序更易于进行调试。建议的做法还可在出现硬件和软件故障时为系统提供保护。
本章介绍有关以下主题的信息:
由于以下原因,驱动程序代码比用户程序更难调试:
驱动程序直接与硬件进行交互
驱动程序在运行时不能受到操作系统的保护,而用户进程可以
请确保您的驱动程序可以支持调试。此支持便于进行维护工作和未来的开发工作。
每个函数、数据元素和驱动程序预处理程序定义的名称必须对每个驱动程序都唯一。
驱动程序模块将链接到内核。对特定驱动程序唯一的每个符号名称不得与其他内核符号冲突。为避免这种冲突,特定驱动程序的每个函数和数据元素的名称必须带有该驱动程序共有的前缀。该前缀必须足以让每个驱动程序符号的名称保持唯一。通常,该前缀是驱动程序的名称,或者是驱动程序名称的缩写。例如,xx_open() 是驱动程序 xx 的 open(9E) 例程的名称。
在构建驱动程序时,驱动程序一定包含许多系统头文件。这些头文件中的全局可见名称无法预测。为避免与这些名称产生冲突,必须使用一个标识前缀为每个驱动程序预处理程序定义指定唯一的名称。
在进行错误诊断时,还可以借助唯一的驱动程序符号前缀来解读系统日志和故障消息。您看到的将是与 xx_attach() 有关的错误消息,而不是与二义性 attach() 函数有关的错误。
使用 cmn_err(9F) 函数可以将来自设备驱动程序的消息列显到系统日志中。用于内核模块的 cmn_err (9F) 函数与用于应用程序的 printf(3C) 函数类似。cmn_err(9F) 函数可提供其他格式字符,如用于列显设备寄存器位的 %b 格式。cmn_err(9F) 函数可以将消息写入系统日志中。使用 tail(1) 命令可以监视 /var/adm/messages 中的这些消息。
% tail -f /var/adm/messages |
断言是活动文档一种极有价值的形式。ASSERT(9F) 的语法如下:
void ASSERT(EXPRESSION)
如果预期为 true 的条件实际为 false,则 ASSERT() 宏会停止执行内核。ASSERT() 为程序员提供了对某段代码所做的假设进行验证的方法。
仅当定义了 DEBUG 编译符号时,才会定义 ASSERT() 宏。如果未定义 DEBUG,ASSERT() 宏将无效。
以下示例断言将测试特定指针值不是 NULL 的假设:
ASSERT(ptr != NULL);
如果驱动程序已使用 DEBUG 进行编译,并且执行至此时 ptr 的值为 NULL,则在控制台上会列显以下故障消息:
panic: assertion failed: ptr != NULL, file: driver.c, line: 56
由于 ASSERT(9F) 使用 DEBUG 编译符号,因此任何条件调试代码也应使用 DEBUG。
mutex_owned(9F) 的语法如下:
int mutex_owned(kmutex_t *mp);
驱动程序开发过程中的一项重要工作涉及正确处理多个线程。获得了互斥锁时,应始终使用注释。在未获得明确需要的互斥锁时,注释将更有用。要确定线程是否持有互斥锁,请在 ASSERT(9F) 中使用 mutex_owned():
void helper(void) { /* this routine should always be called with xsp's mutex held */ ASSERT(mutex_owned(&xsp->mu)); /* ... */ }
mutex_owned() 只在 ASSERT() 宏内有效。应该使用 mutex_owned() 控制驱动程序的行为。
可以使用预处理程序符号(例如 DEBUG)或全局变量,借助条件编译将用于调试的代码插入驱动程序中。使用条件编译,可在生产驱动程序中删除不必要的代码。使用变量可以在运行时设置调试输出量。在运行时使用 ioctl 或调试程序设置调试级别可以指定输出。通常,可以组合使用这两种方法。
以下示例依赖编译器来删除无法访问的代码(在本例中是跟在始终为 false 的零测试之后的代码)。此示例还提供了可在 /etc/system 中设置或由调试程序修补的局部变量。
#ifdef DEBUG /* comments on values of xxdebug and what they do */ static int xxdebug; #define dcmn_err if (xxdebug) cmn_err #else #define dcmn_err if (0) cmn_err #endif /* ... */ dcmn_err(CE_NOTE, "Error!\n");
此方法可处理 cmn_err(9F) 具有可变数量参数的情况。另一种方法依赖于宏只有一个参数的情况,即 cmn_err(9F) 的用括号括起的参数列表。宏可以删除此参数。此宏还可在未定义 DEBUG 的情况下,通过将宏扩展为不包含任何内容来消除对优化程序的依赖性。
#ifdef DEBUG /* comments on values of xxdebug and what they do */ static int xxdebug; #define dcmn_err(X) if (xxdebug) cmn_err X #else #define dcmn_err(X) /* nothing */ #endif /* ... */ /* Note:double parentheses are required when using dcmn_err. */ dcmn_err((CE_NOTE, "Error!"));
可以采用多种方法扩展此技术。一种方法是根据 xxdebug 的值指定来自 cmn_err(9F) 的不同消息。但在此类情况下,必须注意不要用大量的调试信息使代码变得晦涩难懂。
另一种常见方案是编写 xxlog() 函数,以便使用 vsprintf(9F) 或 vcmn_err(9F) 来处理变量参数列表。
volatile 是声明任何引用设备寄存器的变量时必须应用的一个关键字。如果不使用 volatile,编译时优化程序可能会意外删除重要访问。省略使用 volatile 可能会生成很难跟踪的错误。
要防止出现难懂的错误,必须正确使用 volatile。volatile 关键字指示编译器对已声明的对象使用精确语义,特别指示不能删除或重新排序对对象的访问。设备驱动程序必须使用 volatile 限定符的两个实例为:
当数据引用外部硬件设备寄存器(即除了存储功能之外还具有负面影响的内存)时。但请注意,如果使用 DDI 数据访问函数访问设备寄存器,则无需使用 volatile。
当数据引用的全局内存可由多个线程访问、不受锁定保护并且依赖于内存访问的序列时。与使用锁定相比,使用 volatile 使用的资源较少。
以下示例使用 volatile。忙标志用于防止线程在设备忙时继续执行,该标志不受锁定保护:
while (busy) { /* do something else */ }
测试线程将在另一个线程关闭 busy 标志时继续执行:
busy = 0;
由于 busy 会在测试线程中被频繁地访问,因此编译器可能通过将 busy 的值放在寄存器中来优化测试,并测试寄存器的内容,而无需在每次测试前都读取内存中的 busy 值。测试线程将永远无法看到 busy 的更改,其他线程将只更改内存中的 busy 值,从而导致死锁。将 busy 标志声明为 volatile 会强制在每次测试前读取其值。
busy 标志的一种替代方法是使用条件变量。请参见线程同步中的条件变量。
使用 volatile 限定符时,请避免意外省略的风险。例如,以下代码
struct device_reg { volatile uint8_t csr; volatile uint8_t data; }; struct device_reg *regp;
比下一个示例更可取:
struct device_reg { uint8_t csr; uint8_t data; }; volatile struct device_reg *regp;
尽管这两个示例在功能上等效,但第二个示例要求编写人员确保类型 struct device_reg 的每个声明中都使用 volatile。第一个示例将导致所有声明中都将数据视为可变数据,因此首选该示例。如上所述,如果使用 DDI 数据访问函数访问设备寄存器,就不必将变量限定为 volatile 了。
为了确保可维护性,必须使驱动程序可以执行以下操作:
潜在故障是在其他某个操作发生后才显现的故障。例如,冷待机设备中出现的硬件故障在主设备出现故障之前无法检测到。此时,系统包含两个有缺陷的设备,并且可能无法继续运行。
未检测到的潜在故障往往最终会导致系统故障。如果不执行潜在故障检查,冗余系统的整体可用性将受到危害。为避免出现这种情况,设备驱动程序必须检测潜在故障,并以与报告其他故障的方法相同的方法来报告这些故障。
应为驱动程序提供对设备进行定期运行状况检查的机制。在容错情况(其中,设备可以是辅助设备或故障转移设备)下,较早地检测到发生故障的辅助设备是确保在主设备出现故障前可以修复或更换辅助设备所必需的。
定期运行状况检查可用来执行以下活动:
检查自上次轮询后值已更改的设备中任何寄存器或内存的位置。
通常表现确定行为的设备功能包括心跳信号、设备计时器(例如,下载使用的本地 lbolt)以及事件计数器。从设备中读取更新的可预测值可提供一切事项的进行程度令人满意的置信度。
时间标记外发请求,如传输块或驱动程序发出的命令。
定期运行状况检查可以查找尚未完成的任何可疑请求。
在设备上启动应在下一次预定检查前完成的操作。
如果此操作为中断操作,则此检查是确保硬件设备能够送出中断的理想方法。