Device Driver Tutorial

Chapter 1 Introduction to Device Drivers

This chapter gives an overview of the Solaris Operating System and kernel. This chapter also gives an overview of the driver development environment and the development tools available to you.

Solaris Operating System Definition

The Solaris Operating System (Solaris OS) is implemented as an executable file that runs at boot time. The Solaris OS is referred to as the kernel. The kernel contains all of the routines that are necessary for the system to run. Because the kernel is essential for the running of the machine, the kernel runs in a special, protected mode that is called kernel mode. In contrast, user-level applications operate in a restricted mode called user mode that has no access to kernel instructions or to the kernel address space. Device drivers run in kernel mode and are prevented from directly accessing processes in user mode.

Kernel Overview

The kernel manages the system resources, including file systems, processes, and physical devices. The kernel provides applications with system services such as I/O management, virtual memory, and scheduling. The kernel coordinates interactions of all user processes and system resources. The kernel assigns priorities, services resource requests, and services hardware interrupts and exceptions. The kernel schedules and switches threads, pages memory, and swaps processes.

Differences Between Kernel Modules and User Programs

This section discusses several important differences between kernel modules and user programs.

Execution Differences Between Kernel Modules and User Programs

The following characteristics of kernel modules highlight important differences between the execution of kernel modules and the execution of user programs:

Structural Differences Between Kernel Modules and User Programs

The following characteristics of kernel modules highlight important differences between the structure of kernel modules and the structure of user programs:

Data Transfer Differences Between Kernel Modules and User Programs

Data transfer between a device and the system typically is slower than data transfer within the CPU. Therefore, a driver typically suspends execution of the calling thread until the data transfer is complete. While the thread that called the driver is suspended, the CPU is free to execute other threads. When the data transfer is complete, the device sends an interrupt. The driver handles the interrupt that the driver receives from the device. The driver then tells the CPU to resume execution of the calling thread. See Chapter 8, Interrupt Handlers, in Writing Device Drivers.

Drivers must work with user process (virtual) addresses, system (kernel) addresses, and I/O bus addresses. Drivers sometimes copy data from one address space to another address space and sometimes just manipulate address-mapping tables. See Bus Architectures in Writing Device Drivers.

User and Kernel Address Spaces on x86 and SPARC Machines

On SPARC machines, the system panics when a kernel module attempts to directly access user address space. You must make sure your driver does not attempt to directly access user address space on a SPARC machine.

On x86 machines, the system does not enter an error state when a kernel module attempts to directly access user address space. You still should make sure your driver does not attempt to directly access user address space on an x86 machine. Drivers should be written to be as portable as possible. Any driver that directly accesses user address space is a poorly written driver.


Caution – Caution –

A driver that works on an x86 machine might not work on a SPARC machine because the driver might access an invalid address.


Do not access user data directly. A driver that directly accesses user address space is using poor programming practice. Such a driver is not portable and is not supportable. Use the ddi_copyin(9F) and ddi_copyout(9F) routines to transfer data to and from user address space. These two routines are the only supported interfaces for accessing user memory. Modifying Data Stored in Kernel Memory shows an example driver that uses ddi_copyin(9F) and ddi_copyout(9F).

The mmap(2) system call maps pages of memory between a process's address space and a file or shared memory object. In response to an mmap(2) system call, the system calls the devmap(9E) entry point to map device memory into user space. This information is then available for direct access by user applications.

Device Drivers

A device driver is a loadable kernel module that manages data transfers between a device and the OS. Loadable modules are loaded at boot time or by request and are unloaded by request. A device driver is a collection of C routines and data structures that can be accessed by other kernel modules. These routines must use standard interfaces called entry points. Through the use of entry points, the calling modules are shielded from the internal details of the driver. See Device Driver Entry Points in Writing Device Drivers for more information on entry points.

A device driver declares its general entry points in its dev_ops(9S) structure. A driver declares entry points for routines that are related to character or block data in its cb_ops(9S) structure. Some entry points and structures that are common to most drivers are shown in the following diagram.

Figure 1–1 Typical Device Driver Entry Points

Diagram shows entry points that are common to most drivers
and how the entry points are used.

The Solaris OS provides many driver entry points. Different types of devices require different entry points in the driver. The following diagram shows some of the available entry points, grouped by driver type. No single device driver would use all the entry points shown in the diagram.

Figure 1–2 Entry Points for Different Types of Drivers

Diagram shows subsets of entry points that are used by
various types of device drivers.

