Writing Device Drivers

Chapter 17 Compiling, Loading, Packaging, and Testing Drivers

This chapter describes the procedure for driver development, including code layout, compilation, packaging, and testing. The chapter provides information on the following subjects:

Driver Code Layout Structure

The code for a device driver is usually divided into the following files:

Header Files

Header files define:

Some of the header file definitions, such as the state structure, might be needed only by the device driver. This information should go in private header files that are only included by the device driver itself.

Any information that an application might require, such as the I/O control commands, should be in public header files. These are included by the driver and any applications that need information about the device.

While there is no standard for naming private and public files, one convention is to name the private header file xximpl.h and the public header file xxio.h.

.c Files

A .c file for a device driver contains the data declarations and the code for the entry points of the driver. It contains the #include statements the driver needs, declares extern references, declares local data, sets up the cb_ops and dev_ops structures, declares and initializes the module configuration section (the modlinkage(9S) and modldrv(9S) structures), makes any other necessary declarations, and defines the driver entry points.

driver.conf Files

driver.conf files are required for devices that are not self-identifying. Entries in the driver.conf file specify possible device instances the driver will probe for existence. For more information, see the scsi(4) and driver.conf(4) man pages. Driver global properties can also be set by entries in the driver.conf file. driver.conf files are optional for self-identifying devices (SID), where the entries can be used to add properties into SID nodes. See the sbus(4) and pci(4) man pages for details.

Preparing for Driver Installation

Before the driver is installed, it must be compiled into a binary, and a configuration file created, if necessary. The driver's module name must either match the name of the device nodes, or the system must be informed through add_drv(1M) or update_drv(1M) that this driver should manage other names.

The system maintains a one-to-one association between the name of the driver module and the name of the dev_info node. For example, a dev_info node for a device named wombat is handled by a driver module called wombat in a subdirectory called drv (resulting in drv/wombat if a 32–bit kernel or drv/sparcv9/wombat if 64–bit) found in the module path.

If the driver is a STREAMS network driver, then the driver name needs to meet the following constraints:

If the driver should manage dev_info nodes with different names, the add_drv(1M) utility can create aliases. The -i flag specifies the names of other dev_info nodes that the driver handles. The update_drv() function can also modify aliases for an installed device driver.

Compiling and Linking the Driver

Compile each driver source file and link the resulting object files into a driver module. The example below shows a driver called xx that has two C-language source files and generates a driver module xx. The driver created in this example is intended for the 32–bit kernel:

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

The _KERNEL symbol must be defined while compiling kernel (driver) code. No other symbols (such as sun4m) should be defined, aside from driver private symbols. DEBUG can also be defined to enable any calls to assert(9F). There is no need to use the -I flag for the standard headers.

Drivers intended for the 64-bit SPARC kernel should specify the -xarch=v9 option. Use the following compile line:

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

After the driver is stable, optimization flags can be used to build a production quality driver. For the Sun WorkShop™ Compilers C, the normal -O flag, or its equivalent -xO3, can be used. Note that -xO3 is the highest level of optimization device drivers should use (see cc(1)).

The following compile line was used to create 64–bit SPARC drivers provided with the Solaris 9 operating environment:

% cc -D_KERNEL -xcg92 -xarch=v9 -xcode=abs32 -xO3 -c xx1.c

Where -xcg92 refers to the code generator, and the use of -xcode=abs32 leads to more compact code.


Note –

Running ld -r is necessary even if there is only one object module.


Module Dependencies

If the driver module depends on symbols exported by another kernel module, the dependency can be specified by the -dy and -N options of ld. If the driver depends on a symbol exported by misc/foo, the example below should be used to create the driver binary. See the ld(1) man page.

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

Writing a Hardware Configuration File

If the device is non-self-identifying, the kernel requires a hardware configuration file for it. If the driver is called xx, the hardware configuration file for it should be called xx.conf. See the driver.conf(4), pseudo(4), sbus(4), scsi(4), and update_drv(1M) man pages for more information on hardware configuration files. On the Intel platform, device information is now supplied by the booting system. Hardware configuration files should no longer be needed, even for non-self-identifying devices.

