Writing Device Drivers

Chapter 2 Overview of Solaris Device Drivers

This chapter gives an overview of Solaris device drivers. It discusses what a device driver is and the types of device drivers that Solaris 8 supports. It also provides a general discussion of the routines that device drivers must implement and points out compiler-related issues.

What Is a Device Driver?

A device driver is a kernel module responsible for managing low-level I/O operations for a particular hardware device. Device drivers can also be software-only, emulating a device that exists only in software, such as a RAM disk or a pseudo-terminal.

A device driver contains all the device-specific code necessary to communicate with a device and provides a standard set of interfaces to the rest of the system. This interface protects the kernel from device specifics just as the system call interface protects application programs from platform specifics. Application programs and the rest of the kernel need little (if any) device-specific code to address the device. In this way, device drivers make the system more portable and easier to maintain.

Types of Device Drivers

There are several kinds of device drivers, each handling a different kind of I/O. Block device drivers manage devices with physically addressable storage media, such as disks. All other devices are considered character devices. Two types of character device drivers are standard character device drivers and STREAMS device drivers.

Block Device Drivers

Devices that support a file system are known as block devices. Drivers written for these devices are known as block device drivers. Block device drivers take a file system request, in the form of a buf(9S) structure, and issue the I/O operations to the disk to transfer the specified block. The main interface to the file system is the strategy(9E) routine. See Chapter 11, Drivers for Block Devices for more information.

Block device drivers can also provide a character driver interface that allows utility programs to bypass the file system and access the device directly. This device access is commonly referred to as the raw interface to a block device.

Character Device Drivers

Character device drivers normally perform I/O in a byte stream. They can also provide additional interfaces not present in block drivers, such as I/O control (ioctl) commands, memory mapping, and device polling. See Chapter 10, Drivers for Character Devices for more information.

Byte-Stream I/O

The main task of any device driver is to perform I/O, and many character device drivers do what is called byte-stream or character I/O. The driver transfers data to and from the device without using a specific device address. This is in contrast to block device drivers, where part of the file system request identifies a specific location on the device.

The read(9E) and write(9E) entry points handle byte-stream I/O for standard character drivers. See "I/O Request Handling" for more information.

Memory Mapped Devices

For certain devices, such as frame buffers, it is more efficient for application programs to have direct access to device memory. Applications can map device memory into their address spaces using the mmap(2) system call. To support memory mapping, device drivers implement segmap(9E) and devmap(9E) entry points. For information on devmap(9E), see Chapter 12, Mapping Device or Kernel Memory. For information on segmap(9E), see Chapter 10, Drivers for Character Devices.

Drivers that define the devmap(9E) entry point usually do not define read(9E) and write(9E) entry points, as application programs perform I/O directly to the devices after calling mmap(2).

STREAMS Drivers

STREAMS is a separate programming model for writing a character driver. Devices that receive data asynchronously (such as terminal and network devices) are suited to a STREAMS implementation. STREAMS device drivers must provide the loading and autoconfiguration support described in Chapter 5, Autoconfiguration. See the Streams Programming Guide for additional information on how to write STREAMS drivers.

Driver Module Entry Points

Each device driver defines a standard set of functions called entry points, which are listed in Intro(9E). These entry points are called by the Solaris kernel to load and unload the driver, autoconfigure devices, and provide the character, block, or STREAMS driver I/O services. Drivers for different types of devices have different sets of entry points according to the kinds of operations the devices perform. A driver for a memory-mapped character-oriented device, for example, supports a devmap(9E) entry point, while a block driver does not.

Figure 2-1 Device Driver Overview

Graphic

Some operations are common to all drivers, such as the functions that are required for module loading (_init(9E), _info(9E), and _fini(9E)), and the required autoconfiguration entry points attach(9E), detach(9E), and getinfo(9E). Drivers also support the optional autoconfiguration entry point for probe(9E). Most leaf drivers have open(9E) and close(9E) entry points to control access to their devices.

Traditionally, all driver function and variable names have some prefix added to them. Usually this is the name of the driver, such as xxopen() for the open(9E) routine of driver xx. In subsequent examples, xx is used as the driver prefix.


Note -

In the Solaris 8 operating environment, only the loadable module routines must be visible outside the driver object module. Other routines can have the storage class static.


Loadable Module Entry Points

All drivers are required to implement the loadable module entry points _init(9E), _fini(9E), and _info(9E) entry points to load, unload, and report information about the driver module.

It is recommended that drivers allocate and initialize any global resources in _init(9E) and release their resources in _fini(9E).

Autoconfiguration Entry Points

Drivers are required to implement the attach(9e), detach(9e), and getinfo(9e) entry points for device autoconfiguration. Drivers might need to implement probe(9e) if the driver supports devices that are not self identifying, such as SCSI target devices.

