编写设备驱动程序

第 3 部分 生成设备驱动程序

本书的第三部分在为 Solaris 操作系统生成设备驱动程序方面提供了建议:

第 21 章 编译、装入、打包和测试驱动程序

本章介绍驱动程序的开发过程,包括代码布局、编译、打包和测试。

本章介绍有关以下主题的信息:

驱动程序开发摘要

本章以及后面的两章,第 22 章第 23 章,提供了有关开发设备驱动程序的详细信息。

    可采用以下步骤来生成设备驱动程序:

  1. 编写、编译和链接新代码。

    有关文件的命名约定,请参见驱动程序代码布局。使用 C 编译器编译驱动程序。使用 ld(1) 链接驱动程序。请参见编译和链接驱动程序模块相关性

  2. 创建必需的硬件配置文件。

    创建一个特定于名为 xx.conf 的设备的硬件配置文件,其中 xx 为设备的前缀。该文件用于更新 driver.conf(4) 文件。请参见编写硬件配置文件。对于伪设备驱动程序,需要创建一个 pseudo(4) 文件。

  3. 将驱动程序复制到相应的模块目录。

    请参见将驱动程序复制到模块目录

  4. 使用 add_drv(1M) 安装设备驱动程序。

    使用 add_drv 安装驱动程序通常是作为后安装脚本的一部分来执行的。请参见使用 add_drv 安装驱动程序update_drv(1M) 命令用于对驱动程序进行更改。请参见更新驱动程序信息

  5. 装入驱动程序。

    通过访问设备可自动装入驱动程序。请参见装入和卸载驱动程序软件包后安装。另外,也可以使用 modload(1M) 命令装入驱动程序。modload 命令不会调用模块中的任何例程,因此适用于进行测试。请参见装入和卸载测试模块

  6. 测试驱动程序。

    驱动程序应在以下方面进行严格的测试:

    有关其他特定于驱动程序的测试,请参见测试特定类型驱动程序

  7. 删除驱动程序(如有必要)。

    使用 rem_drv(1M) 命令可删除设备驱动程序。请参见删除驱动程序软件包预删除

驱动程序代码布局

设备驱动程序代码通常分为以下文件:

头文件

头文件提供以下定义:

某些头文件定义(如状态结构)可能只有设备驱动程序才需要。这些信息应该放在设备驱动程序本身所包含的专用头文件中。

应用程序可能需要的任何信息(如 I/O 控制命令)均应放在公共头文件中。这些文件包含在驱动程序和任何需要设备相关信息的应用程序中。

虽然专用文件和公共文件并没有命名标准,但一种约定是将专用头文件命名为 xximpl.h,将公共头文件命名为 xxio.h

源文件

设备驱动程序的 C 源文件 (.c 文件)具有以下职责:

配置文件

一般来说,驱动程序的配置文件定义驱动程序需要的所有属性。驱动程序配置文件中的项指定了驱动程序可以探测其存在情况的可能设备实例。可以在驱动程序的配置文件中设置驱动程序的全局属性。有关更多信息,请参见 driver.conf(4) 手册页。

驱动程序配置文件对于非自标识设备是必需的。

驱动程序配置文件对于自标识设备 (self-identifying device, SID) 是可选的。对于自标识设备,配置文件可用于向 SID 节点中添加属性。

以下属性是在驱动程序配置文件中设置的属性示例:

准备安装驱动程序

    安装驱动程序之前,需要执行以下步骤:

  1. 编译驱动程序。

  2. 创建配置文件(如有必要)。

  3. 通过以下任一备选方法,在系统中标识驱动程序模块:

    • 将驱动程序的名称与设备节点的名称匹配。

    • 使用 add_drv(1M)update_drv(1M) 将模块名称通知给系统。

系统维护驱动程序模块名称与 dev_info 节点名称之间的一对一关联。例如,假设名为 mydevice 的设备包含一个 dev_info 节点。处理设备 mydevice 的驱动程序模块也命名为 mydevicemydevice 模块驻留在名为 drv 的子目录中,该子目录位于模块路径下。如果使用 32 位内核,则该模块位于 drv/mydevice 中。如果使用 64 位 SPARC 内核,则该模块位于 drv/sparcv9/mydevice 中。如果使用 64 位 x86 内核,则该模块位于 drv/amd64/mydevice 中。

如果驱动程序是一个 STREAMS 网络驱动程序,则驱动程序名称必须满足以下约束:

如果驱动程序必须使用不同的名称管理 dev_info 节点,则 add_drv(1M) 实用程序可以创建别名。-i 标志指定驱动程序处理的其他 dev_info 节点的名称。update_drv 命令也可以修改已安装的设备驱动程序的别名。

编译和链接驱动程序

您需要编译每个驱动程序源文件,并将生成的对象文件链接到驱动程序模块中。Solaris OS 既与 Sun Studio C 编译器兼容,又与 Free Software Foundation, Inc. 提供的 GNU C 编译器兼容。除非另有说明,否则本节中的示例均使用 Sun Studio C 编译器。有关 Sun Studio C 编译器的信息,请参见《Sun Studio 12:C 用户指南》以及 Sun Developer Network Web 站点上的 Sun Studio Documentation(Sun Studio 文档)。有关编译和链接选项的更多信息,请参见《Sun Studio Man Pages》/usr/sfw 目录中提供了GNU C 编译器。有关 GNU C 编译器的信息,请参见 http://gcc.gnu.org/,或者查看 /usr/sfw/man 中的手册页。

以下示例显示一个名为 xx 的驱动程序,该驱动程序包含两个 C 源文件。生成的驱动程序模块名为 xx。本示例中创建的驱动程序适用于 32 位内核。您必须使用 ld -r,即使您的驱动程序只有一个对象模块也是如此。


% cc -D_KERNEL -c xx1.c
% cc -D_KERNEL -c xx2.c
% ld -r -o xx xx1.o xx2.o

必须定义 _KERNEL 符号以指示此代码定义了一个内核模块。除了驱动程序专用符号以外,不应定义任何其他符号。可以定义 DEBUG 符号,以启用任何对 ASSERT(9F) 的调用。

如果使用 Sun Studio 9、Sun Studio 10 或 Sun Studio 11 编译 64 位 SPARC 体系结构,请使用 -xarch=v9 选项:


% cc -D_KERNEL -xarch=v9 -c xx.c

如果使用 Sun Studio 12 编译 64 位 SPARC 体系结构,请使用 -m64 选项:


% cc -D_KERNEL -m64 -c xx.c

如果使用 Sun Studio 10 或 Sun Studio 11 编译 64 位 x86 体系结构,请同时使用 -xarch=amd64 选项和 -xmodel=kernel 选项:


% cc -D_KERNEL -xarch=amd64 -xmodel=kernel -c xx.c

如果使用 Sun Studio 12 编译 64 位 x86 体系结构,请使用 -m64 选项、-xarch=sse2a 选项和 -xmodel=kernel 选项:


% cc -D_KERNEL -m64 -xarch=sse2a -xmodel=kernel -c xx.c

注 –

Sun Studio 9 不支持 64 位 x86 体系结构。请使用 Sun Studio 10、Sun Studio 11 或 Sun Studio 12 编译和调试 64 位 x86 体系结构的驱动程序。


驱动程序稳定后,您可能需要添加优化标志来生成符合生产质量要求的驱动程序。有关 Sun Studio C 编译器优化的具体信息,请参见《Sun Studio Man Pages》中的 cc(1) 手册页。

在设备驱动程序中,应该将全局变量视为 volatilevolatile 标记将在将变量声明为可变变量中详细介绍。标志的使用取决于平台。请参见手册页。

模块相关性

如果驱动程序模块依赖于其他内核模块导出的符号,则可以通过装载器 ld(1)-dy-N 选项指定相关性。如果驱动程序依赖于 misc/mySymbol 导出的符号,则应使用以下示例创建驱动程序二进制文件。


% ld -dy -r -o xx xx1.o xx2.o -N misc/mySymbol

编写硬件配置文件

如果设备是非自标识设备,则内核需要该设备的硬件配置文件。如果驱动程序名为 xx,则该驱动程序的硬件配置文件应该命名为 xx.conf。有关硬件配置文件的更多信息,请参见 driver.conf(4)pseudo(4)sbus(4)scsi_free_consistent_buf(9F)update_drv(1M) 手册页。

在硬件配置文件中可以定义任意属性。配置文件中项的形式为 property= value,其中 property 是属性名称,value 是其初始值。借助配置文件方法,可以通过更改属性值来配置设备。

安装、更新和删除驱动程序

必须先将驱动程序已存在的信息通知系统,然后才能使用该驱动程序。必须使用 add_drv(1M) 实用程序来正确安装设备驱动程序。安装驱动程序之后,无需使用 add_drv 命令便可从内存中装入和卸载该驱动程序。

将驱动程序复制到模块目录

设备驱动程序模块的路径取决于以下三个条件:

设备驱动程序驻留在以下位置:

/platform/`uname -i`/kernel/drv

包含仅在特定平台上运行的 32 位驱动程序。

/platform/`uname -i`/kernel/drv/sparcv9

包含仅在基于 SPARC 的特定平台上运行的 64 位驱动程序。

/platform/`uname -i`/kernel/drv/amd64

包含仅在基于 x86 的特定平台上运行的 64 位驱动程序。

/platform/`uname -m`/kernel/drv

包含仅在特定平台系列上运行的 32 位驱动程序。

/platform/`uname -m`/kernel/drv/sparcv9

包含仅在基于 SPARC 的特定平台系列上运行的 64 位驱动程序。

/platform/`uname -m`/kernel/drv/amd64

包含仅在基于 x86 的特定平台系列上运行的 64 位驱动程序。

/usr/kernel/drv

包含与平台无关的 32 位驱动程序。

/usr/kernel/drv/sparcv9

包含基于 SPARC 的系统上与平台无关的 64 位驱动程序。

/usr/kernel/drv/amd64

包含基于 x86 的系统上与平台无关的 64 位驱动程序。