In the Solaris OS, drivers can manage physical devices, such as disk drives, or software (pseudo) devices, such as bus nexus devices or ramdisk devices. In the case of hardware devices, the device driver communicates with the hardware controller that manages the device. The device driver shields the user application layer from the details of a specific device so that application level or system calls can be generic or device independent.

Drivers are accessed in the following situations:

The following diagram illustrates how a device driver interacts with the rest of the system.

Figure 1–3 Typical Device Driver Interactions

Diagram shows typical interactions between a device driver
and other elements in the operating system.

Driver Directory Organization

Device drivers and other kernel modules are organized into the following directories in the Solaris OS. See the kernel(1M) and system(4) man pages for more information about kernel organization and how to add directories to your kernel module search path.

/kernel

These modules are common across most platforms. Modules that are required for booting or for system initialization belong in this directory.

/platform/`uname -i`/kernel

These modules are specific to the platform identified by the command uname -i.

/platform/`uname -m`/kernel

These modules are specific to the platform identified by the command uname -m. These modules are specific to a hardware class but more generic than modules in the uname -i kernel directory.

/usr/kernel

These are user modules. Modules that are not essential to booting belong in this directory. This tutorial instructs you to put all your drivers in the /usr/kernel directory.

One benefit of organizing drivers into different directories is that you can selectively load different groups of drivers on startup when you boot interactively at the boot prompt as shown in the following example. See the boot(1M) man page for more information.


Type    b [file-name] [boot-flags] <ENTER>      to boot with options
or      i <ENTER>                               to enter boot interpreter
or      <ENTER>                                 to boot with defaults

                  <<< timeout in 5 seconds >>>

Select (b)oot or (i)nterpreter: b -a
bootpath: /pci@0,0/pci8086,2545@3/pci8086,
Enter default directory for modules [/platform/i86pc/kernel /kernel 
/usr/kernel]: /platform/i86pc/kernel /kernel

In this example, the /usr/kernel location is omitted from the list of directories to search for modules to load. You might want to do this if you have a driver in /usr/kernel that causes the kernel to panic during startup or on attach. Instead of omitting all /usr/kernel modules, a better method for testing drivers is to put them in their own directory. Use the moddir kernel variable to add this test directory to your kernel modules search path. The moddir kernel variable is described in kernel(1M) and system(4). Another method for working with drivers that might have startup problems is described in Device Driver Testing Tips.

Devices as Files

In UNIX, almost everything can be treated as a file. UNIX user applications access devices as if the devices were files. Files that represent devices are called special files or device nodes. Device special files are divided into two classes: block devices and character devices. See Character and Block Devices for more information.

Every I/O service request initially refers to a named file. Most I/O operations that read or write data perform equally well on ordinary or special files. For example, the same read(2) system call reads bytes from a file created with a text editor and reads bytes from a terminal device.

Control signals also are handled as files. Use the ioctl(9E) function to manipulate control signals.

Devices Directories

The Solaris OS includes both /dev and /devices directories for device drivers. Almost all the drivers in the /dev directory are links to the /devices directory. The /dev directory is UNIX standard. The /devices directory is specific to the Solaris OS.

By convention, file names in the /dev directory are more readable. For example, the /dev directory might contain files with names such as kdb and mouse that are links to files such as /devices/pseudo/conskbd@0:kbd and /devices/pseudo/consms@0:mouse. The prtconf(1M) command shows device names that are very similar to the file names in the /devices directory. In the following example, only selected output of the command is shown.


% prtconf -P
        conskbd, instance #0
        consms, instance #0

Entries in the /dev directory that are not links to the /devices directory are device nodes or special files created by mknod(1M) or mknod(2). These are zero-length files that just have a major number and minor number attached to them. Linking to the physical name of the device in the /devices directory is preferred to using mknod(1M).

Prior to the Solaris 10 OS, /devices was an on-disk filesystem composed of subdirectories and files. Beginning with the Solaris 10 OS, /devices is a virtual filesystem that creates these subdirectories and special files on demand.

For more information about the devices file system, see the devfs(7FS) man page.

Device Tree

The device files in the /devices directory are also called the device tree.

The device tree shows relationships among devices. In the device tree, a directory represents a nexus device. A nexus is a device that can be a parent of other devices. In the following example, pci@1f,0 is a nexus device. Only selected output from the command is shown.


# ls -l /devices
drwxr-xr-x   4 root     sys          512 date time pci@1f,0/
crw-------   1 root     sys      111,255 date time pci@1f,0:devctl

You can use prtconf(1M) or prtpicl(1M) to see a graphic representation of the device tree. See Overview of the Device Tree in Writing Device Drivers for more information about the device tree.

