- George Pajari. Writing UNIX Device Drivers. Addison-Wesley. 1991.
- Alessandro Rubini and Jonathan Corbet. Linux Device Drivers, 2nd edition. O'Reilly. 2001. (There is a 3rd edition available now.)
- Linux online documentation.
Discussing device drivers is diffucult because, by their nature, they are specific to particular hardware, the internals of the operating system, and also to the details of how the hardware is connected to the system. For example, a serial connector (RS232, say) on your computer is typically supported by a chip called a UART (Universal Asychronous Receiver/Transmitter) or USART (Universal Synchrounous/Asychronous Receiver/Transmitter). You probably saw these chips in CS240. Each chip contains a receiver shift register and a transmitter shift register plus some control pins (including an all-important clock). A serial device driver has to know how to control these things, how to tell if the chip is ready to transmit, whether there is data that has been read, etc. But that's not enough. Is the chip wired into the computer so that it appears at a particular physical memory address (i.e., is it memory mapped), or is there a separate address space for I/O devices (a separate bus on the mother board or particular I/O control lines on the system bus)? These choices, in turn depend on the particular CPU and a variety of industry standards. When you've answered these questions and written a device driver, the likelihood that the code will be portable to another machine is slim (though standards help a lot). And the device knowledge transfer to a SCSI disk driver project is very small.
The OS side of the driver story is a little better. Operating systems are generally modularized so that there are a small number of internal device driver interfaces through which particular drivers interact with the rest of the OS.
In some operating systems, you can install a driver and
reboot: the OS configures its device support at boot-time. The
Unix tradition has been that you had to recompile some of the OS
or at least relink the Unix kernel with the new driver to make a
new vmunix
file (the OS kernel executable), and
then reboot.
Linux has improved the situation quite a bit. Linux supports
something it calls modules, which are operating system
components that can be installed either at boot time or
dynamically while the system is running. (man
insmod
). Device drivers are one kind of Linux module.
Unix kernels distinguish different classes of driver depending on the kind of interaction the device will have with the kernel:
- Block devices work with larger units of randomly
addressable blocks. Disk controllers are the principal
example. They transfer data in large chunks (512 bytes is
fairly typical for hard drives, but 2K is normal for
CDRoms).
Block devices are the kinds of things one can put behind a file system, i.e., you can mount them. They are addressed as nodes in the file system, such as
/dev/hda6
. Their random addressability supportsseek()
, and the operating system provides substantial support for block-oriented buffering. One interesting detail is that the unit of storage (sometimes called a sector or physical block) may not be the same size as the I/O transfer blocksize used by the kernel. Kernel I/O is done in some multiple of the physical block size, and the device driver must translate between the two.One can open, close, read, and write block devices.
- Character devices represent physical devices that
generate or consume a stream of characters. Typically,
they do not support random access. What would it mean to
have random access to the input stream coming from the
keyboard? Character devices are generally simpler, and
receive far less support in the kernel. Character devices
are also used for devices that transfer data in unusual or
variable sized chunks (like tape drives). Since character
devices do not rely on a kernel buffer cache, user programs
interact with them much more directly.
Character devices are also addressed as nodes in the file system, e.g., the system printer (parallel printer port) is usually
/dev/lp0
. They also can be opened, closed, read, and written (but one does notseek()
with them). - Terminal (tty) devices are a special class of
character device, but they are worth mentioning. These
character devices represent terminals connected to the
system, and they support line editing etc.
/dev/console
and/dev/ttyS0
are examples. - Network drivers are for devices, like ethernet
cards, that connect the computer to a network. They are
based on the STREAMS Network device drivers. They do have
an entry in the
/dev
directory, but they do not have representatives in the file system (one must make different calls to interact with them). Network drivers support protocol stacks.
Device driver functionality is normally invoked via the VFS,
that is, there is a file system abstraction between the device
driver and the application code. This works very nicely for
typical I/O operations like read()
and
write()
. However, many devices offer functionality
that is not readily mapped into file system operations. For
example, a mouse driver can adjust a mouse's sensitivity, a
display driver can often alter the contrast and resoultion of a
device. There is a standard Unix way to talk to the driver
without going through the file system abstraction:
ioctl()
takes a file descriptor, a device-specific
request, and parameters.
We're not in Kansas Anymore
Devide drivers are part of the kernel, and are not application programs. That means- They run in kernel mode: they can do anything the superuser can do. One must be very careful!
- They do not run from start to finish, and they don't have a
main()
function. - They export a set of entry points, which are functions that are called whenever the kernel requires some service. Entry points can be invoked as a result of a user program action, as part of some kernel operation, or in response to an interrupt.
- They are not linked with
libc
, only with the kernel. That means device drivers can only use symbols exported by the kernel. The usualprintf()
is not available, for example. (Linux provides a similarprintk()
for kernel code.) There is no floating point support. - They must be disigned with concurrency in mind: think reentrant thread programming. With multiprocessing, several programs may be requesting device services at once, even on a single CPU system. In addition, device interrupts are asynchronous: they may come at any time, even while the driver is servicing another call. Avoiding race conditions must be considered from the beginning.
- Kernel code is memory resident (unpaged), and size is
therefore much more important. Also, since all OS modules
are linked together, they share one global namespace. To
avoid namespace pollution, export as few symbols as
possible (use
static
) and choose unique prefixes for the symbols you do export. - The kernel stack, in particular, is small and not paged, so recursion is bad.
- The kernel exports various names that drivers can use like
imported variables. Traditional Unix systems exported a
user structure called
u
. Linux systems provide access to the current task via the namecurrent
, which denotes a pointer to astruct task_struct
. For operations in which the kernel is operating on behalf of a user process, this structure will contain important information, such as a current file offset or a location to place results. This structure is not valid during interrupt processing, however. - Kernel space addresses are not the same as user space addresses. This means you can't just dereference a pointer from the user process that caused the driver function to be invoked. There are special kernel functions for copying data between kernel and user space.
Device drivers typically need exclusive access to their I/O
ports or I/O memory. How this is acheived is highly
OS-specific. Linux has a registration scheme that we won't go
into. However, you can see what is registered where by looking
at /proc/ioports
and /proc/iomem
.
Some Linux Details
As I said above, previous Unix systems required new drivers to be statically linked to produce a new kernel which was then rebooted. The Linux module system works by having theinsmod
command invoke the module's
init()
function (using init_module()
).
This function registers the module's entry points with the
operating system. The rmmod
command invokes the
cleanup_module()
function, which calls the modules
cleanup()
function which unregisters the module's
entry points.
Registration is important: The way drivers work is they export certain facilties. There are a wide variety of facilities (like serial ports). A registration request consists of the name of the facility and a pointer to a structure that depends on the facility. This structure will contain pointers to functions defined in the driver. Just as we saw with the VFS, the OS will use a kind of object oriented method dispatch to invoke the driver function.
In fact, a driver must export entry points corresponding to the file operations permitted on that type of device. The VFS file operations will ultimately point to driver entry points.
To get kernel and module functionality in Linux, you must define
the symbols __KERNEL__
and MODULE
and
then use appropriate include files from the kernel source tree
(usually in /usr/src/linux/include
. It is wise to
allow your code to configure itself for multiprocessors if
appropriate:
#include <linux/config.h>
#ifdef CONFIG_SMP
# define __SMP__
#endif
Example Drivers
Drivers typically involve several parts:- System configuration (finding out details of the system
it's running on and doing apropriate
#define
s). - Include files.
- Device configuration (defining device-specific contants and structures).
- Driver entry points.
Both the texts above have relatively simple examples. Rubini
and Corbet have a bunch of online
examples. However, I thought it might be fun to peek a
couple of drivers loaded on our system. It's best to start with
character drivers, since they are the simplest. One example
is the Linux driver for the MacIntosh
mouse. Another supports serial I/O.
Modified: 09 May 2005