要安装 32 位驱动程序,必须将驱动程序及其配置文件复制到模块路径中的 drv 目录。例如,要将驱动程序复制到 /usr/kernel/drv,请键入:


$ su
# cp xx /usr/kernel/drv
# cp xx.conf /usr/kernel/drv

要安装 SPARC 驱动程序,请将驱动程序复制到模块路径中的 drv/sparcv9 目录。将驱动程序配置文件复制到模块路径中的 drv 目录。例如,要将驱动程序复制到 /usr/kernel/drv,应键入:


$ su
# cp xx /usr/kernel/drv/sparcv9
# cp xx.conf /usr/kernel/drv

要安装 64 位 x86 驱动程序,请将驱动程序复制到模块路径中的 drv/amd64 目录。将驱动程序配置文件复制到模块路径中的 drv 目录。例如,要将驱动程序复制到 /usr/kernel/drv,应键入:


$ su
# cp xx /usr/kernel/drv/amd64
# cp xx.conf /usr/kernel/drv

注 –

所有驱动程序配置文件 (.conf 文件)都必须放入模块路径中的 drv 目录。不能将 .conf 文件放入 drv目录的任何子目录。


使用 add_drv 安装驱动程序

使用 add_drv(1M) 命令可将驱动程序安装到系统中。如果驱动程序成功安装, add_drv 将运行 devfsadm(1M),以便在 /dev 目录中创建逻辑名称。


# add_drv xx

在本例中,设备将自身标识为 xx。设备的特殊文件具有缺省的拥有权和权限 (0600 root sys)。add_drv 命令也允许为设备指定其他名称(别名)。有关显式添加别名和设置文件权限的信息,请参见 add_drv(1M) 手册页。


注 –

请勿使用 add_drv 命令安装 STREAMS 模块。有关详细信息,请参见《STREAMS Programming Guide》


如果驱动程序创建了不表示终端设备(如磁盘、磁带或者端口)的次要节点,则可以修改 /etc/devlink.tab,以使 devfsadm/dev 中创建逻辑设备名称。另外,也可以通过安装驱动程序时运行的程序来创建逻辑名称。

更新驱动程序信息

使用 update_drv(1M) 命令可以通知系统对已安装的设备驱动程序所做的任何更改。缺省情况下,系统将会重新读取驱动程序配置文件,并重新装入驱动程序二进制模块。

删除驱动程序

要从系统中删除驱动程序,请使用 rem_drv(1M) 命令,然后从模块路径中删除驱动程序模块和配置文件。使用 add_drv(1M) 重新安装驱动程序之前,无法再使用该驱动程序。删除 SCSI HBA 驱动程序需要重新引导才能生效。

装入和卸载驱动程序

打开与设备驱动程序关联的特殊文件(访问该设备)即可装入该驱动程序。您可以使用 modload(1M) 命令将驱动程序装入内存,但 modload 不会调用模块中的任何例程。 首选方法是打开设备。

通常,当不再使用设备时,系统会自动卸载设备驱动程序。在开发过程中,要显式卸载驱动程序,可能需要使用 modunload(1M)。为了成功执行 modunload,设备驱动程序必须处于非活 动状态。对设备的任何未完成引用(如通过 open(2)mmap(2) 的引用)均不应存在。

modunload 命令将与运行时相关的 module_id 用作参数。要查找 module_id,请使用 grepmodinfo(1M) 的输出中搜索相关的驱动程序名称。然后检查第一列。


# modunload -i module-id

要卸载当前无法装入的所有模块,请将模块 ID 指定为零:


# modunload -i 0

要成功执行 modunload(1M),驱动程序除了需要处于非活动状态外,还必须包含正常的 detach(9E)_fini(9E) 例程。

驱动程序打包

软件的标准交付方式是创建一个包含所有软件组件的软件包。软件包为软件产品所有组件的安装和删除提供了一种受控机制。除了用于使用产品的文件以外,软件包还包括用于安装和卸载应用程序的控制文件。后安装和预删除安装脚本即为两种此类控制文件。

软件包后安装

将包含驱动程序二进制文件的软件包安装到系统中之后, 必须运行 add_drv(1M) 命令。add_drv 命令将完成驱动程序的安装。通常,add_drv 是在后安装脚本中运行的,如以下示例 中所示。

#!/bin/sh
#
#       @(#)postinstall 1.1

PATH="/usr/bin:/usr/sbin:${PATH}"
export PATH

#
# Driver info
#
DRV=<driver-name>
DRVALIAS="<company-name>,<driver-name>"
DRVPERM='* 0666 root sys'

ADD_DRV=/usr/sbin/add_drv

#
# Select the correct add_drv options to execute.
# add_drv touches /reconfigure to cause the
# next boot to be a reconfigure boot.
#
if [ "${BASEDIR}" = "/" ]; then
    #
    # On a running system, modify the
    # system files and attach the driver
    #
    ADD_DRV_FLAGS=""
else     
    #
    # On a client, modify the system files
    # relative to BASEDIR
    #
    ADD_DRV_FLAGS="-b ${BASEDIR}"
fi       
 
#
# Make sure add_drv has not been previously executed
# before attempting to add the driver.
#
grep "^${DRV} " $BASEDIR/etc/name_to_major > /dev/null 2>&1
if [ $? -ne 0 ]; then
    ${ADD_DRV} ${ADD_DRV_FLAGS} -m "${DRVPERM}" -i "${DRVALIAS}" ${DRV}
    if [ $? -ne 0 ]; then
        echo "postinstall: add_drv $DRV failed\n" >&2
        exit 1
    fi
fi
exit 0

软件包预删除

删除包含驱动程序的软件包时,必须先运行 rem_drv(1M) 命令,然后删除驱动程序二进制文件和其他组件。以下示例演示了一个使用 rem_drv 命令删除驱动程序的预删除脚本。

#!/bin/sh
#
#       @(#)preremove  1.1
 
PATH="/usr/bin:/usr/sbin:${PATH}"
export PATH
 
#
# Driver info
#
DRV=<driver-name>
REM_DRV=/usr/sbin/rem_drv
 
#
# Select the correct rem_drv options to execute.
# rem_drv touches /reconfigure to cause the
# next boot to be a reconfigure boot.
#
if [ "${BASEDIR}" = "/" ]; then
    #
    # On a running system, modify the
    # system files and remove the driver
    #
    REM_DRV_FLAGS=""
else     
    #
    # On a client, modify the system files
    # relative to BASEDIR
    #
    REM_DRV_FLAGS="-b ${BASEDIR}"
fi
 
${REM_DRV} ${REM_DRV_FLAGS} ${DRV}
 
exit 0

驱动程序测试条件

设备驱动程序可以正常运行后,应在分发之前全面测试该驱动程序。除了测试传统 UNIX 设备驱动程序中的功能以外,Solaris 驱动程序还需要测试电源管理功能,如驱动程序的动态装入和卸载。

配置测试

驱动程序能否处理多种设备配置是测试过程的重要部分。驱动程序可以在一种简单(或缺省)配置中正常工作后,还应该测试其他配置。根据设备不同,可以通过更改跳线或 DIP 开关来完成配置测试。如果可能的配置数较少,则应尝试测试所有配置。如果可能的配置数较多,则应为这些可能的配置定义不同的类,并抽样测试每类配置。定义这些类取决于不同配置参数之间的潜在交互。这些交互是设备类型与驱动程序编写方式之间的配合。

对于每种设备配置,必须测试基本运行情况,包括装入、打开、读取、写入、关闭和卸载驱动程序。任何取决于配置的运行情况都需要特别注意。例如,更改设备寄存器的基本内存地址不可能影响大多数驱动程序函数的行为。如果驱动程序在一个地址上正常工作,则该驱动程序在其他地址上也能正常工作。另一方面,特殊 I/O 控制调用的结果可能会因特定设备配置而异。

以不同配置装入驱动程序可确保 probe(9E)attach(9E) 入口点能够在不同地址找到设备。对于基本功能测试,使用常规的 UNIX 命令(如 cat(1)dd(1M))通常就可满足字符设备的要求。对于块设备,可能需要挂载或引导。

功能测试

对驱动程序进行全面的配置测试之后,应全面测试驱动程序的所有功能。这些测试需要执行驱动程序所有入口点的操作。

许多驱动程序需要使用自定义的应用程序来测试功能。但是,对于磁盘、磁带或异步板等设备的基本驱动程序,使用标准系统实用程序即可进行测试。在此过程中,应测试所有入口点,包括 devmap(9E)chpoll(9E)ioctl(9E)(如果适用)。对于每种驱动程序,ioctl () 测试可能完全不同。对于非标准设备,通常需要使用自定义的测试应用程序。

错误处理

在理想环境中,驱动程序也许可以正确执行,但如果出现错误(如操作错误或数据错 误),则可能会失败。因此,驱动程序测试的一个重要部分是测试驱动程序的错误处理。

应该执行驱动程序的所有可能的错误情况,包括实际硬件故障导致的错误情况。某些硬件错误情况可能难于引发,但如有可能,应尽力强制引发或模拟此类错误。在实际使用时,所有这些情况都有可能遇到。为了测试以找出这些错误的根源,应该拆除或松开电缆、拆除板以及编写错误的用户应用程序代码。另请参见第 13 章


注意 – 注意 –

测试时,请务必采取正确的电气预防措施。


测试装入和卸载

由于没有装入或卸载的驱动程序会导致意外的停机时间,因此必须全面测试装入和卸载。

与以下示例类似的脚本应该足够满足要求:

#!/bin/sh
cd <location_of_driver>
while [ 1 ]
do
    modunload -i 'modinfo | grep " <driver_name> " | cut -cl-3' &
    modload <driver_name> &
done

压力、性能和互操作性测试

为有助于确保驱动程序正常执行,应对该驱动程序进行强有力的压力测试。例如,通过驱动程序运行单个线程并不会测试必须等待的锁定逻辑或条件变量。设备操作应由多个进程同时执行,以使几个线程同时执行同一代码。