Character and Block Driver Entry Points

Drivers for character and block devices export a cb_ops(9S) structure, which defines the driver entry points for block device access and character device access. Both types of drivers are required to support open(9E) and close(9E). Block drivers are required to support strategy(9E), while character drivers can choose to implement whatever mix of read(9E), write(9E), ioctl(9E), mmap(9E), ordevmap(9E) entry points as appropriate for the type of device. Character drivers can also support a polling interface through chpoll(9E), as well as asynchronous I/O through aread(9E) and awrite(9E).

For information on character driver entry points, see Chapter 10, Drivers for Character Devices. For information on block driver entry points, see Chapter 11, Drivers for Block Devices.

Power Management Entry Point

Drivers for hardware devices that provide Power Management functionality can support the optional power(9E) entry point. See Chapter 9, Power Management for details about this entry point.

Driver Context

The driver context determines which kernel routines the driver is permitted to call. There are four contexts in which driver code executes:

The manual pages in section 9F document the allowable contexts for each function. For example, in kernel context the driver must not call copyin(9F).

Interrupt Handling

The Solaris 8 DDI/DKI addresses these aspects of device interrupt handling:

Device interrupt sources are contained in a property called interrupts, which is either provided by the PROM of a self-identifying device, in a hardware configuration file, or by the booting system on the IA platform.

Callback Functions

Certain DDI mechanisms provide a callback mechanism. DDI functions provide a mechanism for scheduling a callback when a condition is met. Conditions for which callback functions are used include:

In some sense, callback functions are similar to entry points--interrupt handlers, for example. DDI functions that allow callbacks expect the callback function to perform certain tasks. In the case of DMA routines, a callback function must return a value indicating whether the callback function needs to be rescheduled in case of a failure.

Callback functions execute as a separate interrupt thread and must handle all the usual multithreading issues.


Note -

A driver must cancel all scheduled callback functions before detaching a device.


Printing Messages

Device drivers do not usually print messages. Instead, the driver entry points should return error codes so that the application can determine how to handle the error. If the driver must print a message, it should use cmn_err(9F) to do so. This is similar to the C function printf(3C), which prints to the console, to the message buffer, or both.

The format string specifier interpreted by cmn_err(9F) is similar to the printf(3C) format string, with the addition of the format %b, which prints bit fields. Callers to cmn_err(9F) also specify the level, which indicates the label to be printed. The first character of the format string is treated specially. See cmn_err(9F) for more details.

CE_PANIC has the side effect of crashing the system. This level should be used only if the system is in such an unstable state that to continue would cause more problems. It can also be used to get a system core dump when debugging. It should not be used in production device drivers.

Device IDs

The Solaris DDI provides interfaces that allow drivers to provide a persistent unique identifier for a device, a 'device ID', which can be used to identify or locate a device and which is independent of the devices name or number (dev_t). Applications can use the functions defined in libdevid(3LIB) to read and manipulate the device IDs registered by the drivers.

Software State Management

To assist device driver writers in allocating state structures, the Solaris 8 DDI/DKI provides a set of memory management routines called the software state management routines (also known as the soft state routines). These routines dynamically allocate, retrieve, and destroy memory items of a specified size, and hide the details of list management. An instance number is used to identify the desired memory item; this number can be (and usually is) the instance number assigned by the system.

Routines are provided to:

See "Loadable Driver Interfaces" for an example of how to use these routines.

Dynamic Memory Allocation

Device drivers must be prepared to simultaneously handle all attached devices that they claim to drive. There should be no driver limit on the number of devices that the driver handles, and all per-device information must be dynamically allocated.

void *kmem_alloc(size_t size, int flag);

The standard kernel memory allocation routine is kmem_alloc(9F). It is similar to the C library routine malloc(3C), with the addition of the flag argument. The flag argument can be either KM_SLEEP or KM_NOSLEEP, indicating whether the caller is willing to block if the requested size is not available. If KM_NOSLEEP is set, and memory is not available, kmem_alloc(9F) returns NULL.

kmem_zalloc(9F) is similar to kmem_alloc(9F), but also clears the contents of the allocated memory.


Note -

Kernel memory is a limited resource, not pageable, and competes with user applications and the rest of the kernel for physical memory. Drivers that allocate a large amount of kernel memory can cause system performance to degrade.


void kmem_free(void *cp, size_t size);

Memory allocated by kmem_alloc(9F) or by kmem_zalloc(9F) is returned to the system with kmem_free(9F). This is similar to the C library routine free(3C), with the addition of the size argument. Drivers must keep track of the size of each object they allocate in order to call kmem_free(9F) later.

Programmed I/O Device Access