Arbitrary properties can be defined in hardware configuration files by adding entries of the form property=value, where property is the property name, and value is its initial value. This enables devices to be configured by changing the property values.

Installing, Updating, and Removing Drivers

Before a driver can be used, the system must be informed that it exists. The add_drv(1M) utility must be used to correctly install the device driver. After the driver is installed, it can be loaded and unloaded from memory without using add_drv(1M) again.

Copying the Driver to a Module Directory

A device driver's module path (location) depends on the platform it runs on, the architecture it is compiled for, and whether it is needed at boot time. Platform-dependent device drivers reside in the following locations:

Platform-independent drivers reside in either of these directories:

To install a 32–bit driver, the driver and its configuration file must be copied to a drv directory in the module path. For example, to copy a driver to /usr/kernel/drv, type:

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

To install a 64-bit SPARC driver, copy the driver to a drv/sparcv9 directory in the module path. Copy the driver configuration file to the drv directory in the module path. For example, to copy a driver to /usr/kernel/drv, type:

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

Note –

All driver configuration files (.conf files) must go in the drv directory in the module path. Even on 64–bit systems, the .conf file goes in the drv directory, not the drv/sparcv9 directory.


Installing Drivers with add_drv()

Run add_drv(1M) to install the driver in the system. If the driver installs successfully, add_drv(1M) will run devfsadm(1M) to create the logical names in /dev.

# add_drv xx

This is a simple case in which the device identifies itself as xx and the device special files will have default ownership and permissions (0600 root sys). add_drv(1M) also allows additional names for the device (aliases) to be specified. See add_drv(1M) to determine how to add aliases and set file permissions explicitly.


Note –

add_drv(1M) should not be run when installing a STREAMS module. See the STREAMS Programming Guide for details.


If the driver creates minor nodes that do not represent disks, tapes, or ports (terminal devices), /etc/devlink.tab can be modified to cause devfsadm(1M) to create logical device names in /dev.

Alternatively, logical names can be created by a program run at driver installation time.

Updating Driver Information

Use the update_drv(1M) command to notify the system of any changes to an installed device driver. By default, the system re-reads the driver.conf(4) file and reloads the driver binary module.

Removing the Driver

To remove a driver from the system, use rem_drv(1M), then delete the driver module and configuration file from the module path. The driver cannot be used again until it is reinstalled with add_drv(1M). Removing a SCSI HBA driver will require a reboot to take effect.

Loading and Unloading Drivers

Opening a special file associated with the device driver causes the driver to be loaded. modload(1M) can also be used to load the driver into memory, but it does not call any routines in the module. Opening the device is the preferred method.

Normally, the system automatically unloads device drivers when they are no longer in use. During development, it might be necessary to use modunload(1M) to unload the driver explicitly. In order for modunload(1M) to be successful, the device driver must be inactive; there must be no outstanding references to the device, such as through open(2) or mmap(2).

modunload takes a runtime-dependentmodule_id as an argument. The module_id can be found by using grep to search the output of modinfo for the driver name in question and looking at the first column.

# modunload -i module_id

To unload all currently unloadable modules, specify module ID zero:

# modunload -i 0

In addition to being inactive, the driver must have working detach(9E) and _fini(9E) routines for modunload(1M) to succeed.

Driver Packaging

The normal delivery vehicle for software is to create a package with all the components of the software packaged together. A package provides a controlled mechanism for installation and removal of all the components of a software product. In addition to the files for using the product, the package includes control files for installing (and de-installing) the application. The postinstall and preremove installation scripts are two such control files.

Package Postinstall

After installing a package that includes a driver binary onto a system, the add_drv(1M) command must be run to complete the installation of the driver. Typically, add_drv is run a postinstall script as in the following example.

#!/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

Package Preremove

When removing a package that includes a driver, the rem_drv(1M) command must be run prior to removing the driver binary and other components. Here is an example preremove script that uses rem_drv(1M) for driver removal.

