One of the purposes of an operating system is to hide the peculiarities of the system’s hardware devices from its users. For example the Virtual File System presents a uniform view of the mounted filesystems irrespective of the underlying physical devices. This chapter describes how the Linux kernel manages the physical devices in the system.
The CPU is not the only intelligent device in the system, every physical device has its own hardware controller. The keyboard, mouse and serial ports are controlled by a SuperIO chip, the IDE disks by an IDE controller, SCSI disks by a SCSI controller and so on. Each hardware controller has its own control and status registers (CSRs) and these differ between devices. The CSRs for an Adaptec 2940 SCSI controller are completely different from those of an NCR 810 SCSI controller. The CSRs are used to start and stop the device, to initialize it and to diagnose any problems with it. Instead of putting code to manage the hardware controllers into every application, the code is kept in the Linux kernel. The software that handles or manages a hardware controller is known as a device driver. The Linux kernel device drivers are, essentially, a shared library of privileged, memory resident, low level hardware handling routines. It is Linux’s device drivers that handle the peculiarities of the devices they are managing.
Linux supports three types of hardware devices: character, block and network. Character devices are read and written directly without buffering, for example the system’s serial ports /dev/cua0 and /dev/cua1. Block devices can only be written to and read from in multiples of the block size, typically 512 or 1024 bytes. Block devices are accessed via the buffer cache and may be randomly accessed, that is to say, any block can be read or written no matter where it is on the device. Block devices can be accessed via their device special file but more commonly they are accessed via the file system. Only a block device can support a mounted file system. Network devices are accessed via the BSD socket interface and the networking subsytems described in the section on networking.
There are many different device drivers in the Linux kernel (that is one of Linux’s strengths) but they all share some common attributes:
- Kernel code
- Device drivers are part of the kernel and, like other code within the kernel, if they go wrong they can seriously damage the system. A badly written driver may even crash the system, possibly corrupting file systems and losing data.
- Kernel interfaces
- Device drivers provide a standard interface to the Linux kernel or to the appropriate subsystem. For example, the terminal driver provides a file I/O interface to the Linux kernel and a SCSI device driver provides a SCSI device interface to the SCSI subsystem which, in turn, provides both file I/O and buffer cache interfaces to the kernel.
- Kernel mechanisms and services
- Device drivers make use of standard kernel services such as memory allocation, interrupt delivery and wait queues to operate.
- Loadable
- Most of the Linux device drivers can be loaded on demand as kernel modules when they are needed and unloaded when they are no longer being used. This makes the kernel very adaptable and efficient with the system’s resources.
- Configurable
- Linux device drivers can be built into the kernel. Which devices are built is configurable when the kernel is compiled.
- Dynamic
- As the system boots and each device driver is initialized it looks for the hardware devices that it will control. If those devices do not exist (maybe are just not found), the device driver is simply redundant and causes no harm apart from occupying a little of the system’s memory.
Memory
Device drivers have to be careful when using memory. As they are part of the Linux kernel they cannot use virtual memory. Each time a device driver runs, maybe as an interrupt is received or as a bottom half or task queue handler is scheduled, the current process may change. The device driver cannot rely on a particular process running even if it is doing work on its behalf. Like the rest of the kernel, device drivers use data structures to keep track of the device they controll. The kernel would become unnecessarily large if these data structures were statically allocated, part of the device driver’s code. Most device drivers allocate kernel, non-paged, memory to hold their data.
Linux provides kernel memory allocation and deallocation routines and it is these that the device drivers use. Kernel memory is allocated in chunks that are powers of 2. For example 128 or 512 bytes, even if the device driver asks for less. The number of bytes that the device driver requests is rounded up to the next block size boundary. This makes kernel memory deallocation easier as the smaller free blocks can be recombined into bigger blocks.
It may be that Linux needs to do quite a lot of extra work when the kernel memory is requested. If the amount of free memory is low, physical pages may need to be discarded or written to the swap device. Normally, Linux would suspend the requestor, putting the process onto a wait queue until there is enough physical memory. Not all device drivers (or indeed Linux kernel code) may want this to happen and so the kernel memory allocation routines can be requested to fail if they cannot immediately allocate memory.
The device driver can also specify that it wants to DMA to and from the memory it allocates. It is the Linux kernel, not the device driver, however, that takes care of the details. This way it is the Linux kernel rather than the device driver decides what constitutes DMA’able memory for the system.
Interfacing Device Drivers with the Kernel
To ensure that access is always done in the correct manner, the Linux kernel must be able to interact with device drivers in standard ways. Each class of device driver, character, block and network, provides common interfaces that the kernel uses when requesting services from them. These common interfaces mean that the kernel can treat often very different devices and their device drivers absolutely the same. For example, SCSI and IDE disks behave very differently but the Linux kernel uses the same interface to both of them.
Linux is very dynamic, every time a Linux kernel boots it may encounter different physical devices and thus need different device drivers. Linux allows you to include device drivers at kernel build time via its configuration scripts. When these drivers are initialized at boot time they may not discover any hardware to control. Other drivers can be loaded as kernel modules when they are needed. To cope with this dynamic nature of device drivers, device drivers register themselves with the kernel as they are initialized. Linux maintains tables of registered device drivers as part of its interfaces with them. These tables include pointers to routines and information that supports the interface with the device class.
Character Devices
Figure: Character Devices
Character devices, the simplest of Linux’s devices, are accessed as files, applications use standard system calls to open them, read from them, write to them and close them exactly as if the device were a file. This is true even if the device is a modem being used by the PPP daemon to connect a Linux system onto a network. As a character device is initialized its device driver registers itself with the Linux kernel by adding an entry into the chrdevs vector of device_struct data structures. The device’s major device identifier (for example 4 for the tty device) is used as an index into this vector. The major device identifier for a device is fixed.
Each entry in the chrdevs vector, a device_struct data structure contains two elements: a pointer to the name of the registered device driver and a pointer to a block of file operations. This block of file operations is itself the addresses of routines within the character device driver, each of which handles specific file operations such as open, read, write and close. The contents of /proc/devices for character devices is taken from the chrdevs vector.
When a character special file representing a character device (for example /dev/cua0) is opened, the kernel must set things up so that the correct character device driver’s file operation routines will be called. Just like an ordinairy file or directory, each device special file is represented by a VFS inode . The VFS inode for a character special file, indeed for all device special files, contains both the major and minor identifiers for the device. This VFS inode was created by the underlying filesystem, for example EXT2, from information in the real filesystem when the device special file’s name was looked up.
Each VFS inode has associated with it a set of file operations and these are different depending on the filesystem object that the inode represents. Whenever a VFS inode representing a character special file is created, its file operations are set to the default character device operations.
This has only one file operation, the open file operation. When the character special file is opened by an application the generic open file operation uses the device’s major identifier as an index into the chrdevs vector to retrieve the file operations block for this particular device. It also sets up the file data structure describing this character special file, making its file operations pointer point to those of the device driver. Thereafter all of the applications file operations will be mapped to calls to the character devices set of file operations.