Programmed I/O device access is the act of reading and writing of device registers or device memory by the host CPU. The Solaris DDI provides interfaces for mapping a devices registers or memory by the kernel as well as interfaces for reading and writing to device memory from the driver. These interfaces are designed to enable drivers to be developed that are platform and bus independent, by automatically managing any difference in device and host endianness as well as enforcing any memory store ordering requirements imposed by the device.

Direct Memory Access (DMA)

Solaris defines a high level architecture independent model for supporting DMA capable devices. The Solaris DDI is designed to shield drivers from platform specific details, which enables a common driver to be developed that runs across multiple platforms and architectures.

Properties

Properties define attributes of the device or device driver. Properties can be defined by the FCode of a self-identifying device, by a hardware configuration file (see driver.conf(4)), or by the driver itself using the ddi_prop_update(9F) family of routines.

A property is a name-value pair. The name is a string that identifies the property with an associated value. The value of a property can be one of five types:

A property that has no value is known as a Boolean property. It is considered to be true if it exists and false if it doesn't exist.

Driver and Device Statistics

Solaris provides a rich set of interfaces for maintaining and exporting kernel level statistics, or 'kstats'. Drivers are free to use these interfaces to export driver and device statistics that can be used by user applications to observe the internal state of the driver. See kstat_create(9F) and kstat(3KSTAT) for additional information.

64-Bit Considerations

The Solaris system can run in 64-bit mode on appropriate hardware and provides a 64-bit kernel with a 64-bit address space for applications. To update a device driver to be 64-bit ready, driver writers need to understand the 32-bit and 64-bit C data type models, know how to use the system derived types and the fundamental C data types, and understand specific driver issues, such as how to enable a 64-bit driver and a 32-bit application to share data structures.

For details on making a device driver ready for a 64-bit environment, see Appendix C, Making a Device Driver 64-Bit Ready .

Kernel Programming Model

The Solaris kernel is a large collection of code that is compiled in one of two ways; it is either compiled as a 32-bit program that supports solely 32-bit applications, or as a 64-bit program that supports both 32-bit and 64-bit applications. To allow drivers and STREAMS modules to be used on both systems, you must write kernel code that is both portable between these two compilation environments and supportive of 32-bit and 64-bit applications. The resulting code must be compiled in two ways, creating two separate modules: a 32-bit module for the 32-bit kernel, and a 64-bit module for the 64-bit kernel.

Some classes of portability issues can best be solved using the standard derived types, such as size_t, off_t, time_t, and caddr_t, since these grow and shrink appropriately. To provide better support of 32-bit applications in the 64-bit kernel, fixed-width types corresponding to the sizes expected by 32-bit applications are available in <sys/types32.h>, for example, size32_t, off32_t, time32_t, and caddr32_t.

Other classes of portability problems, in particular those describing hardware registers or data sent over the wire, are best described using the size-invariant types in <sys/inttypes.h>; for example, uint16_t, and int64_t. It also includes the definition of intptr_t and uintptr_t.

See the Solaris 64-bit Developer's Guide for the full list of changes to derived types and more information on fixed-width types.

Data Model Concepts

The term data model is used here to describe the model for addresses and data that is used by the kernel and applications.

On the 32-bit kernel, the same data model is used by both kernel and applications: ILP32. There is no 64-bit application support on the 32-bit kernel.

On the 64-bit kernel, two different kinds of applications are supported concurrently: 32-bit applications using the ILP32 data model, and 64-bit applications using the LP64 data model. The 64-bit kernel itself uses the LP64 data model.

These concepts are captured in three flags that are associated with every system call, including ioctl(2):

At first sight, the most useful question to answer about the application invoking the kernel is: "Is it ILP32 or LP64?" However, a better test is: "Is the application using the same model as the kernel, or a different model?" The concept of native data model serves to answer that question; it is conditionally defined to match the data model of the kernel implementation. This approach enables you to write substantially cleaner code.

ioctl(9E) Considerations

Most driver entry points are managed by the 32-bit and 64-bit system caller handlers in the kernel in such a way that a driver does not need to be concerned about whether it is performing an operation on behalf of a 32-bit or a 64-bit application.

However, ioctl(9E) offers a direct connection between applications and the kernel. It enables a driver to implement device-specific operations. That is, it can cause the driver to perform a device-specific command or to pass arbitrary data between the driver and the application.

The third argument to ioctl() is either a simple integral value or a pointer to some other value, typically a data structure. The data structure might be different in size and alignment between a 32-bit and a 64-bit application. Because the form of the interface between the driver and application is generally a private agreement between the driver and the application, the kernel cannot intervene to automatically translate the data structures. It cannot even tell whether the argument is an integer or a pointer.

Therefore, drivers and STREAMS modules need to know how to interpret the data structures passed in from an application.