Character and Block Devices

A file in the device tree that is not a directory represents either a character device or a block device.

A block device can contain addressable, reusable data. An example of a block device is a file system. Any device can be a character device. Most block devices also have character interfaces. Disks have both block and character interfaces. In your /devices/pseudo directory, you might find devices such as the following:

brw-r-----   1 root     sys       85,  0 Nov  3 09:43 md@0:0,0,blk
crw-r-----   1 root     sys       85,  0 Nov  3 09:43 md@0:0,0,raw
brw-r-----   1 root     sys       85,  1 Nov  3 09:43 md@0:0,1,blk
crw-r-----   1 root     sys       85,  1 Nov  3 09:43 md@0:0,1,raw
brw-r-----   1 root     sys       85,  2 Nov  3 09:43 md@0:0,2,blk
crw-r-----   1 root     sys       85,  2 Nov  3 09:43 md@0:0,2,raw

Block devices have a b as the first character of their file mode. Character devices have a c as the first character of their file mode. In this example, the block devices have blk in their names and the character devices have raw in their names.

The md(7D) device is a metadevice that provides disk services. The block devices access the disk using the system's normal buffering mechanism. The character devices provide for direct transmission between the disk and the user's read or write buffer.

Device Names

This section shows a complex device name and explains the meaning of each part of the name in /dev and also in /devices. The following example is the name of a disk slice:


/dev/dsk/c0t0d0s7 -> ../../devices/pci@1c,600000/scsi@2/sd@0,0:h

First, examine the name of the file in the /dev directory. These names are managed by the devfsadmd(1M) daemon.

c0

Controller 0

t0

Target 0. On SCSI controllers, this value is the disk number.

d0

SCSI LUN. This value indicates a virtual partitioning of a target or single physical device.

s7

Slice 7 on the target 0 disk.

For the same device, compare the name of the file in the /devices directory. These names show the physical structure and real device names. Note that some of the components of the device name in the /devices directory are subdirectories.

pci@1c,600000

PCI bus at address 1c,600000. These addresses are meaningful only to the parent device.

scsi@2

SCSI controller at address 2 on the PCI bus at address 1c,600000. This name corresponds to the c0 in /dev/dsk/c0t0d0s7.

sd@0,0

SCSI disk at address 0,0 on the SCSI controller at address 2. This name represents target 0, LUN 0 and corresponds to the t0d0 in /dev/dsk/c0t0d0s7. The sd name and driver can also apply to IDE CD-ROM devices.

sd@0,0:h

Minor node h on the SCSI disk at address 0,0. This name corresponds to the s7 in /dev/dsk/c0t0d0s7.

Device Numbers

A device number identifies a particular device and minor node in the device tree. The dev_t parameter that is required in many DDI/DKI routines is this device number.

Each device has a major number and a minor number. A device number is a major,minor pair. A long file listing shows the device number in the column where file sizes are usually listed. In the following example, the device number is 86,255. The device major number is 86, and the device minor number is 255.


% ls -l /devices/pci@0,0:devctl
crw-------   1 root     sys       86,255 date time /devices/pci@0,0:devctl

In the Solaris OS, the major number is chosen for you when you install the driver so that it will not conflict with any other major number. The kernel uses the major number to associate the I/O request with the correct driver code. The kernel uses this association to decide which driver to execute when the user reads or writes the device file. All devices and their major numbers are listed in the file /etc/name_to_major.


% grep 86 /etc/name_to_major
pci 86

The minor number is assigned in the driver. The minor number must map each driver to a specific device instance. Minor numbers usually refer to sub-devices. For example, a disk driver might communicate with a hardware controller device that has several disk drives attached. Minor nodes do not necessarily have a physical representation.

The following example shows instances 0, 1, and 2 of the md device. The numbers 0, 1, and 2 are the minor numbers.

brw-r-----   1 root     sys       85,  0 Nov  3 09:43 md@0:0,0,blk
crw-r-----   1 root     sys       85,  0 Nov  3 09:43 md@0:0,0,raw
brw-r-----   1 root     sys       85,  1 Nov  3 09:43 md@0:0,1,blk
crw-r-----   1 root     sys       85,  1 Nov  3 09:43 md@0:0,1,raw
brw-r-----   1 root     sys       85,  2 Nov  3 09:43 md@0:0,2,blk
crw-r-----   1 root     sys       85,  2 Nov  3 09:43 md@0:0,2,raw