执行同时测试的方法取决于驱动程序。某些驱动程序需要使用特殊的测试应用程序,而在后台启动多个 UNIX 命令适用于其他驱动程序。正确的测试取决于特定驱动程序在何处使用锁定和条件变量。在多处理器计算机上测试驱动程序比在单处理器计算机上测试更有可能暴露问题。

此外,还必须测试驱动程序之间的互操作性,尤其是在不同的设备可以共享中断级别的情况下。如有可能,请配置与正在测试的设备的中断级别相同的另一个设备。压力测试可以确定驱动程序是否正确请求其自己的中断,以及是否按照预期目标运行。应该对两个设备同时运行压力测试。即使设备不共享中断级别,该测试仍然很重要。例如,假设在测试某个网络驱动程序时,串行通信设备遇到错误。同一问题也可能会导致系统的其余部分遇到中断延迟问题。

这些压力测试下的驱动程序性能应使用 UNIX 性能度量工具进行度量。此类测试与使用 time(1) 命令以及压力测试所用命令一样简单。

DDI/DKI 兼容性测试

为确保与更高发行版的兼容性以及对当前发行版的可靠支持,每个驱动程序都应该与 DDI/DKI 兼容。检查是否仅使用了《man pages section 9: DDI and DKI Kernel Functions》《man pages section 9: DDI and DKI Driver Entry Points》中的内核例程以及《man pages section 9: DDI and DKI Properties and Data Structures》中的数据结构。

安装和打包测试

驱动程序是以软件包形式提供给客户的。使用标准机制,可在系统中添加或删除软件包(请参见《应用程序包开发者指南》)。

应对用户在系统中添加或删除软件包的能力进行测试。测试时,应从供发行使用的每种类型的介质中安装和删除软件包。该测试应该包含多种系统配置。对于目标系统的目录环境,软件包不能做出任何无根据的假设。但是,可以对标准内核文件的保留位置做出某些有效假设。此外,还要在新安装的、尚未修改开发环境的计算机上测试软件包的添加和删除。常见的打包错误是软件包依赖于仅在开发时使用的工具或文件。例如,在驱动程序安装程序中,不应使用源兼容性软件包 SUNWscpu 中的任何工具。

驱动程序安装必须在不带任何可选软件包的最小 Solaris 系统上测试。

测试特定类型驱动程序

本节提供了一些有关如何测试某些类型的标准设备的建议。

磁带驱动程序

磁带驱动程序应通过执行多次归档和恢复操作来测试。cpio(1)tar(1) 命令可用于此目的。使用 dd(1M) 命令可将整个磁盘分区写入磁带。接下来,读回数据,并将数据写入另一个相同大小的分区。然后比较这两个副本。mt(1) 命令可以执行特定于磁带驱动程序的大多数 I/O 控制。请参见 mtio(7I) 手册页。尝试使用所有选项。以下三种方法可以测试磁带驱动程序的错误处理能力:

磁带驱动程序通常实现以独占方式访问的 open(9E) 调用。可以通过打开设备,然后让另一个进程尝试打开同一设备,来测试这些 open() 调用。

磁盘驱动程序

磁盘驱动程序应在原始设备模式和块设备模式下进行测试。对于块设备测试,请在设备上创建一个新的文件系统。然后,尝试挂载该新文件系统,并尝试执行多种文件操作。


注 –

文件系统使用页缓存,因此重复读取相同文件实际上并不会执行驱动程序。通过使用 mmap(2) 对文件进行内存映射,可以强制页缓存从设备中检索数据。然后使用 msync(3C) 使内存中的副本无效。


将另一个相同大小的(未挂载)分区复制到原始设备。然后使用 fsck(1M) 之类的命令检验副本的正确性。 新分区也可以挂载,然后以后与旧分区进行逐文件比较。

异步通信驱动程序

通过为串行端口设置一个 login 连接线,可对异步驱动程序进行基本级别的测试。是否为一种较好的测试方法要看用户是否可以通过此连接线登录。但是,要充分测试异步驱动程序,必须使用多个高速中断来测试所有 I/O 控制函数。涉及回送串行电缆和较高数据传输速率的测试有助于确定驱动程序的可靠性。您可以在该连接线上运行 uucp(1C),以提供某些实践。但是,由于 uucp 执行其自己的错误处理,因此请验证驱动程序不会报告 uucp 进程的额外的错误数。

这些类型的设备通常是基于 STREAMS 的。有关更多信息,请参见《STREAMS Programming Guide》

网络驱动程序

可以使用标准网络实用程序对网络驱动程序进行测试。由于可在网络的每个端点上比较文件,因此 ftp(1)rcp(1) 命令非常有用。该驱动程序应在网络负载较重的情况下测试,以便多个进程可以运行各种命令。

网络负载较重包括以下情况:

执行测试时,应拔下网络电缆,以确保驱动程序可从产生的错误情况中正常恢复。另一项重要测试是让驱动程序快速连续接收多个包,即背对背包。在这种情况下,负载较轻的网络上相对较快的主机应向测试计算机快速连续发送多个包。请检验接收驱动程序不会丢弃第二个以及后续的包。

这些类型的设备通常是基于 STREAMS 的。有关更多信息,请参见《STREAMS Programming Guide》

第 22 章 调试、测试和调优设备驱动程序

本章简要介绍用于帮助测试、调试和调优设备驱动程序的各种工具。本章介绍有关以下主题的信息:

测试驱动程序

为了避免数据丢失和出现其他问题,在对新设备驱动程序进行测试时应特别注意。本节将对各种测试策略进行讨论。例如,设置可通过串行连接来控制的单独系统是对新驱动程序进行测试的最安全方法。可用不同的内核变量设置来装入测试模块,以便在不同的内核条件下测试性能。如果系统崩溃,应准备好恢复备份数据、分析任何崩溃转储并重新生成设备目录。

启用 Deadman 功能以避免硬挂起

如果系统处于硬挂起状态,则不能中断调试程序。如果启用 deadman 功能,系统将出现紧急情况,而不是无限期挂起。您随后可以使用 kmdb(1) 内核调试程序来分析您的问题。

Deadman 功能每秒检查一次系统时钟是否正在更新。如果系统时钟未在更新,则表明您处于无限期的挂起状态中。如果系统时钟在 50 秒内未更新,则 deadman 功能将导致出现紧急情况并会将您置于调试程序中。

    执行以下步骤以启用 deadman 功能:

  1. 确保正在使用 dumpadm(1M) 捕获崩溃映像。

  2. /etc/system 文件中设置 snooping 变量。有关 /etc/system 文件的信息,请参见 system(4) 手册页。

    set snooping=1
  3. 重新引导系统,以便再次读取 /etc/system 文件,从而使 snooping 设置生效。

请注意,系统中的所有区域也都会继承 deadman 设置。

如果系统在启用 deadman 功能的情况下挂起,则应在控制台上看到类似如下示例的输出:

panic[cpu1]/thread=30018dd6cc0: deadman: timed out after 9 seconds of
clock inactivity

panic: entering debugger (continue to save dump)

在调试程序内,使用 ::cpuinfo 命令调查时钟中断无法触发的原因并使系统时间提前。

使用串行连接进行测试

使用串行连接是测试驱动程序的一种好方法。使用 tip(1) 命令可在主机系统和测试系统之间建立串行连接。借助此方法,可将主机控制台上的 tip 窗口用作测试计算机的控制台。有关其他信息,请参见 tip(1) 手册页。

tip 窗口具有如下优点:


注 –

尽管调试 Solaris 设备驱动程序时不要求使用 tip 连接和另一台计算机,但仍建议使用此方法。


Procedure针对 tip 连接设置主机系统

  1. 使用主机系统和测试计算机上的串行端口 A 将两台计算机连接起来。

    必须使用空调制解调器电缆建立此连接。

  2. 在主机系统中,确保 /etc/remote 中存在对应于该连接的项。有关详细信息,请参见 remote(4) 手册页。

    终端项必须与使用的串行端口匹配。Solaris 操作系统附带对应于串行端口 B 的适当项,但必须为串行端口 A 添加一个终端项:


    debug:\
            :dv=/dev/term/a:br#9600:el=^C^S^Q^U^D:ie=%$:oe=^D:

    注 –

    波特率必须设置为 9600。


  3. 在主机上的 shell 窗口中,运行 tip(1) 并指定项的名称:


    % tip debug
    connected

    现在,shell 窗口是一个 tip 窗口,具有到测试计算机的控制台的连接。


    注意 – 注意 –

    请勿在主机上使用 STOP-A(针对 SPARC 计算机)或 F1-A(针对 x86 体系结构计算机)来停止测试计算机。此操作实际上会停止主机。要向测试计算机发送中断,请在 tip 窗口中键入 ~#。仅当诸如 ~# 命令的这些字符位于行首才能识别这些命令。如果命令无效,请按回车键或 Ctrl-U 组合键。


在 SPARC 平台上设置目标系统

一种在 SPARC 平台上快速设置测试计算机的方法是在打开计算机之前拔除键盘。然后,该计算机将自动使用串行端口 A 作为控制台。

另一种设置测试计算机的方法是使用引导 PROM 命令使串行端口 A 成为控制台。在测试计算机上,在引导 PROM ok 提示符处,将控制台 I/O 定向到串行线路。要使测试计算机始终以串行端口 A 作为控制台,请设置环境变量 input-deviceoutput-device


示例 22–1 使用引导 PROM 命令设置 input-deviceoutput-device


ok setenv input-device ttya
ok setenv output-device ttya

也可以使用 eeprom 命令使串行端口 A 成为控制台。以超级用户身份,执行以下命令使 input-deviceoutput-device 参数指向串行端口 A。以下示例对 eeprom 命令进行了说明。


示例 22–2 使用 eeprom 命令设置 input-deviceoutput-device


# eeprom input-device=ttya
# eeprom output-device=ttya

eeprom 命令会导致在以后每次系统引导时都将控制台重定向到串行端口 A。

