编写设备驱动程序

第 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》