In the name sd@0,0:h,, h represents a minor node. When the driver receives a request for minor node h, the driver actually receives a corresponding minor number. The driver for the sd node interprets that minor number to be a particular section of disk, such as slice 7 mounted on /export.

Chapter 2, Template Driver Example shows how to use the ddi_get_instance(9F) routine in your driver to get an instance number for the device you are driving.

Development Environment and Tools

This section summarizes the driver development process and provides some pointers to resources. For more information on the development process, see Driver Development Summary in Writing Device Drivers.

Sun offers training courses in Solaris OS internals, crash dump analysis, writing device drivers, DTrace, Sun Studio, and other topics useful to Solaris developers. See http://www.sun.com/training/ for more information.

    The general steps in writing a device driver are as follows:

  1. Write a .c source file using the interfaces and structures defined in man page sections 9E, 9F, and 9S. Most of the include files you need are in /usr/include/sys. The function and structure man pages show which include files you need.

  2. Write a .conf hardware configuration file to define property values for your driver.

  3. Compile and link your driver. Always use the -D_KERNEL option when you compile a driver for the Solaris OS. The default compile result is 32-bit. To get a 64-bit result on a 64-bit platform, specify the appropriate 64-bit option as described in Building a Driver.

  4. Copy your driver binary file and your driver configuration file to the appropriate [platform]/kernel directories. See Driver Directory Organization for descriptions of driver directories.

  5. Use the add_drv(1M) command to load your driver. When your driver is loaded, you can see your driver in /dev and /devices. You can also see an entry for your driver in the /etc/name_to_major file.

Writing a Driver

A driver consists of a C source file and a hardware configuration file.

Writing a Driver Module

The C code for a driver is a collection of data and functions that define a kernel module. As noted in Structural Differences Between Kernel Modules and User Programs, a driver has no main() routine. Many of the subroutines of a driver are special functions called entry points. See Device Drivers for information about entry points.

The function man pages provide both the function declaration that you need in your driver and the list of header files you need to include. Make sure you consult the correct man page. For example, the following command displays the ioctl(2) man page. The ioctl(2) system call cannot be used in a device driver.


% man ioctl

Use one of the following commands to display the ioctl(9E) man page. The ioctl(9E) subroutine is a device driver entry point.


% man ioctl.9e
% man -s 9e ioctl

By convention, the names of functions and data that are unique to this driver begin with a common prefix. The prefix is the name of this driver or an abbreviation of the name of this driver. Use the same prefix for all names that are specific to this driver. This practice makes debugging much easier. Instead of seeing an error related to an ambiguous attach() function, you see an error message about mydriver_attach() or newdriver_attach().

A 64-bit system can run both 32-bit user programs and 64-bit user programs. A 64-bit system runs 32-bit programs by converting all data needed between the two data models. A 64-bit kernel supports both 64-bit and 32-bit user data. Whenever a 64-bit driver copies data between kernel space and user space, the driver must use the ddi_model_convert_from(9F) function to determine whether the data must be converted between 32-bit and 64-bit models. For an example, see Reporting and Setting Device Size and Re-initializing the Device.

The Sun Studio IDE includes the following three source editors: GVIM, XEmacs, and the built-in Source Editor provided by NetBeans. The IDE provides online help for these tools. You can also run GVIM and XEmacs from the command line. See vim(1) and xemacs(1).

For more information, see the following resources:

Writing a Configuration File

A driver that is not self-identifying must have a configuration file named node_name.conf, where node_name is the prefix for the device. A self-identifying driver is a driver that can obtain all the property information it needs from the DDI property interfaces such as ddi_prop_get_int(9F) and ddi_prop_lookup(9F). The minimum information that a configuration file must contain is the name of the device node and the name or type of the device's parent.

For more information about device driver configuration files, see the driver.conf(4) man page. For an example configuration file, see Writing the Device Configuration File.

Building a Driver

This section tells you how to compile and link a driver for different architectures.

Make sure you have installed the Solaris OS at the Developer level or above. Follow the instructions in Chapter 2, Installing With the Solaris Installation Program (Tasks), in Solaris 10 Installation Guide: Basic Installations. Select Custom Install, and select the Developer cluster or above.

In your path environment variable, include /opt/SUNWspro/bin followed by /usr/ccs/bin.

A 64-bit kernel cannot use a 32-bit driver. A 64-bit kernel can use only 64-bit drivers. All parts of any particular program must use the same data model. A device driver is not a complete program. The kernel is a complete program. A driver is a part of the kernel program. If you want your device to work with the Solaris OS in 32-bit mode and with the Solaris OS in 64-bit mode, then you must provide both a 32-bit driver and a 64-bit driver.