在 x86 平台上设置目标系统

在 x86 平台上,使用 eeprom 命令可使串行端口 A 成为控制台。此过程与 SPARC 平台过程相同。请参见在 SPARC 平台上设置目标系统eeprom 命令会使控制台在重新引导期间切换到串行端口 A (COM1)。


注 –

除非 BIOS 支持控制台重定向到串行端口,否则 x86 计算机在引导过程的早期阶段之前不会将控制台控制权转交给 tip 连接。在 SPARC 计算机中,tip 连接在整个引导过程中都维护着控制台控制权。


设置测试模块

使用 /etc 目录中的 system(4) 文件可在引导时设置内核变量的值。使用内核变量,可在驱动程序中切换不同的行为,并利用内核提供的调试功能。内核变量 moddebugkmem_flags 在调试中非常有用,本节稍后将对其进行讨论。另请参见启用 Deadman 功能以避免硬挂起

由于仅在内核引导时读取 /etc/system 一次,因此引导后对内核变量所做的更改不可靠。修改此文件后,必须重新引导系统,更改才能生效。如果文件中的更改导致系统无法工作,请使用询问功能 (-a) 选项进行引导。然后,将 /dev/null 指定为系统文件。


注 –

后续发行版中不一定存在内核变量。


设置内核变量

set 命令可以更改模块变量或内核变量的值。要设置模块变量,请指定模块名称和变量:


set module_name:variable=value

例如,要在名为 myTest 的驱动程序中设置变量 test_debug,请按如下方式使用 set


% set myTest:test_debug=1

要设置由内核自身导出的变量,可忽略模块名称。

还可以使用按位 OR 运算设置值,例如:


% set moddebug | 0x80000000

装入和卸载测试模块

使用命令 modload(1M)modunload(1M)modinfo(1M) 可以添加测试模块,在对驱动程序进行调试和压力测试时,这是一种非常有用的方法。正常操作中通常不需要这些命令,因为内核会自动装入需要的模块并卸载未使用的模块。moddebug 内核变量可与这些命令一起使用,以提供信息并设置控制。

使用 modload() 函数

使用 modload(1M) 可将模块强制装入内存。modload 命令可验证装入驱动程序时该驱动程序是否具有未解析的引用。装入驱动程序并表明该驱动程序一定可以连接。驱动程序成功装入时,将调用该驱动程序的 _info(9E) 入口点,但不一定调用 attach() 入口点。

使用 modinfo() 函数

使用 modinfo(1M) 可以确认驱动程序已装入。


示例 22–3 使用 modinfo 确认已装入的驱动程序


$ modinfo
 Id Loadaddr   Size Info Rev Module Name
  6 101b6000    732   -   1  obpsym (OBP symbol callbacks)
  7 101b65bd  1acd0 226   1  rpcmod (RPC syscall)
  7 101b65bd  1acd0 226   1  rpcmod (32-bit RPC syscall)
  7 101b65bd  1acd0   1   1  rpcmod (rpc interface str mod)
  8 101ce8dd  74600   0   1  ip (IP STREAMS module)
  8 101ce8dd  74600   3   1  ip (IP STREAMS device)
...
$ modinfo | grep mydriver
169 781a8d78   13fb   0   1  mydriver (Test Driver 1.5)

info 字段中的数字是为驱动程序选择的主设备号。如果提供了模块 ID,则可使用 modunload(1M) 命令来卸载模块。模块 ID 位于 modinfo 输出的左列中。

有时,发出 modunload 后驱动程序不会按预期卸载,因为该驱动程序被确定处于忙状态。由于驱动程序确实繁忙或 detach 入口点未正确实现而导致驱动程序无法执行 detach(9E) 时,会出现上述情况。

使用 modunload()

要从内存中删除所有当前未使用的模块,请使用模块 ID 0 运行 modunload(1M):


# modunload -i 0

设置 moddebug 内核变量

moddebug 内核变量可控制模块装入过程。moddebug 的可能值包括:

0x80000000

装入或卸载模块时向控制台列显消息。

0x40000000

提供更详细的错误消息。

0x20000000

装入或卸载时列显更多详细信息,如包含地址和大小。

0x00001000

不自动卸载驱动程序。系统资源变少时,系统不尝试卸载设备驱动程序。

0x00000080

不自动卸载流。系统资源变少时,系统不尝试卸载 STREAMS 模块。

0x00000010

不自动卸载任何类型的内核模块。

0x00000001

如果与 kmdb 一起运行,moddebug 会导致执行断点,并在调用每个模块的 _init() 例程之前立即返回到 kmdb。此设置还会在执行模块的 _info()_fini() 例程时生成其他调试消息。

设置 kmem_flags 调试标志

kmem_flags 内核变量用于启用内核的内存分配器中的调试功能。将 kmem_flags 设置为 0xf 即启用分配器的调试功能。这些功能包括运行时检查,以找出以下代码条件:

《Solaris 模块调试器指南》介绍了如何使用内核内存分配器来分析此类问题。


注 –

在将 kmem_flags 设置为 0xf 的情况下进行测试和开发有助于检测潜在的内存损坏错误。由于将 kmem_flags 设置为 0xf 会更改内核内存分配器的内部行为,因此最好应不使用 kmem_flags 而执行全面测试。


避免测试系统中发生数据丢失

驱动程序错误有时会导致系统无法引导。通过采取预防措施(如本节中所述),可以避免在此情况下重新安装系统。

备份关键系统文件

许多与驱动程序相关的系统文件都很难重新构造(如果可以重新构造)。如果安装期间驱动程序使系统崩溃,则诸如 /etc/name_to_major /etc/driver_aliases/etc/driver_classes/etc/minor_perm 之类的文件会损坏。请参见 add_drv(1M) 手册页。

为安全起见,请在正确配置测试计算机后生成根文件系统的副本。如果打算修改 /etc/system 文件,请在修改之前生成该文件的副本。

Procedure使用替代内核进行引导