#!/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

Testing Drivers

Once a device driver is functional, it should be thoroughly tested before it is distributed. In addition to testing traditional UNIX device driver features, Solaris 9 drivers require testing of power management features, such as dynamic loading and unloading of drivers.

Configuration Testing

A driver's ability to handle multiple device configurations is an important part of the test process. Once the driver is working on a simple, or default, configuration, additional configurations should be tested. Depending upon the device, this can be accomplished by changing jumpers or DIP switches. If the number of possible configurations is small, all of them should be tried. If the number is large, various classes of possible configurations should be defined, and a sampling of configurations from each class should be tested. The designation of such classes depends on how the different configuration parameters might interact, which in turn depends on the device and on how the driver was written.

For each device configuration, the basic functions must be tested, which include loading, opening, reading, writing, closing, and unloading the driver. Any function that depends upon the configuration deserves special attention. For example, changing the base memory address of device registers is not likely to affect the behavior of most driver functions; if the driver works well with one address, it is likely to work as well with a different address, provided the configuration code enables it to work at all. On the other hand, a special I/O control call might have different effects depending upon the particular device configuration.

Loading the driver with varying configurations ensures that the probe(9E) and attach(9E) entry points can find the device at different addresses. For basic functional testing, using regular UNIX commands such as cat(1) or dd(1M) is usually sufficient for character devices. Mounting or booting might be required for block devices.

Functionality Testing

After a driver has been completely tested for configuration, all of its functionality should be thoroughly tested. This requires exercising the operation of all the driver's entry points.

Many drivers will require custom applications to test functionality, but basic drivers for devices such as disks, tapes, or asynchronous boards can be tested using standard system utilities. All entry points should be tested in this process, including devmap(9E), chpoll(9E), and ioctl(9E), if applicable. The ioctl(9E) tests might be quite different for each driver, and for nonstandard devices, a custom testing application will be required.

Error Handling

A driver might perform correctly in an ideal environment, but fail to handle cases where a device encounters an error or an application specifies erroneous operations or sends bad data to the driver. Therefore, an important part of driver testing is the testing of its error handling.

All possible error conditions of a driver should be exercised, including error conditions for actual hardware malfunctions. Some hardware error conditions might be difficult to induce, but an effort should be made to cause them or to simulate them if possible. All of these conditions could be encountered in the field. Cables should be removed or loosened, boards should be removed, and erroneous user application code should be written to test those error paths.


Caution – Caution –

Be sure to take proper electrical precautions when testing.


Testing Loading and Unloading

Because a driver that will not load or unload can force unscheduled downtime, loading and unloading must be thoroughly tested.

A script like the following should suffice:

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

Stress, Performance, and Interoperability Testing

To help ensure that the driver performs well, it should be subjected to vigorous stress testing. Running single threads through a driver will not test any of the locking logic and might not test condition variable waits. Device operations should be performed by multiple processes at once to cause several threads to execute the same code simultaneously. The way to do this depends upon the driver; some drivers will require special testing applications, but starting several UNIX commands in the background will be suitable for others. Appropriate testing depends upon where the particular driver uses locks and condition variables. Testing a driver on a multiprocessor machine is more likely to expose problems than testing on a single-processor machine.

Interoperability between drivers must also be tested, particularly because different devices can share interrupt levels. If possible, configure another device at the same interrupt level as the one being tested. Then stress-test the driver to determine if it correctly claims its own interrupts and otherwise operates according to expectations. Stress tests should be run on both devices at once. Even if the devices do not share an interrupt level, this test can still be valuable; for example, if serial communication devices start to experience errors while a network driver is being tested, this could indicate that the network driver is causing the rest of the system to encounter interrupt latency problems.

Driver performance under these stress tests should be measured using UNIX performance-measuring tools. This can be as simple as using the time(1) command along with commands used for stress tests.

DDI/DKI Compliance Testing

To ensure compatibility with later releases and reliable support for the current release, every driver should be Solaris 9 DDI/DKI compliant. One way to determine if the driver is compliant is by inspection. The driver can be visually inspected to ensure that only kernel routines and data structures specified in man pages section 9F: DDI and DKI Kernel Functions and man pages section 9S: DDI and DKI Data Structures of the Solaris 8 Reference Manual Collection are used.

The Solaris 9 Driver Developer Kit (DDK) includes a DDI compliance tool (DDICT) that checks device driver C source code for non-DDI/DKI compliance and issues either error or warning messages when it finds non-compliant code. For best results, all drivers should be written to pass DDICT. For more information, check out the Solaris Developer Connection, which is currently at www.sun.com/solaris/ddk.

Installation and Packaging Testing

Drivers are delivered to customers in packages. A package can be added and removed from the system using a standard mechanism (see the Application Packaging Guide).

Testing should be performed to ensure that the end user can add it to and remove it from a system. In testing, the package should be installed and removed from every type of media on which it will be released and on several system configurations. Packages must not make unwarranted assumptions about the directory environment of the target system. Certain valid assumptions, however, can be made about where standard kernel files are kept. Also test adding and removing of packages on newly installed machines that have not been modified for a development environment. A common packaging error is for a package to use a tool or file that exists only in a development environment, or only on the driver writer's own development system. For example, no tools from Source Compatibility package, SUNWscpu, should be used in driver installation programs.

The driver installation must be tested on a minimal Solaris system without any of the optional packages installed.

Testing Specific Types of Drivers

Because each type of device is different, it is difficult to describe how to test them all specifically. This section provides some information about how to test certain types of standard devices.

Tape Drivers

Tape drivers should be tested by performing several archive and restore operations. The cpio(1) and tar(1) commands can be used for this purpose. The dd(1M) command can be used to write an entire disk partition to tape, which can then be read back and written to another partition of the same size, and the two copies compared. The mt(1) command will exercise most of the I/O controls that are specific to tape drivers (see mtio(7I)); all the options should be attempted. The error handling of tape drivers can be tested by attempting various operations with the tape removed, attempting writes with the write protect on, and removing power during operations. Tape drivers typically implement exclusive-access open(9E) calls, which should be tested by having a second process try to open the device while a first process already has it open.

Disk Drivers

Disk drivers should be tested in both the raw and block device modes. For block device tests, a new file system should be created on the device and mounted. Multiple file operations can be performed on the device at this time.


Note –

The file system uses a page cache, so reading the same file over and over again will not really exercise the driver. The page cache can be forced to retrieve data from the device by memory-mapping the file (with mmap(2)), and using msync(3C) to invalidate the in-memory copies.


Another (unmounted) partition of the same size can be copied to the raw device and then commands such as fsck(1M) can be used to verify the correctness of the copy. The new partition can also be mounted and compared to the old one on a file-by-file basis.

Asynchronous Communication Drivers

Asynchronous drivers can be tested at the basic level by setting up a login line to the serial ports. A good test is if a user can log in on this line. To sufficiently test an asynchronous driver, however, all the I/O control functions must be tested, with many interrupts at high speed. A test involving a loopback serial cable and high data transfer rates will help determine the reliability of the driver. Running uucp(1C) over the line also provides some exercise; however, since uucp(1C) performs its own error handling, verify that the driver is not reporting excessive numbers of errors to the uucp(1C) process.

These types of devices are usually STREAMS based (see STREAMS Programming Guide).

Network Drivers

Network drivers can be tested using standard network utilities. ftp(1) and rcp(1) are useful because the files can be compared on each end of the network. The driver should be tested under heavy network loading, so that various commands can be run by multiple processes. Heavy network loading means:

Network cables should be unplugged while the tests are executing to ensure that the driver recovers gracefully from the resulting error conditions. Another important test is for the driver to receive multiple packets in rapid succession (back-to-back packets). In this case, a relatively fast host on a lightly loaded network should send multiple packets in quick succession to the test machine. Verify that the receiving driver does not drop the second and subsequent packets.

These types of devices are usually STREAMS-based (see STREAMS Programming Guide).