By default, compilation on the Solaris OS yields a 32-bit result on every architecture. To obtain a 64-bit result, use the compilation options specified in this section for 64-bit architectures.

Use the prtconf(1M) command with the -x option to determine whether the firmware on this system is 64-bit ready.

Compiling with Sun Studio

Use the -D_KERNEL option to indicate that this code defines a kernel module.


Note –

Sun Studio 9 does not support 64-bit x86 architectures. Use Sun Studio 10, Sun Studio 11, or Sun Studio 12 to compile and debug drivers for 64-bit x86 architectures.


For more information on compile and link options, see the Sun Studio Man Pages and the Sun Studio 12: C User’s Guide. See the Sun Studio Information Center in the Sun Studio 12 Collection for Sun Studio books about dbx, dmake, Performance Analyzer, and other software development topics. To read technical articles about Sun Studio, see Sun Studio Technical Articles. To download Sun Studio, go to http://developers.sun.com/sunstudio/.

Compiling with the GNU C Compiler

To get the GNU C compiler, you must install the Solaris OS at the Developer level or above. Follow the instructions in Chapter 2, Installing With the Solaris Installation Program (Tasks), in Solaris 10 Installation Guide: Basic Installations. Select Custom Install, and select the Developer cluster or above. The GNU C compiler is installed in /usr/sfw.

Use the -D_KERNEL option to indicate that this code defines a kernel module. These examples show options that are required for correct functionality of the result.

For more information on these and other options, see the gcc(1) man page. See also the GCC web site at http://gcc.gnu.org/. More information about using the gcc compiler with the Solaris OS is on the OpenSolaris web site at http://opensolaris.org/os/community/tools/gcc/.

Installing a Driver

After you write and build your driver, you must install the driver binary. To install a driver, copy the driver binary and the configuration file to the appropriate /kernel/drv directory.

Make sure you are user root when you install a driver.

Copy the configuration file to the kernel driver area of the system.


# cp mydriver.conf /usr/kernel/drv

Install drivers in the /tmp directory until you are finished modifying and testing the _info(), _init(), and attach() routines. See Device Driver Testing Tips for more information.

Copy the driver binary to the /tmp directory.


# cp mydriver /tmp

Link to the driver from the kernel driver directory.

When the driver is well tested, copy the driver directly to the appropriate kernel driver area of the system.

Adding, Updating, and Removing a Driver

Use the add_drv(1M) command to make the installed driver usable. Be sure you are user root when you use the add_drv(1M) command.


# add_drv mydriver

The following events take place when you add a driver:

The file /etc/driver_aliases might be updated. The /etc/driver_aliases file shows which devices are bound to which drivers. If a driver is not listed in the /etc/driver_aliases file, then the Solaris OS does not load that driver or attach to that driver. Each line of the /etc/driver_aliases file shows a driver name followed by a device name. You can search this file to determine which driver is managing your device.


Note –

Do not edit the /etc/driver_aliases file manually. Use the add_drv(1M) command to establish a device binding. Use the update_drv(1M) command to change a device binding.


The example drivers shown in this book manage pseudo devices. If your driver manages real hardware, then you need to use the -c and -i options on the add_drv(1M) command or the -i option on the update_drv(1M) command. To specify a device class or device ID, you might find the following sites useful. This information also is useful to search the /etc/driver_aliases file to find out whether a device already is supported.

Use the update_drv(1M) command to notify the system about attribute changes to an installed device driver. By default, the update_drv(1M) command reloads the hardware configuration file for the specified driver. Use the prtconf(1M) command to review the current configuration information for a device and driver. For example, the -D option shows which driver manages a particular device. The -P option shows information about pseudo devices.

Use the rem_drv(1M) command to update the system driver configuration files so that the driver is no longer usable. The rem_drv(1M) command does not physically delete driver files. If possible, the rem_drv(1M) command unloads the driver from memory.

Loading and Unloading a Driver

A driver is loaded into memory when a device that the driver manages is accessed. A driver might be unloaded from memory when the driver is not being used. Normally, you do not need to load a driver into memory manually or unload a driver from memory manually.

To manually load a loadable module into memory, use the modload(1M) command.

While you are developing your driver, you might want to manually unload the driver and then update the driver. To manually unload a loadable module from memory, use the modunload(1M) command.

Testing a Driver

Drivers should be thoroughly tested in the following areas:

For detailed information on how to test your driver and how to avoid problems during testing, see the following references:

Additional testing is specific to the type of driver.