为避免系统无法运行,应从内核副本和关联的二进制文件进行引导,而不是从缺省内核进行引导。

  1. /platform/* 中生成驱动程序的副本。


    # cp -r /platform/`uname -i`/kernel /platform/`uname -i`/kernel.test
    
  2. 将驱动程序模块放在 /platform/`uname -i`/kernel.test/drv 中。

  3. 引导替代内核,而非缺省内核。

    创建并存储替代内核后,可通过许多方法来引导此内核。

    • 可通过重新引导来引导替代内核:


      # reboot -- kernel.test/unix
      
    • 在基于 SPARC 的系统中,还可以从 PROM 进行引导:


      ok boot kernel.test/sparcv9/unix
      

      注 –

      要使用 kmdb 调试程序进行引导,请按模块调试程序入门中所述使用 -k 选项。


    • 在基于 x86 的系统中,当引导过程中显示 Select (b)oot or (i)nterpreter: 消息时,请键入以下命令:


      boot kernel.test/unix
      

示例 22–4 引导替代内核

以下示例说明如何使用替代内核进行引导。


ok boot kernel.test/sparcv9/unix
Rebooting with command: boot kernel.test/sparcv9/unix
Boot device: /sbus@1f,0/espdma@e,8400000/esp@e,8800000/sd@0,0:a File and \
    args:
kernel.test/sparcv9/unix


示例 22–5 使用 -a 选项引导替代内核

也可以使用询问功能 (-a) 选项进行引导来更改模块路径。使用此选项会生成用于配置引导方法的一系列提示。


ok boot -a
Rebooting with command: boot -a
Boot device: /sbus@1f,0/espdma@e,8400000/esp@e,8800000/sd@0,0:a File and \
args: -a
Enter filename [kernel/sparcv9/unix]: kernel.test/sparcv9/unix
Enter default directory for modules
[/platform/sun4u/kernel.test /kernel /usr/kernel]: <CR>
Name of system file [etc/system]: <CR>
SunOS Release 5.10 Version Generic 64-bit
Copyright 1983-2002 Sun Microsystems, Inc. All rights reserved.
root filesystem type [ufs]: <CR>
Enter physical name of root device
[/sbus@1f,0/espdma@e,8400000/esp@e,8800000/sd@0,0:a]: <CR>

考虑替代备份计划

如果将系统连接到网络,则可将测试计算机添加为服务器的客户机。如果出现问题,可从网络引导系统。然后,便可挂载本地磁盘,并进行任何修复。也可以直接从 Solaris 系统 CD-ROM 引导系统。

另一种从灾难中恢复的方法是获取另一个可引导的根文件系统。使用 format(1M) 创建一个大小与原始分区完全相同的分区。然后,使用 dd(1M) 复制可引导的根文件系统。创建副本后,在新文件系统上运行 fsck(1M) 以确保其完整性。

以后,如果系统无法从原始根分区进行引导,可引导备份分区。可以使用 dd(1M) 将备份分区复制到原始分区。可能会遇到这样的情况,即使根文件系统未损坏,也无法引导系统。例如,只有引导块或引导程序损坏。在这种情况下,可使用询问功能 (-a) 选项从备份分区进行引导。然后,将原始文件系统指定为根文件系统。

捕获系统崩溃转储

当系统出现紧急情况时,系统会将内核内存的映像写入转储设备。缺省情况下,该转储设备是最合适的交换设备。该转储是系统崩溃转储,它与应用程序生成的核心转储类似。在系统出现紧急情况后进行重新引导时,savecore(1M) 会检查转储设备中是否存在崩溃转储。如果找到转储,savecore 将生成名为 unix.n 的内核符号表的副本。然后,savecore 实用程序将在核心映像目录中转储名为 vmcore.n 的核心转储文件。缺省情况下,核心映像目录为 /var/crash/machine_name。如果 /var/crash 没有足够的空间用于核心转储,系统将显示所需的空间,但不实际保存转储。然后,可针对核心转储和已保存的内核使用 mdb(1)

在 Solaris 操作系统中,缺省情况下会启用崩溃转储。dumpadm(1M) 命令用于配置系统崩溃转储。使用 dumpadm 命令可以验证崩溃转储是否已启用,并确定保存核心转储文件的位置。


注 –

可以阻止 savecore 实用程序填满文件系统,即在要保存转储的目录中添加一个名为 minfree 的文件。在此文件中,指定在 savecore 运行后保持可用的千字节数。如果没有足够的可用空间,则不保存核心转储文件。


恢复设备目录

如果驱动程序在执行 attach(9E) 期间崩溃,可能会损坏 /devices/dev 目录。如果任一目录被损坏,则可引导系统并运行 fsck(1M) 以修复损坏的根文件系统,从而重新生成目录。然后,便可挂载根文件系统。通过运行 devfsadm(1M) 并在已挂载的磁盘上指定 /devices 目录可以重新创建 /devices/dev 目录。

以下示例说明如何在 SPARC 系统上修复损坏的根文件系统。在此示例中,损坏的磁盘为 /dev/dsk/c0t3d0s0,替代引导磁盘为 /dev/dsk/c0t1d0s0


示例 22–6 恢复损坏的设备目录


ok boot disk1
...
Rebooting with command: boot kernel.test/sparcv9/unix
Boot device: /sbus@1f,0/espdma@e,8400000/esp@e,8800000/sd@31,0:a File and \
    args:
kernel.test/sparcv9/unix
...
# fsck /dev/dsk/c0t3d0s0** /dev/dsk/c0t3d0s0
** Last Mounted on /
** Phase 1 - Check Blocks and Sizes
** Phase 2 - Check Pathnames
** Phase 3 - Check Connectivity
** Phase 4 - Check Reference Counts
** Phase 5 - Check Cyl groups
1478 files, 9922 used, 29261 free
     (141 frags, 3640 blocks, 0.4% fragmentation)
# mount /dev/dsk/c0t3d0s0 /mnt
# devfsadm -r /mnt


注 –

/devices/dev 目录进行修复后,就可以在系统的其他部分仍处于损坏状态时引导系统。此类修复只是在重新安装系统前进行的临时修复,作用是保存信息(如系统崩溃转储)。


调试工具

本节介绍可以应用于设备驱动程序的两个调试程序。《Solaris 模块调试器指南》中详细介绍了这两个调试程序。

kmdbmdb 调试程序的用户界面大部分是一样的。因此,许多调试方法都可在这两种工具中使用相同命令来应用。这两种调试程序都支持宏、dcmd 和 dmod。dcmd(读作为 dee-command)是调试程序中的例程,它可以访问当前目标程序的任何属性。dcmd 可在运行时动态装入。dmod(调试程序模块的缩写)是可以装入以提供非标准行为的 dcmd 包。

mdbkmdb 都可向后兼容传统调试程序(如 adbkadb)。mdb 调试程序可以执行可用于 kmdb 的所有宏以及用于 adb 的任何用户定义的传统宏。有关在何处查找标准宏集的信息,请参见《Solaris 模块调试器指南》。

事后调试

事后分析为驱动程序开发者提供了许多益处。多个开发者可以并行检查一个问题。可以针对单个崩溃转储使用调试程序的多个实例。可以脱机执行分析,以便在可能的情况下使崩溃的系统恢复运行。事后分析允许以 dmod 形式使用用户开发的调试程序功能。Dmod 可为实时调试程序(如 kmdb)捆绑内存密集程度过高的功能。

如果装入 kmdb 时系统出现紧急情况,则控制权会传递给调试程序,以便立即进行检查。如果不适合使用 kmdb 分析当前问题,则使用 :c 继续执行并保存崩溃转储不失为一种好的策略。系统重新引导时,可以使用 mdb 对已保存的崩溃转储执行事后分析。此过程类似于从进程核心转储文件调试应用程序崩溃。


注 –

在 Solaris 操作系统的早期版本中,adb(1) 是推荐用于事后分析的工具。在当前的 Solaris 操作系统中,mdb(1) 是推荐用于事后分析的工具。mdb() 功能集不仅包含传统 crash (1M) 实用程序中的命令集,还具有更多功能。Solaris 操作系统中不再提供 crash 实用程序。


使用 kmdb 内核调试程序

kmdb 调试程序是可提供以下功能的交互式内核调试程序:

本节假定您已熟悉 kmdb 调试程序。本节重点介绍在设备驱动程序设计中非常有用的 kmdb 功能。要详细了解如何使用 kmdb,请参阅kmdb(1) 手册页和《Solaris 模块调试器指南》。如果您熟悉 kadb,请参阅 kadb(1M) 手册页以了解 kadbkmdb 的主要差别。

可以任意装入和卸载 kmdb 调试程序。《Solaris 模块调试器指南》中包括装入和卸载 kmdb 的说明。为了安全和方便起见,强烈建议使用替代内核进行引导。如本节中所述,在 SPARC 平台与 x86 平台上引导过程略有不同。


注 –

缺省情况下,当 kmdb 运行时,kmdb 使用 CPU ID 作为提示符。在本章的示例中,除非另有指定,否则使用 [0] 作为提示符。


在 SPARC 平台上使用替代内核引导 kmdb

使用以下任一命令通过 kmdb 和替代内核引导 SPARC 系统:


boot kmdb -D kernel.test/sparcv9/unix 
boot kernel.test/sparcv9/unix -k

在 x86 平台上使用替代内核引导 kmdb

使用以下任一命令通过 kmdb 和替代内核引导 x86 系统:


b kmdb -D kernel.test/unix 
b kernel.test/unix -k

kmdb 中设置断点

使用 bp 命令设置断点,如以下示例中所示。


示例 22–7 在 kmdb 中设置标准断点


[0]> myModule`myBreakpointLocation::bp
        

如果尚未装入目标模块,则会显示指示这一情况的错误消息,并且不会创建断点。在这种情况下,可以使用延迟断点。装入指定的模块时,会自动激活延迟断点。通过在 bp 命令后面指定目标位置可以设置延迟断点。以下示例对延迟断点进行了说明。


示例 22–8 在 kmdb 中设置延迟断点


[0]>::bp myModule`myBreakpointLocation       

有关使用断点的更多信息,请参见《Solaris 模块调试器指南》。也可以通过键入以下任意一行来获取帮助:


> ::help bp
> ::bp dcmd

为驱动程序开发者提供的 kmdb

kmdb(1M) 调试程序支持可用于显示内核数据结构的宏。可以使用 $M 来显示 kmdb 宏。宏的使用形式为:


[ address ] $<macroname

注 –

这些宏所显示的信息以及显示信息所用的格式都不构成接口。因此,该信息和格式可以随时更改。


下表中的 kmdb 宏对于设备驱动程序的开发者特别有用。为方便起见,给出了传统的宏名称(如果适用)。

表 22–1 kmdb

Dcmd 

传统宏 

说明 

::devinfo

devinfo

devinfo_brief

devinfo.prop

列显设备节点的摘要 

::walk devinfo_parents

devinfo.parent

遍历设备节点的祖先 

::walk devinfo_sibling

devinfo.sibling

遍历设备节点的同级节点 

::minornodes

devinfo.minor

列显与给定设备节点对应的次要节点 

::major2name

 

列显绑定到给定设备节点的设备的名称。 

::devbindings

 

列显绑定到给定设备节点或主设备号的设备节点。 

::devinfo dcmd 显示节点状态,其值为以下所列之一:

DS_ATTACHED

驱动程序的 attach(9E) 例程成功返回。

DS_BOUND

节点已绑定到驱动程序,但尚未调用驱动程序的 probe(9E) 例程。

DS_INITIALIZED

父结点已为驱动程序指定总线地址。特定于实现的初始化已完成。此时尚未调用驱动程序的 probe(9E) 例程。

DS_LINKED

设备节点已链接至内核的设备树中,但系统尚未找到用于此节点的驱动程序。

DS_PROBED

驱动程序的 probe(9E) 例程成功返回。

DS_READY

设备已完全配置。

使用 mdb 模块调试程序

mdb(1) 模块调试程序可以应用于以下文件类型:

mdb 调试程序可为分析内核问题提供复杂的调试支持。本节概述 mdb 功能。有关 mdb 的完整讨论,请参阅《Solaris 模块调试器指南》

尽管 mdb 可用来改变实时内核状态,但 mdb 缺少 kmdb 提供的内核执行控制。因此,kmdb 是进行运行时调试的首选调试程序,而 mdb 调试程序更多用于静态情况。


注 –

mdb 的提示符为 >


模块调试程序入门

mdb 为实现调试程序模块提供了大量编程 API,从而使驱动程序开发者可以实现自定义调试支持。mdb 调试程序还提供了许多可用功能,如命令行编辑、命令历史记录、输出页面调度程序和联机帮助。


注 –

不应再使用 adb 宏。该功能已被 mdb 中的 dcmd 替代。


mdb 调试程序提供了一组丰富的模块和 dcmd。借助这些工具,可以调试 Solaris 内核、任何关联的模块以及设备驱动程序。通过这些功能可以执行一些任务,如:

首先切换到崩溃目录,键入 mdb 并指定系统崩溃转储,如以下示例所示。


示例 22–9 针对崩溃转储调用 mdb


% cd /var/crash/testsystem
% ls
bounds     unix.0    vmcore.0
% mdb unix.0 vmcore.0
Loading modules: [ unix krtld genunix ufs_log ip usba s1394 cpc nfs ]
> ::status
debugging crash dump vmcore.0 (64-bit) from testsystem
operating system: 5.10 Generic (sun4u)
panic message: zero
dump content: kernel pages only

mdb> 提示符进行响应时,便可运行命令。

要检查实时系统中正在运行的内核,请按如下所示从系统提示符处运行 mdb


示例 22–10 针对正在运行的内核调用 mdb


# mdb -k
Loading modules: [ unix krtld genunix ufs_log ip usba s1394 ptm cpc ipc nfs ]
> ::status
debugging live kernel (64-bit) on testsystem
operating system: 5.10 Generic (sun4u)

使用 kmdbmdb 执行的有用调试任务

本节提供了有用的调试任务示例。除非特别说明,否则本节中的任务均可使用 mdbkmdb 来执行。本节假定您已了解 kmdbmdb 的基本使用知识。请注意,此处提供的信息取决于所使用系统的类型。这些示例是使用运行 64 位内核的 Sun Blade 100 工作站生成的。


注意 – 注意 –

由于修改内核结构中的数据会导致无法恢复的数据损毁,因此务必要格外谨慎。请勿修改或依赖于不属于 Solaris DDI 结构中的数据。有关属于 Solaris DDI 的结构的信息,请参见 Intro(9S) 手册页。


使用 kmdb 查找系统寄存器

kmdb 调试程序可按组或单独显示计算机寄存器。要按组显示所有寄存器,请按以下示例所示使用 $r


示例 22–11 使用 kmdb 读取 SPARC 处理器中的所有寄存器


[0]: $r

g0    0                                 l0      0
g1    100130a4      debug_enter         l1      edd00028
g2    10411c00      tsbmiss_area+0xe00  l2      10449c90
g3    10442000      ti_statetbl+0x1ba   l3      1b
g4    3000061a004                       l4      10474400     ecc_syndrome_tab+0x80
g5    0                                 l5      3b9aca00
g6    0                                 l6      0
g7    2a10001fd40                       l7      0
o0    0                                 i0      0
o1    c                                 i1      10449e50
o2    20                                i2      0
o3    300006b2d08                       i3      10
o4    0                                 i4      0
o5    0                                 i5      b0
sp    2a10001b451                       fp      2a10001b521
o7    1001311c      debug_enter+0x78    i7      1034bb24     zsa_xsint+0x2c4
y     0
tstate: 1604  (ccr=0x0, asi=0x0, pstate=0x16, cwp=0x4)
pstate: ag:0 ie:1 priv:1 am:0 pef:1 mm:0 tle:0 cle:0 mg:0 ig:0
winreg: cur:4 other:0 clean:7 cansave:1 canrest:5 wstate:14
tba   0x10000000
pc    edd000d8 edd000d8:        ta      %icc,%g0 + 125
npc   edd000dc edd000dc:        nop

调试程序会将每个寄存器值导出到与寄存器同名的一个变量中。如果读取该变量,则返回对应寄存器的当前值。如果写入该变量,则会更改关联的计算机寄存器值。以下示例将一台 x86 计算机上 %o0 寄存器的值由 0 更改为 1。


示例 22–12 使用 kmdb 读/写 x86 计算机中的寄存器


[0]> &lt;eax=K
        c1e6e0f0
[0]> 0>eax
[0]> &lt;eax=K
        0
[0]>  c1e6e0f0>eax

如果需要检查不同处理器的寄存器,则可使用 ::cpuregs dcmd。要检查的处理器的 ID 可以作为 dcmd 的地址或 -c 选项的值来提供,如以下示例所示。


示例 22–13 检查不同处理器的寄存器


[0]> 0::cpuregs
   %cs = 0x0158            %eax = 0xc1e6e0f0 kmdbmod`kaif_dvec
   %ds = 0x0160            %ebx = 0x00000000

以下示例从 SPARC 计算机上的处理器 0 切换到处理器 3。检查了寄存器 %g3,然后将其清除。为确认新值,再次读取 %g3


示例 22–14 从指定的处理器中检索单个寄存器值


[0]> 3::switch
[3]> <g3=K
        24
[3]> 0>g3
[3]> <g3
        0

检测内核内存泄漏

::findleaks dcmd 可对内核崩溃转储中的内存泄漏提供强大、有效的检测。必须启用一整套内核内存调试功能,::findleaks 才会有效。有关更多信息,请参见设置 kmem_flags 调试标志。在驱动程序开发和测试期间运行 ::findleaks,以检测泄漏内存从而浪费内核资源的代码。有关 ::findleaks 的完整讨论,请参见《Solaris 模块调试器指南》中的第 9  章 “使用内核内存分配器进行调试”


注 –

泄漏内核内存的代码会使系统容易受到拒绝服务攻击。


使用 mdb 编写调试程序命令

mdb 调试程序提供了一个功能强大的 API,用于实现为调试驱动程序而自定义的调试程序功能。《Solaris 模块调试器指南》详细介绍了该编程 API。

SUNWmdbdm 软件包将 mdb 源代码示例安装在目录 /usr/demo/mdb 中。可以使用 mdb 来自动完成冗长的调试日常事务,或帮助验证驱动程序是否正常工作。还可以将 mdb 调试模块与驱动程序产品一起打包。通过打包,服务人员可在客户站点处使用这些功能。

获取内核数据结构信息

Solaris 内核在可用 kmdbmdb 检查的结构中提供数据类型信息。


注 –

kmdbmdb dcmd 只能用于包含设计用于 mdb 的压缩符号调试信息的对象。此信息当前只能用于某些 Solaris 内核模块。必须安装 SUNWzlib 软件包,才能处理符号调试信息。


以下示例说明如何显示 scsi_pkt 结构中的数据。


示例 22–15 使用调试程序显示内核数据结构


> 7079ceb0::print -t 'struct scsi_pkt'
{
    opaque_t pkt_ha_private = 0x7079ce20
    struct scsi_address pkt_address = {
        struct scsi_hba_tran *a_hba_tran = 0x70175e68
        ushort_t a_target = 0x6
        uchar_t a_lun = 0
        uchar_t a_sublun = 0
    }
    opaque_t pkt_private = 0x708db4d0
    int (*)() *pkt_comp = sd_intr
    uint_t pkt_flags = 0
    int pkt_time = 0x78
    uchar_t *pkt_scbp = 0x7079ce74
    uchar_t *pkt_cdbp = 0x7079ce64
    ssize_t pkt_resid = 0
    uint_t pkt_state = 0x37
    uint_t pkt_statistics = 0
    uchar_t pkt_reason = 0
}

数据结构的大小在调试中很有用。使用 ::sizeof dcmd 可获取结构的大小,如以下示例所示。


示例 22–16 显示内核数据结构的大小


> ::sizeof struct scsi_pkt
sizeof (struct scsi_pkt) = 0x58

结构中特定成员的地址在调试中也很有用。有几种方法可用来确定成员的地址。

使用 ::offsetof dcmd 可以获取结构中给定成员的偏移,如以下示例所示。


示例 22–17 显示内核数据结构的偏移


> ::offsetof struct scsi_pkt pkt_state
offsetof (struct pkt_state) = 0x48

使用带 -a 选项的 ::print dcmd 可以显示结构中所有成员的地址,如以下示例所示。


示例 22–18 显示内核数据结构的相对地址


> ::print -a struct scsi_pkt
{
    0 pkt_ha_private
    8 pkt_address {
    ...
    }
    18 pkt_private
    ...
}

如果结合使用 ::print-a 选项来指定地址,则会显示每个成员的绝对地址。


示例 22–19 显示内核数据结构的绝对地址


> 10000000::print -a struct scsi_pkt
{
    10000000 pkt_ha_private
    10000008 pkt_address {
    ...
    }
    10000018 pkt_private
    ...
}

使用 ::print::sizeof::offsetof dcmd,可在驱动程序与 Solaris 内核交互时调试问题。


注意 – 注意 –

通过此功能可访问原始内核数据结构。您可以检查任何结构,无论该结构是否显示为 DDI 的一部分。因此,应避免依赖于未显式构成 DDI 的任何数据结构。



注 –

这些 dcmd 只能用于包含设计用于 mdb 的压缩符号调试信息的对象。符号调试信息当前只能用于某些 Solaris 内核模块。必须安装 SUNWzlib(32 位)或 SUNWzlibx(64 位)解压缩软件,才能处理符号调试信息。无论是否包含 SUNWzlibSUNWzlibx 软件包,kmdb 调试程序均可处理符号类型数据。


获取设备树信息

mdb 调试程序提供了用于显示内核设备树的 ::prtconf dcmd。::prtconf dcmd 的输出与 prtconf(1M) 命令的输出相似。


示例 22–20 使用 ::prtconf Dcmd


> ::prtconf
300015d3e08      SUNW,Sun-Blade-100
    300015d3c28      packages (driver not attached)
        300015d3868      SUNW,builtin-drivers (driver not attached)
        300015d3688      deblocker (driver not attached)
        300015d34a8      disk-label (driver not attached)
        300015d32c8      terminal-emulator (driver not attached)
        300015d30e8      obp-tftp (driver not attached)
        300015d2f08      dropins (driver not attached)
        300015d2d28      kbd-translator (driver not attached)
        300015d2b48      ufs-file-system (driver not attached)
    300015d3a48      chosen (driver not attached)
    300015d2968      openprom (driver not attached)

可以使用宏(如 ::devinfo dcmd)来显示节点,如以下示例所示。


示例 22–21 显示单个节点的设备信息


> 300015d3e08::devinfo
300015d3e08      SUNW,Sun-Blade-100
        System properties at 0x300015abdc0:
            name='relative-addressing' type=int items=1
                value=00000001
            name='MMU_PAGEOFFSET' type=int items=1
                value=00001fff
            name='MMU_PAGESIZE' type=int items=1
                value=00002000
            name='PAGESIZE' type=int items=1
                value=00002000
        Driver properties at 0x300015abe00:
            name='pm-hardware-state' type=string items=1
                value='no-suspend-resume'

使用 ::prtconf 可以查看驱动程序在设备树中连接的位置,以及显示设备属性。还可以为 ::prtconf 指定详细 (-v) 标志,以显示每个设备节点的属性,如下所示。


示例 22–22 在详细模式下使用 ::prtconf Dcmd


> ::prtconf -v
DEVINFO          NAME
300015d3e08      SUNW,Sun-Blade-100
        System properties at 0x300015abdc0:
            name='relative-addressing' type=int items=1
                value=00000001
            name='MMU_PAGEOFFSET' type=int items=1
                value=00001fff
            name='MMU_PAGESIZE' type=int items=1
                value=00002000
            name='PAGESIZE' type=int items=1
                value=00002000
        Driver properties at 0x300015abe00:
            name='pm-hardware-state' type=string items=1
                value='no-suspend-resume'
        ...
        300015ce798      pci10b9,5229, instance #0
                Driver properties at 0x300015ab980:
                    name='target2-dcd-options' type=any items=4
                        value=00.00.00.a4
                    name='target1-dcd-options' type=any items=4
                        value=00.00.00.a2
                    name='target0-dcd-options' type=any items=4
                        value=00.00.00.a4

另一种查找驱动程序实例的方法是使用 ::devbindings dcmd。在给定驱动程序名称的情况下,该命令会显示指定驱动程序的所有实例的列表,如以下示例所示。


示例 22–23 使用 ::devbindings Dcmd 查找驱动程序实例


> ::devbindings dad
300015ce3d8      ide-disk (driver not attached)
300015c9a60      dad, instance #0
        System properties at 0x300015ab400:
            name='lun' type=int items=1
                value=00000000
            name='target' type=int items=1
                value=00000000
            name='class_prop' type=string items=1
                value='ata'
            name='type' type=string items=1
                value='ata'
            name='class' type=string items=1
                value='dada'
...
300015c9880      dad, instance #1
        System properties at 0x300015ab080:
            name='lun' type=int items=1
                value=00000000
            name='target' type=int items=1
                value=00000002
            name='class_prop' type=string items=1
                value='ata'
            name='type' type=string items=1
                value='ata'
            name='class' type=string items=1
                value='dada'

检索驱动程序软状态信息

调试驱动程序的常见问题是检索特定驱动程序实例的软状态。软状态使用 ddi_soft_state_zalloc(9F) 例程来分配。驱动程序可以通过 ddi_get_soft_state(9F) 获取软状态。软状态指针的名称是 ddi_soft_state_init (9F) 的第一个参数。根据名称,可以使用 mdb 通过 ::softstate dcmd 检索特定驱动程序实例的软状态:


> *bst_state::softstate 0x3
702b7578

在此示例中,::softstate 用来获取 bst 示例驱动程序的实例 3 的软状态。此指针引用由驱动程序使用的 bst_soft 结构,以便跟踪该实例的状态。

修改内核变量

可以使用 kmdbmdb 来修改内核变量或其他内核状态。使用 mdb 修改内核状态时要格外谨慎,因为 mdb 在进行修改前不会停止内核。使用 kmdb 可以原子方式进行成组修改,因为 kmdb 会在允许用户访问之前停止内核。mdb 调试程序只能进行单个原子修改。

务必要使用正确的格式指示符来进行修改。格式可以为:

使用 ::sizeof dcmd 可以确定要修改的变量的大小。

以下示例使用值 0x80000000 覆写 moddebug 的值。


示例 22–24 使用调试程序修改内核变量


> moddebug/W 0x80000000
    moddebug:       0 = 0x80000000

调优驱动程序

Solaris OS 提供了内核统计信息结构,以便针对驱动程序实现计数器。使用 DTrace 功能可以实时分析性能。本节介绍有关设备性能的以下主题:

内核统计信息

为了协助进行性能调优,Solaris 内核提供了 kstat(3KSTAT) 功能。kstat 功能提供了一套函数和数据结构,以供设备驱动程序和其他内核模块导出特定于模块的内核统计信息。

kstat 是用于记录设备使用情况的可计量方面的数据结构。kstat 存储为以 null 终止的链接列表。每个 kstat 都有一个通用的头区和一个特定于类型的数据区。头区由 kstat_t 结构定义。

Sun Developer Network 上的文章 "Using kstat From Within a Program in the Solaris OS"(从 Solaris OS 的程序中使用 kstat)(URL 为 http://developers.sun.com/solaris/articles/kstat_api.html)提供了有关如何使用 kstat(3KSTAT) 和 libkstat(3LIB) API 从 Solaris OS 中提取度量的两个实用示例。这两个示例分别是 "Walking Through All the kstat"(遍历所有 kstat)和 "Getting NIC kstat Output Using the Java Platform"(使用 Java 平台获取 NIC kstat 输出)。

内核统计信息结构成员

kstat 结构的成员包括:

ks_class[KSTAT_STRLEN]

kstat 类型分类为 buscontrollerdevice_errordiskhatkmem_cachekstatmiscnetnfspagespartitionrpsufsvmvmem

ks_crtime

kstat 的创建时间。ks_crtime 通常用于计算各个计数器的速率。

ks_data

指向 kstat 的数据区。

ks_data_size

数据区大小总额(以字节为单位)。

ks_instance

创建此 kstat 的内核模块的实例。ks_instanceks_moduleks_name 结合使用,以便为 kstat 指定唯一且有意义的名称。

ks_kid

kstat 的唯一 ID。

ks_module[KSTAT_STRLEN]

标识创建此 kstat 的内核模块。ks_module ks_instanceks_name 结合使用,以便为 kstat 指定唯一且有意义的名称。KSTAT_STRLEN 可设置 ks_module 的最大长度。

ks_name[KSTAT_STRLEN]

为 kstat 指定的名称,与 ks_moduleks_instance 结合使用。KSTAT_STRLEN 可设置 ks_module 的最大长度。

ks_ndata

为以下可支持多个记录的 kstat 类型指示数据记录的个数: KSTAT_TYPE_RAWKSTAT_TYPE_NAMEDKSTAT_TYPE_TIMER

ks_next

指向链表中的下一个 kstat。

ks_resv

保留的字段。

ks_snaptime

上一数据快照的时间标记,在计算速率时很有用。

ks_type

数据类型,对于二进制数据可为 KSTAT_TYPE_RAW,对于名称/值对可为 KSTAT_TYPE_NAMED,对于中断统计信息可为 KSTAT_TYPE_INTR,对于 I/O 统计信息可为 KSTAT_TYPE_IO,对于事件计时器可为 KSTAT_TYPE_TIMER

内核统计信息结构

不同种类的 kstat 的结构如下:

kstat(9S)

由设备驱动程序导出的每条内核统计信息 (kstat) 都由头区和数据区构成。kstat(9S) 结构是统计信息的头部分。

kstat_intr(9S)

中断 kstat 的结构。中断类型包括:

  • 硬中断-源自硬件设备自身

  • 软中断-因系统使用某些系统中断源而引起

  • 监视程序中断-由定期计时器调用引起

  • 虚假中断-输入了中断入口点,但没有需要提供服务的中断

  • 多个服务-在从任何其他类型返回之前检测到中断并提供了服务

驱动程序通常只报告从其处理程序中声明的硬中断和软中断,但度量虚假类中断对自动向量化的设备很有用,以便查找特定系统配置中的任何中断延迟信息。具有多个相同类型中断的设备应使用多个结构。

kstat_io(9S)

I/O kstat 的结构。

kstat_named(9S)

命名的 kstat 的结构。命名的 kstat 是名称-值对数组。这些对位于 kstat_named 结构中。

内核统计信息函数

用于使用 kstat 的函数包括:

kstat_create(9F)

分配和初始化 kstat(9S) 结构。

kstat_delete(9F)

从系统中移除 kstat。

kstat_install(9F)

向系统中添加完全初始化的 kstat。

kstat_named_init(9F)kstat_named_setstr(9F)

初始化已命名的 kstat。kstat_named_setstr()str(一个字符串)与已命名的 kstat 指针相关联。

kstat_queue(9F)

许多 I/O 子系统都至少有两个基本的事务队列要管理。一个队列用于已接受但尚未开始处理的事务。另一个队列用于正在进行处理但尚未处理完的事务。因此,保留了两个累积时间统计量:等待时间运行时间。等待时间是提供服务之前的时间。运行时间是提供服务期间的时间。kstat_queue() 函数系列可根据驱动程序等待队列和运行队列之间的转换来管理这些时间:

Solaris 以太网驱动程序的内核统计信息

下表中介绍的 kstat 接口是从驱动程序中获取以太网物理层统计信息的有效方法。以太网驱动程序应导出这些统计信息,以指导用户更好地诊断和修复以太网物理层问题。除 link_up 之外,所有统计信息在未提供时的缺省值均为 0。应将 link_up 统计信息的值假定为 1。

以下示例给出了所有共享的链路设置。在这种情况下,可使用 mii 来过滤统计信息。

kstat ce:0:mii:link_*
表 22–2 以太网 MII/GMII 物理层接口内核统计信息

Kstat 变量 

类型 

说明 

xcvr_addr

KSTAT_DATA_UINT32

提供当前正在使用的收发器的 MII 地址。 

  • (0) - (31) 用于给定以太网设备的物理层设备的 MII 地址。

  • 在没有可从外部访问的 MII 接口,因此 MII 地址不明确或不相关的情况下,使用 (-1)。

xcvr_id

KSTAT_DATA_UINT32

提供当前正在使用的收发器的特定供应商 ID 或设备 ID。 

xcvr_inuse

KSTAT_DATA_UINT32

指示当前正在使用的收发器的类型。IEEE aPhytType 枚举以下集合:

  • (0) 其他未定义

  • (1) 不存在 MII 接口,但未连接任何收发器

  • (2) 10 Mb/s Clause 7 10 Mb/s Manchester

  • (3) 100BASE-T4 Clause 23 100 Mb/s 8B/6T

  • (4) 100BASE-X Clause 24 100 Mb/s 4B/5B

  • (5) 100BASE-T2 Clause 32 100 Mb/s PAM5X5

  • (6) 1000BASE-X Clause 36 1000 Mb/s 8B/10B

  • (7) 1000BASE-T Clause 40 1000 Mb/s 4D-PAM5

此集合比 ifMauType 指定的集合小,后者定义为包括上述所有类型及其半双工/全双工选件。由于 cap_* 统计信息可提供此信息,因此可从 xcvr_inusecap_* 的组合中派生缺少的定义,以提供 ifMayType 的所有组合。

cap_1000fdx

KSTAT_DATA_CHAR

指示设备支持 1 Gb/s 的全双工传输。 

cap_1000hdx

KSTAT_DATA_CHAR

指示设备支持 1 Gb/s 的半双工传输。 

cap_100fdx

KSTAT_DATA_CHAR

指示设备支持 100 Mb/s 的全双工传输。 

cap_100hdx

KSTAT_DATA_CHAR

指示设备支持 100 Mb/s 的半双工传输。 

cap_10fdx

KSTAT_DATA_CHAR

指示设备支持 10 Mb/s 的全双工传输。 

cap_10hdx

KSTAT_DATA_CHAR

指示设备支持 10 Mb/s 的半双工传输。 

cap_asmpause

KSTAT_DATA_CHAR

指示设备支持非对称暂停以太网流量控制。 

cap_pause

KSTAT_DATA_CHAR

指示当 cap_pause 设置为 1,cap_asmpause 设置为 0 时,设备支持对称暂停以太网流量控制。当 cap_asmpause 设置为 1 时,cap_pause 具有以下含义:

  • cap_pause = 0 基于接收拥塞传送暂停。

  • cap_pause = 1 接收暂停并减慢传送,以避免拥塞。

cap_rem_fault

KSTAT_DATA_CHAR

指示设备支持远程故障指示。 

cap_autoneg

KSTAT_DATA_CHAR

指示设备支持自动协商。 

adv_cap_1000fdx

KSTAT_DATA_CHAR

指示设备正在通告支持 1 Gb/s 的全双工传输。 

adv_cap_1000hdx

KSTAT_DATA_CHAR

指示设备正在通告支持 1 Gb/s 的半双工传输。 

adv_cap_100fdx

KSTAT_DATA_CHAR

指示设备正在通告支持 100 Mb/s 的全双工传输。 

adv_cap_100hdx

KSTAT_DATA_CHAR

指示设备正在通告支持 100 Mb/s 的半双工传输。 

adv_cap_10fdx

KSTAT_DATA_CHAR

指示设备正在通告支持 10 Mb/s 的全双工传输。 

adv_cap_10hdx

KSTAT_DATA_CHAR

指示设备正在通告支持 10 Mb/s 的半双工传输。 

adv_cap_asmpause

KSTAT_DATA_CHAR

指示设备正在通告支持非对称暂停以太网流量控制。 

adv_cap_pause

KSTAT_DATA_CHAR

指示当 adv_cap_pause 设置为 1,adv_cap_asmpause 设置为 0 时,设备正在通告支持对称暂停以太网流量控制。当 adv_cap_asmpause 设置为 1 时,adv_cap_pause 具有以下含义:

  • adv_cap_pause = 0 基于接收拥塞传送暂停。

  • adv_cap_pause = 1 接收暂停并减慢传送,以避免拥塞。

adv_rem_fault

KSTAT_DATA_CHAR

指示设备遇到故障,设备将把该故障转发给链路合作伙伴。 

adv_cap_autoneg

KSTAT_DATA_CHAR

指示设备正在通告支持自动协商。 

lp_cap_1000fdx

KSTAT_DATA_CHAR

指示链路合作伙伴设备支持 1 Gb/s 的全双工传输。 

lp_cap_1000hdx

KSTAT_DATA_CHAR

指示链路合作伙伴设备支持 1 Gb/s 的半双工传输。 

lp_cap_100fdx

KSTAT_DATA_CHAR

指示链路合作伙伴设备支持 100 Mb/s 的全双工传输。 

lp_cap_100hdx

KSTAT_DATA_CHAR

指示链路合作伙伴设备支持 100 Mb/s 的半双工传输。 

lp_cap_10fdx

KSTAT_DATA_CHAR

指示链路合作伙伴设备支持 10 Mb/s 的全双工传输。 

lp_cap_10hdx

KSTAT_DATA_CHAR

指示链路合作伙伴设备支持 10 Mb/s 的半双工传输。 

lp_cap_asmpause

KSTAT_DATA_CHAR

指示链路合作伙伴设备支持非对称暂停以太网流量控制。 

lp_cap_pause

KSTAT_DATA_CHAR

指示当 lp_cap_pause 设置为 1,lp_cap_asmpause 设置为 0 时,链路合作伙伴设备支持对称暂停以太网流量控制。当 lp_cap_asmpause 设置为 1 时,lp_cap_pause 具有以下含义:

  • lp_cap_pause = 0 链路合作伙伴将基于接收拥塞传送暂停。

  • lp_cap_pause = 1 链路合作伙伴将接收暂停并减慢传送,以避免拥塞。

lp_rem_fault

KSTAT_DATA_CHAR

指示链路合作伙伴遇到链路故障。 

lp_cap_autoneg

KSTAT_DATA_CHAR

指示链路合作伙伴设备支持自动协商。 

link_asmpause

KSTAT_DATA_CHAR

指示链路正采用非对称暂停以太网流量控制来运行。 

link_pause

KSTAT_DATA_CHAR

指示暂停功能的精度。指示当 link_pause 设置为 1,link_asmpause 设置为 0 时,链路正采用对称暂停以太网流量控制来运行。当 link_asmpause 设置为 1 且相对于链路的本地视图时,link_pause 具有以下含义:

  • link_pause = 0 此站将基于接收拥塞来传送暂停。

  • link_pause = 1 此站将接收暂停并减慢传送,以避免拥塞。

link_duplex

KSTAT_DATA_CHAR

指示链路双工模式。 

  • link_duplex = 0 链路关闭,且双工模式未知。

  • link_duplex = 1 链路打开,且处于半双工模式。

  • link_duplex = 2 链路打开,且处于全双工模式。

link_up

KSTAT_DATA_CHAR

指示链路是打开还是关闭。 

  • link_up = 0 链路关闭。

  • link_up = 1 链路打开。

用于动态检测过程的 DTrace

DTrace 是一种全面的动态跟踪工具,用于检查用户程序和操作系统自身的行为。通过 DTrace,可以收集环境中处于关键位置(称为探测器)的数据。通过 DTrace 可以记录栈跟踪、时间标记、函数的参数或探测器触发频率计数等数据。由于 DTrace 允许动态插入探测器,因此无需重新编译代码。有关 DTrace 的更多信息,请参见《Solaris 动态跟踪指南》《DTrace User Guide 》DTrace BigAdmin System Administration Portal(DTrace BigAdmin 系统管理门户网站)包含许多指向有关 DTrace 的文章、XPert 会话和其他信息的链接。

第 23 章 推荐的编码方法

本章介绍如何编写强健的驱动程序。根据本章中所讨论的原则编写的驱动程序更易于进行调试。建议的做法还可在出现硬件和软件故障时为系统提供保护。

本章介绍有关以下主题的信息:

调试准备方法

由于以下原因,驱动程序代码比用户程序更难调试:

请确保您的驱动程序可以支持调试。此支持便于进行维护工作和未来的开发工作。

使用唯一前缀来避免内核符号冲突

每个函数、数据元素和驱动程序预处理程序定义的名称必须对每个驱动程序都唯一。

驱动程序模块将链接到内核。对特定驱动程序唯一的每个符号名称不得与其他内核符号冲突。为避免这种冲突,特定驱动程序的每个函数和数据元素的名称必须带有该驱动程序共有的前缀。该前缀必须足以让每个驱动程序符号的名称保持唯一。通常,该前缀是驱动程序的名称,或者是驱动程序名称的缩写。例如,xx_open() 是驱动程序 xx 的 open(9E) 例程的名称。

在构建驱动程序时,驱动程序一定包含许多系统头文件。这些头文件中的全局可见名称无法预测。为避免与这些名称产生冲突,必须使用一个标识前缀为每个驱动程序预处理程序定义指定唯一的名称。

在进行错误诊断时,还可以借助唯一的驱动程序符号前缀来解读系统日志和故障消息。您看到的将是与 xx_attach() 有关的错误消息,而不是与二义性 attach() 函数有关的错误。

使用 cmn_err() 记录驱动程序活动

使用 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() 捕捉无效假设

断言是活动文档一种极有价值的形式。ASSERT(9F) 的语法如下:

void ASSERT(EXPRESSION)

如果预期为 true 的条件实际为 false,则 ASSERT() 宏会停止执行内核。ASSERT() 为程序员提供了对某段代码所做的假设进行验证的方法。

仅当定义了 DEBUG 编译符号时,才会定义 ASSERT() 宏。如果未定义 DEBUGASSERT() 宏将无效。

以下示例断言将测试特定指针值不是 NULL 的假设:

ASSERT(ptr != NULL);

如果驱动程序已使用 DEBUG 进行编译,并且执行至此时 ptr 的值为 NULL,则在控制台上会列显以下故障消息:

panic: assertion failed: ptr != NULL, file: driver.c, line: 56

注 –

由于 ASSERT(9F) 使用 DEBUG 编译符号,因此任何条件调试代码也应使用 DEBUG


使用 mutex_owned() 验证和记录锁定要求

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 可能会生成很难跟踪的错误。

要防止出现难懂的错误,必须正确使用 volatilevolatile 关键字指示编译器对已声明的对象使用精确语义,特别指示不能删除或重新排序对对象的访问。设备驱动程序必须使用 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 了。

可维护性

为了确保可维护性,必须使驱动程序可以执行以下操作:

定期运行状况检查

潜在故障是在其他某个操作发生后才显现的故障。例如,冷待机设备中出现的硬件故障在主设备出现故障之前无法检测到。此时,系统包含两个有缺陷的设备,并且可能无法继续运行。

未检测到的潜在故障往往最终会导致系统故障。如果不执行潜在故障检查,冗余系统的整体可用性将受到危害。为避免出现这种情况,设备驱动程序必须检测潜在故障,并以与报告其他故障的方法相同的方法来报告这些故障。

应为驱动程序提供对设备进行定期运行状况检查的机制。在容错情况(其中,设备可以是辅助设备或故障转移设备)下,较早地检测到发生故障的辅助设备是确保在主设备出现故障前可以修复或更换辅助设备所必需的。

定期运行状况检查可用来执行以下活动: