The Ins and Outs of Linux Kernel Device Drivers
Kernel device drivers give you direct control of I/O ports and system resources.
David Marsh, Contributing Technical Editor -- Test & Measurement World, 10/15/2000
My previous article introduced you to Linux drivers and helped you get started using and writing simple user-space I/O programs.1 In this article, I hope to familiarize you with kernel-level I/O drivers and explain how they work and how you can use them. A complete driver example on the T&MW Web site provides you with code for a sample kernel-level driver, a makefile, and shells to test the driver.
Kernel drivers prove particularly useful because they offer direct access to hardware such as interrupts, I/O ports, and physical memory. But because these drivers run in kernel memory space, any bugs in your code can crash your machine. Before you experiment with kernel-level drivers, first review T&MW’s past Linux articles1, 2, 3 and try to do as much coding as you can in user space. (For this article, assume “driver” refers to kernel-space code unless noted otherwise.)
Kernel drivers link to the heart of the operating system (OS) and provide entry points to functions such as open, read, write, and close. Depending on your version of Linux, the OS offers several mechanisms that accommodate kernel-space drivers and make their facilities available to calling application programs. If you’re writing a driver for use on a variety of computers, you must research and solve compatibility problems before releasing drivers to the public. A full discussion of compatibility goes beyond the scope of this article. For now, you can circumvent most incompatibility problems by using a kernel version of 2.2.x or later.
Depending on the kernel version you have, Linux can accommodate monolithic-driver and modular-driver code. (I’ll assume you have version 2.2.x or later.) A monolithic driver gets permanently bound to the kernel during kernel compilation. Thus, changing monolithic-driver code requires recompiling the kernel. That’s not a task most programmers relish.
As an alternative to recompiling the kernel, you can use modular drivers that application programs load and unload on demand by using a supervisory program, or “daemon.” The program, called kerneld, should have been placed in your /sbin directory by default, along with other utilities from the modutils package (Table 1), when you installed Linux on your PC. You can examine the operation of the modutils commands by typing their names at the command line prompt. When a process invokes kerneld, the daemon monitors messages that pass between calling applications and the kernel. The daemon notifies applications when the kernel tries to access drivers that aren’t available. If the application can point kerneld to the missing drivers, execution continues; otherwise, the kernel returns an error code to the application. When you’re ready to employ modular loading, you can access the daemon’s facilities by including the linux/kerneld.h header in your driver source files. (For more information, refer to the kerneld tutorial in the mini-HOWTO section at www.linuxdoc.org.)
If you have a recent Linux distribution, such as Red Hat 6.2, you may find that kmod supercedes kerneld. The command functions essentially the same way, but the implementation details change. (See the Linux file /user/doc/kernel-doc-2.2.12/kmod. txt for more specific information).
Table 1: Linux Modular Driver-Support Functions
| Application | File Location (Red Hat 6.1) | Function |
| insmod | /sbin/insmod | Installs a module |
| rmmod | /sbin/rmmod | Removes a module |
| lsmod | /sbin/lsmod | Lists loaded modules |
| ksyms | /sbin/ksyms | Lists kernel module symbols |
| modprobe | /sbin/modprobe | Automatically loads modules |
| depmod | /sbin/depmod | Creates dependency info for modprobe |
| kerneld | /sbin/kerneld | Monitors driver requests |
| genksyms | /sbin/genksyms | Sets module versions |
| modinfo | /sbin/modinfo | Displays module information |
When you compile a module driver, the compiler includes a kernel-version symbol that the insert-module command, insmod, uses to check the run-time environment. If there’s a version mismatch between a driver and the version of Linux that’s running on your machine, it could compromise your system. To avoid such mismatches, the Linux kernel returns an error message that prevents you from loading incompatible drivers. You can use several methods to examine the kernel version, as shown in a later example.
Filesystem Mechanics for Drivers
Two directories, the /dev device-file directory and the /proc processes directory, provide information that lets a kernel module communicate with other processes. The /proc “directory” is a special structure that Linux creates on the fly from data structures within the kernel to reflect the state of the running system. Entries in the /proc filesystem can be created by any kernel module.
Files in the /dev directory allow calling applications to communicate with device drivers in the kernel. By convention, developers place drivers in the /dev directory. Both /dev and /proc files use the same set of “major” and “minor” numbers to track and identify device drivers. To implement a driver, you first load driver code identified by a name and a major number into the kernel. Then you make a filename node in /dev directory that uses your driver’s major number to provide file-like access, to your driver’s facilities that you can access by name.
The range of major numbers varies from as few as 32 to what now seems to be 255 (my documentation suggests only 127, but my system disagrees!). The Linux kernel reserves some numbers for its own use. (See the Linux file usr/doc/kernel-doc-2.2.12/devices.txt for more information). Block and character devices each have their own set of unique major numbers.
When a computer contains several of the same type of device, say multiple digital I/O boards, minor numbers identify individual boards so one driver can control multiple boards. If a driver services only one device, assign minor number 0 to the device. Minor numbers can range from 0 to 255.
The make-node command, mknod, registers major and minor numbers with the kernel. Previous articles explain how to allocate a major number and five minor numbers to accommodate two data-acquisition cards.2,3 These articles also explain how to use the chmod command to grant user permissions to use a new device.
When you installed your Linux operating system, mknod created entries for every driver option in the /dev directory. You can list active devices and their major numbers by typing cat /proc/devices at the command line. You can then determine which numbers are already in use and select an unused major number.
Some programmers write a script that examines /proc/devices for a free number to use. When you’re ready to explore dynamic major number allocation, find out more about the register_chrdev function that assigns a free major number and registers your module with /proc/devices. If register_chrdev succeeds, the function returns the major number it assigned for you to employ in your /dev device file entry.
Linux uses a Virtual Filesystem Switch (VFS) mechanism to “mount” and manipulate multiple filesystems. One of the switch’s underlying structures, file_operations, is essential for writing device drivers. The file_operations structure includes function pointers that the kernel uses to access your device’s functions.
The structure supports functions that let applications read from and write to devices, and perform other file operations, such as open and close. You can examine a complete list of device-control commands in the struct file_operations section of the file /usr/include/ linux/fs.h. Notice that read and write commands only operate with character devices; block devices use bread and bwrite commands that invoke a caching mechanism.
Considerations for Writing a Driver
If you read my first article, you may remember that to make functions available to your code, you must include a header within the declarations at the start of your program. You also may remember that Linux allows far fewer functions in kernel drivers than in user-space drivers. When you’re writing code for kernel drivers, you can only include headers from the groups asm/*.h and linux/*.h, or headers for kernel-level function libraries you’ve already written. Never include standard libraries such as stdio.h. Most driver source programs start off with
#include <LINUX kernel.h>
to define kernel level operations,
followed by
#include <LINUX module.h>
to accommodate modular code.
After these opening statements, your software must handle module and kernel version support. Current kernels automatically include version support in the linux/module.h. header by referencing linux/version.h. If your code comprises more than one module, you must restrict the scope of the global variable that linux/module.h returns by including
#define __NO_VERSION__
and then
#include <LINUX version.h>
in all your source code after the first module.
The kerneld daemon requires the kernel-compilation option for version support, which you can examine by testing the CONFIG_ MODVERSIONS flag with a code segment such as
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <LINUX modversions.h>
#endif
The linux/modversions.h header contains preprocessor symbols for all the public kernel symbols. Symbols that the kernel exports to modules have their names modified (or “mangled,” in C++ terminology) so modules will notice any changes in kernel structures. These mechanisms prevent kernels from loading incompatible modules that may cause a kernel to “panic,” or, worse, to crash. (Actually, Linux kernels rarely crash.)
Typically, when a module misbehaves, the kernel generates an exception and “kills” the offending process. The kernel also writes an “oops” message to the var/log/messages log file, to report the processor state at the time of the fault. When you’re ready to debug, you can use the ksymoops tool to decode “oops” messages. You can also use printk, the kernel-level equivalent of printf, to write to the var/log/messages file.3
After handling CONFIG_MODVERSIONS, the next part of your module’s code should include any other headers you need, for example, the linux/fs.h header that makes operations in the file_operations structure available to read and write from a device. After making your declarations and definitions, code your driver’s functions. Remember, there’s no main() in a kernel-space routine. Finally, include the init_module and cleanup_module statements that allow the kernel to register your module and release resources when the OS unloads the module.
When you’re ready to compile a driver using the standard Linux GNU C-language compiler, gcc, the compiler requires several directives—also called flags and switches—to properly enable kernel-level compilation. You can compile kernel code from the command line with a line similar to
gcc -O -DMODULE -D__KERNEL__ -c mydriver.c -o mydriver
Due to a quirk in the GNU C compiler, you must turn on optimization with the -O switch. If you don’t, the compiler won’t expand some kernel-specific macros. The module and kernel references explicitly declare what you’re about to compile, and the -c switch builds linkable code, without invoking the linker. The -o switch directs output to a named object file, otherwise you’ll get a default file called a.out. The line above contains about the minimum number of compiler directives you can get away with.
You can compile programs from the command line with a statement like the one above. Alternatively, you can write a batch file, called a makefile script, to compile code. Then, you use GNU’s make utility to run the script and compile your program. Just store your source mydriver.c and your makefile in the same directory, and run the make utility from the command line. Unfortunately, the Linux documentation offers few instructions on getting started with the GNU tools—somehow you’re expected to know how to use the compiler and related tools. The references and sources listed in “For Further Reading,” p. 28, will help you get started.
Compile a Real Driver
After writing the obligatory “hello world” test program, it’s traditional for beginning driver programmers to compile a character driver that contains a readable buffer. To better understand what’s involved, download the code listings from the T&MW Web site. The file contains four listings: Listing 1, a simple device driver; Listing 2, the corresponding makefile source; and Listings 3a and 3b, a shell script loader and unloader. You can compile the code in Listing 1 using the makefile in Listing 2. You can then access the driver directly from the Linux command line. If you wish, you can write an application that actually calls the driver. To exercise the example driver, follow the steps below:
1. Change the active directory to something suitable for testing, such as /home/your_name/test.
2. Copy Listing 1 into a file called mydriver.c and copy Listing 2 into a file called makefile (with no extension).
3. Compile Listing 1 by typing make on the command line.
4. Switch to root (su) and type /sbin/insmod mydriver at the command line.
5. Type cat /proc/devices at the command line. You should see mydriver with an xxx major number prefix listed under “Character Devices.” Write down the major number.
6. Type mknod /dev/mydriver c xxx 0 on the command line, inserting the major number for xxx. This step produces a device driver file that you can access.
7. You can now read the message in the device file buffer using the command cat /dev/mydriver.
8. Type /sbin/rmmod mydriver at the command line to unload the driver from /proc/devices.
9. Type rm -f /dev/mydriver on the command line to remove the now-unused node from /dev.
10. Finally, examine the contents of the file /var/log/messages for any messages about unusual behavior of the driver.
One efficient way to automate loading and unloading drivers is to use a shell script. A shell script is a text file that contains commands that direct your shell to perform a set of actions. The standard Linux shell is bash, or “Bourne again shell,” after the author of the original Bourne shell. The bash shell is a programming environment in its own right.4 You can examine an example of a loader in Listing 3a and an unloader in Listing 3b. The bash shell’s source command loads and executes the contents of the filename that immediately follows. If you place your loader in your development directory with the files you want to load, you can see what happens by entering source load.sh from the command line. (When you’re ready to write application code to call your driver, see “Writing Application Code”)
Writing device drivers involves more issues than I can explore here. I haven’t mentioned reserving memory, how to handle interrupts, or tricky issues such as writing re-entrant driver code. For these and other details, I recommend the book Linux Device Drivers.5 Because Linux undergoes continuous development, you should keep up with changes that can affect device drivers. T&MW
| Author’s Notes
In this article, I’ve assumed you have a PC running a Linux distribution with kernel version 2.2.x (currently at version 2.2.16). I also assume you have some C-programming experience. I wrote this article using a PC running Red Hat 6.1 Linux that uses kernel version 2.2.12. My path names reflect my Linux setup. Remember that Linux is a moving target. Various operations depend on kernel revision levels. To research this article, I regularly ran four windows in Linux to switch between programs such as Sun’s StarOffice suite and multiple instances of text editors, file navigators, and shell windows. I’ve done some stunningly stupid things while I’ve been poking about, logged in at root. One day, our office cleaner even turned the Linux machine off. Yet, this Linux box has never crashed and keeps on running. Meantime, my NT box crashed doing nothing more than keeping an Ethernet connection open, and it took me six hours to fix. Yes, I’m a Linux fan, too.—David Marsh |
FOOTNOTES
1. Marsh, David, “Understand Linux Device Drivers,” Test & Measurement World, April 15, 2000, pp. 6–15.
2. Ivchenko, Alex, “Get Those Boards Talking Under Linux, Part 1,” Test & Measurement World, May 2000, pp. 36–50.
3. Ivchenko, Alex, “Get Those Boards Talking Under Linux, Part 2,” Test & Measurement World, June 2000, pp. 47–62.
4. Newham, Cameron, and Bill Rosenblatt, Learning the bash Shell, 2nd ed., O’Reilly & Associates, Sebastopol, CA, 1998.
5. Rubini, Alessandro, Linux Device Drivers, O’Reilly & Associates, Sebastopol, CA, 1998.
FOR FURTHER READING
Holmes, Steve, “C Programming,” University of Strathclyde Computer Centre, Glasgow, Scotland. An online course in C programming. www.strath.ac.uk/CC/Courses/NewCcourse/ccourse.html.
“How to Write a Makefile,” vertigo.hsrl.rutgers.edu/ug/make_help.html.
Schuresko, Mike, “Mike’s Makefile Tutorial,” www.andrew.cmu.edu/user/mds2/Makefile.html.
Siever, Ellen, et al., Linux in a Nutshell, 3rd ed., O’Reilly & Associates, Sebastopol, CA, 2000.
Wall, Kurt, Mark Watson, and Mark Whitis, Linux Programming Unleashed, Sams, Indianapolis, IN, 1999.
You can get kernel release information at www.kernelnotes.org.
ACKNOWLEDGMENT
The author gratefully acknowledges help and suggestions from Alex Ivchenko at United Electronic Industries (Watertown, MA, www.ueidaq.com).
David Marsh is a freelance writer with more than 20 years experience in the electronics industry. He also works as a contributing editor for EDN magazine. E-mail: forncett@compuserve.com.
| Writing Application Code
When you’re ready to write application code to call your driver, your calls take the following form: To open the device: To close the device: To write to the device: |















