Module 4 · Unit 11 · Lesson 4 of 7
Device Drivers
The previous three lessons presented the I/O subsystem's hardware: the devices, the controllers, the buses, the interrupts, and the DMA machinery. This lesson presents the software layer that turns the hardware's details into the kernel's abstractions: the device driver. A driver is the kernel module that knows a specific device — its registers, its commands, its interrupt behaviour, its DMA capabilities — and that translates the kernel's generic requests into the device's specific operations. Drivers are the largest part of a modern kernel's source code, and their design determines the system's reliability, its performance, and its ability to support new hardware.
01 Introduction
Lesson 11.1 presented the I/O subsystem's layers and placed the device driver between the kernel's device-independent software and the device's controller. Lesson 11.2 presented the interrupt mechanism that the driver uses to learn of a device's completion, and Lesson 11.3 presented the DMA machinery that the driver uses to move the device's data. This lesson examines the driver itself: the kernel module that knows how to operate a specific device, and that bridges the kernel's abstractions and the device's registers.
The driver's role is translation. The kernel
above the driver presents a uniform interface: the
virtual file system's read,
write, open, and
close operations; the block layer's
requests; the network stack's packets. The device
below the driver presents a specific interface: a set
of registers, a command format, a protocol for
reporting completion, and a set of capabilities. The
driver's job is to translate between the two. It
receives a generic request — "read 4 KB from offset
1 MB" — and turns it into the device's specific
sequence of register writes, DMA descriptors, and
interrupt handling. It receives the device's
completion — a status register's value, a DMA
descriptor's completion — and turns it into the
kernel's generic signal — a request's completion, a
process's wakeup.
The translation is the reason drivers dominate a modern kernel's code. A device's interface is not standardized: two storage controllers may implement the same protocol (AHCI, NVMe) but with different register layouts, different command formats, and different quirks; two network adapters may implement the same Ethernet standard but with different descriptor formats and different offload capabilities. Each device needs its own driver (or, more precisely, each device or family of devices needs a driver that knows its details), and the drivers together account for the majority of the Linux kernel's source lines. The observation is why the kernel provides so much infrastructure — the device model, the bus types, the DMA API, the interrupt API, the locking primitives — to make the drivers' work manageable.
This lesson presents the device driver. It defines the
driver and its role. It presents the kernel module
mechanism — the loadable modules that let the kernel
support new devices without being recompiled — and
the driver's structure: its initialization, its
probe and remove functions, and its file operations.
It presents the three device classes' interfaces: the
character device's file_operations, the
block device's request queue and the multi-queue
block layer, and the network device's
net_device and socket buffer. It
presents the driver's use of interrupts and DMA, and
the Linux device model — the bus, device, driver, and
class abstractions that let the kernel match devices
to drivers and expose them in sysfs. It
presents the practical concerns of driver
development: the locking rules, the sleeping rules,
the error handling, and the debugging tools. It
presents the user-space alternatives — FUSE, UIO,
VFIO, DPDK — that move the driver out of the kernel,
and it closes with the design trade-offs that shape
the driver's structure.
Prerequisites
This lesson builds on Lessons 11.1, 11.2, and 11.3. The I/O subsystem's layers, the device controller and its registers, the interrupt mechanism and the top-half / bottom-half split, and the DMA machinery and its address-mapping solutions are all assumed. The kernel-mode and user-mode distinction from Lesson 1.4 and the system-call interface from Lesson 1.4 are the basis for the discussion of the driver's interface. The virtual file system from Lesson 10.1 and the block layer's request queue from Lesson 10.5 are the context in which the character and block drivers operate. No new operating-system concepts are introduced; the lesson is about the driver's structure and its responsibilities. Familiarity with the Linux command line from Lesson 2.2 and with C programming is helpful, since the interactive activity involves reading and building a small kernel module.
02 Learning objectives
By the end of this lesson you will be able to
- Define the device driver and explain its role as the kernel's translation layer between the device-independent software and the device's controller.
- Describe the kernel module mechanism and explain how loadable modules let the kernel support new devices.
- Describe the driver's structure: initialization, probe, remove, and the file operations that the kernel calls.
- Compare the character, block, and network device drivers' interfaces and explain when each is used.
- Describe the driver's use of interrupts and DMA, and the Linux device model's bus, device, driver, and class abstractions.
- Explain the practical concerns of driver development — locking, sleeping, error handling — and describe the user-space alternatives (FUSE, UIO, VFIO, DPDK).
This lesson contributes to U11.4 and U11.5, and to course learning outcomes CLO1, CLO6, and CLO8.
03 Required reading
- Operating System Concepts — Chapter 12, I/O Systems, Section 12.5, I/O Requests to Hardware Operations, and Section 12.6, STREAMS. The standard treatment of the I/O request's path through the kernel and the driver's role.
- OSTEP — Chapter 36, I/O Devices. OSTEP's discussion of the device's interface and the operating system's role in managing it is the clearest available account, and it is the basis for the worked example in this lesson.
- Tanenbaum & Bos — Modern Operating Systems, Chapter 5, Sections 5.2–5.4. A complementary treatment that develops the device driver's structure and its interaction with the kernel from first principles.
- Corbet, J., Rubini, A., & Kroah-Hartman, G. — Linux Device Drivers, 3rd ed. O'Reilly, 2005. The definitive guide to Linux driver development. Chapters 1–3 (the driver's structure and the character device interface), Chapters 4–6 (locking, advanced character drivers, and the ioctl interface), Chapters 10–12 (interrupt handling, the kernel data types, and the PCI interface), and Chapter 14 (the Linux device model).
- Love, R. — Linux Kernel Development, 3rd ed., Chapter 14, Block Devices I/O, and Chapter 16, The Block I/O Layer. The Linux kernel's block-device structures, the request queue, and the multi-queue block layer.
- Bovet, D. P., & Cesati, M. — Understanding the Linux Kernel, 3rd ed. O'Reilly, 2005. Chapter 13, I/O Architecture and Device Drivers. The Linux kernel's device model, bus structures, and the driver's registration.
-
Kerrisk, M. — The Linux
Programming Interface, Chapter 14, File
Systems, and Chapter 15, File
Attributes. The application's view of the
device files and the
ioctlinterface. -
Linux man pages:
man 2 ioctl,man 4 mem,man 4 null,man 4 zero,man 4 random,man 4 tty,man 4 sd,man 5 sysfs,man 8 insmod,man 8 rmmod,man 8 modprobe,man 8 lsmod,man 8 modinfo,man 1 udevadm,man 8 lspci,man 8 lkddb.
Reading strategy
Read the OSTEP chapter first for the device's
interface and the operating system's role in
managing it. Then read the Silberschatz section for
the formal path of an I/O request — from the
application's system call through the device-
independent software, the driver, and the
interrupt handler. The Linux kernel chapters
(Corbet et al. and Love) are the practical view:
they show the driver's structure, the interfaces it
implements, and the rules it must follow. The
device-driver book (Corbet et al.) is the most
detailed, and it is worth reading in full if you
plan to write a driver; its chapters on the
character device interface, the interrupt handling,
and the DMA API are the most important. The
Tanenbaum chapter is the complementary view, and it
is worth reading for the discussion of the driver's
place in the kernel's layering. Finally, the Linux
man pages are the practical tools: lsmod
and modinfo show the loaded modules,
insmod and rmmod load and
unload them, and sysfs shows the
device model's structure. Pay particular attention
to the distinction between the driver
(the code that knows the device) and the
device (the hardware itself). The driver
is a piece of software that may support several
devices (a family of network adapters, a class of
storage controllers), and a device may be driven by
one of several drivers (a generic driver and a
vendor-specific one). The matching between the two
is the device model's job, and its design shapes
the kernel's support for new hardware.
04 Core content
4.1 What a device driver is
A device driver is a piece of kernel code that knows how to operate a specific device (or a family of devices) and that presents the device to the rest of the kernel through one of the kernel's standardized interfaces. The driver is the kernel's translation layer: it receives the generic requests that the kernel's upper layers produce, and it turns them into the device-specific operations that the device's controller requires.
The driver's position is between two very different interfaces:
-
The interface above the driver is
the kernel's abstraction. For a storage device, the
interface is the block layer's request queue: the
driver receives a request that describes a
read or a write of a range of blocks. For a
character device, the interface is the file
operations: the driver receives
open,read,write,ioctl, andclosecalls from the kernel's VFS layer. For a network device, the interface is the network stack's packet interface: the driver receives a packet to transmit or delivers a received packet. The interface is the same for all devices of the same class, so the kernel's upper layers do not need to know which device they are using. - The interface below the driver is the device's hardware. For a storage controller, the interface is a set of registers: a command register, a status register, a DMA address register, and so on. For a network adapter, the interface is a set of descriptor rings and control registers. For a USB device, the interface is a set of endpoints and a protocol. The interface is different for every device, so the driver must know the device's details.
The driver's translation is not merely mechanical. It must handle the device's quirks (a controller that requires a specific command order, an adapter that misreports its capabilities), its error conditions (a transfer that fails, a device that disappears), its timing constraints (a register that must be read within a specific window), and its concurrency (a device that can process several commands at once). It must do all of this while obeying the kernel's rules about locking, sleeping, and memory allocation. The driver is therefore one of the kernel's most demanding components to write, and its quality determines the system's reliability: a buggy driver can crash the kernel, corrupt data, or hang the system, and driver bugs are the source of a large fraction of the kernel's reported crashes.
4.2 Kernel modules and loadable modules
A modern kernel does not include every driver in its
base image. Instead, it uses loadable kernel
modules: pieces of kernel code that can be
loaded and unloaded at runtime, without recompiling
the kernel or rebooting the system. A module is a
compiled object file (a .ko file) that
contains the driver's code and a small amount of
metadata that describes what the module does and what
it needs.
The module mechanism has three advantages:
- Flexibility. A system can support a device by loading the device's module, without recompiling the kernel. The module can be built for the running kernel's version, so a system can be updated without rebuilding its drivers.
- Memory efficiency. A module is loaded only when it is needed. A system with a hundred possible devices loads only the modules for the devices it actually has, so the kernel's memory footprint is smaller.
- Development speed. A driver can be developed, tested, and debugged as a module, without the cycle of building and booting a new kernel. The module can be unloaded and reloaded after each change, so the development cycle is seconds rather than minutes.
The module mechanism's core is the kernel's module loader. The loader:
-
Reads the module's metadata. The
module's
.modinfosection describes the module's name, its author, its license, its parameters, and (most importantly) the devices it supports. The metadata is what lets the kernel match a device to a module. - Resolves the module's dependencies. A module may use symbols that other modules export (a driver may use the USB subsystem's functions, a file system may use the block layer's functions). The loader reads the module's undefined symbols, finds the modules that define them, and loads those modules first.
- Relocates the module's code. The module's code is compiled as position-independent (or with relocations), and the loader adjusts the addresses to the kernel's memory layout.
-
Calls the module's initialization
function. The module's
module_initfunction is called; it registers the driver with the kernel's device model and performs the module's other setup.
The module's unload is the reverse: the kernel calls
the module's module_exit function, which
unregisters the driver and releases the module's
resources; then the loader frees the module's memory.
The unload can fail if the module is in use (a file
system is mounted, a device is open), so the kernel
maintains a reference count for each module and
refuses to unload a module whose count is nonzero.
The module mechanism's interface to user space is the
modprobe, insmod,
rmmod, lsmod, and
modinfo commands, and the
/sys/module/ directory. The
udev daemon uses the module's metadata
to load the modules for the devices that appear, so
the system is self-configuring: the user plugs in a
device, udev recognizes it, and the
kernel loads the appropriate module. The mechanism is
one of the reasons Linux supports such a wide range
of hardware: the drivers are modules, and the
distribution ships modules for thousands of devices,
loading only the ones the system needs.
4.3 The driver's structure
A Linux driver has a recognizable structure, with four parts:
- Initialization and cleanup
-
The
module_initfunction runs when the module is loaded (or when the kernel boots, if the driver is built into the kernel). It registers the driver with the kernel's device model, allocates the driver's global data structures, and performs any global setup. Themodule_exitfunction runs when the module is unloaded; it reverses the initialization, unregisters the driver, and frees the global structures. The two functions are the module's entry and exit points, and their structure is defined by themodule_initandmodule_exitmacros. - Probe and remove
-
The
probefunction runs when the kernel matches the driver to a device. It is the driver's per-device initialization: it enables the device's registers (mapping them into the kernel's address space), allocates the device's per-device data structures, requests the device's interrupts and DMA channels, registers the device with the appropriate subsystem (the block layer, the network stack, the character device layer), and initializes the device's hardware. Theremovefunction runs when the device is removed (or when the module is unloaded); it reverses the probe's work and releases the device's resources. Theprobeandremovefunctions are the driver's per-device entry and exit points, and their structure is defined by the bus type (the PCI, USB, or platform bus's probe and remove signatures). - The interface implementation
-
The driver implements the interface that its device
class requires. A character driver implements the
file_operationsstructure's methods (open,read,write,ioctl,release, and so on). A block driver implements the block layer's interface (thequeue_rqmethod of theblk_mq_opsstructure). A network driver implements thenet_device_opsstructure's methods (ndo_start_xmit,ndo_open,ndo_stop, and so on). The interface is what the kernel's upper layers call, and its methods are the driver's actual work. - The interrupt and deferred-work handlers
- If the device generates interrupts, the driver implements the interrupt handler (the top half) and the deferred-work handler (the bottom half, as a softirq, a tasklet, or a workqueue; Lesson 11.2). The top half acknowledges the device and schedules the bottom half; the bottom half processes the completed work. The driver's interrupt handling is where the device's asynchronous behaviour is translated into the kernel's request completion.
The four parts' interaction is the driver's lifecycle. The module's initialization registers the driver; the kernel's device model matches the driver to a device and calls the probe; the probe sets up the device and registers the driver's interface; the kernel's upper layers call the interface's methods; the device's interrupts and DMA completions drive the driver's handlers; the device's removal (or the module's unload) calls the remove; the module's cleanup unregisters the driver. The lifecycle is the same for every driver, regardless of the device's class, and its structure is the kernel's contribution to making the drivers' work manageable.
file_operations, the block device's
blk_mq_ops and request queue, and the
network device's net_device_ops and
net_device. Each class's interface is
the kernel's contract with the driver, and the
driver must implement the class's methods to be
usable by the kernel's upper layers.
4.4 Character device drivers
A character device is a device that
transfers data as a stream of bytes and does not
support random access. Its interface is the
file_operations structure, a set of
function pointers that the kernel's VFS layer calls
when an application performs a file operation on the
device's node.
The file_operations structure's most
important methods are:
open- Called when an application opens the device's node. The method initializes the device's per-open state (or the device's global state, if the device can be opened only once), checks the requested access mode (read, write, or both), and returns a file descriptor. The method is the device's initialization point for each open.
read-
Called when an application reads from the device.
The method transfers data from the device to the
application's buffer, using the kernel's
copy_to_userfunction (which validates the user-space address and performs the copy with the necessary protections). The method returns the number of bytes transferred, or a negative error code. write-
Called when an application writes to the device. The
method transfers data from the application's buffer
to the device, using the kernel's
copy_from_userfunction. The method returns the number of bytes transferred, or a negative error code. ioctl-
Called when an application performs a device-
specific operation. The method's arguments are a
command number and an optional argument; the
command number's format is defined by the kernel
(the
_IO,_IOR,_IOW, and_IOWRmacros), and the method's implementation interprets the command. Theioctlinterface is the device's escape hatch for the operations that the read and write interface cannot express (setting the device's mode, querying its status, performing a control operation). release- Called when the last reference to the device's open file is closed. The method releases the per-open state and (if the device is exclusive) allows the next open.
mmap-
Called when an application maps the device's memory
into its address space (through the
mmapsystem call). The method sets up the mapping, typically by using the kernel'sremap_pfn_rangefunction to map the device's physical pages (the device's memory-mapped registers or its frame buffer) into the application's address space. The method is used by the devices that expose their memory to applications: graphics cards, frame buffers, high-speed network adapters, and the devices that support user-space drivers.
The character device's interface is deliberately
simple, and its simplicity is its strength: an
application can use the device with the same
open, read,
write, and close calls that
it uses for a file, and the device's driver does not
need to implement any special protocol. The
interface's simplicity is also its limitation: the
character device's operations are a stream, so the
device cannot be accessed randomly, and the
ioctl interface (which the driver must
implement for the device-specific operations) is
notoriously difficult to design well (its commands
are not standardized, its arguments are untyped, and
its implementation is a common source of bugs).
4.5 Block device drivers
A block device is a device that
transfers data in fixed-size blocks and supports
random access. Its interface is the block layer's
request queue, and its driver implements the
blk_mq_ops structure (the multi-queue
block layer's operations).
The block layer's design has evolved significantly.
The classical block layer (the single-queue
block layer) had a single request queue per device,
and the requests were processed by a single
request_fn function. The single queue was
a bottleneck on high-performance devices: the
queue's lock serialized the requests, and the
single function could not use the device's
parallelism. The modern block layer (the
multi-queue block layer, introduced in
Linux 3.13) has a two-level structure:
- The software queues. One queue per processor core (or per application's submission context). The queue is lockless (or uses a per-queue lock), so the submissions from different cores do not contend.
-
The hardware queues. One queue per
device's hardware queue (the device's DMA queue, the
NVMe's queue pair, the SCSI controller's queue).
The hardware queue is what the driver's
queue_rqmethod consumes.
The multi-queue block layer's structure is the reason
modern storage devices achieve their throughput: the
submissions from different cores go to different
software queues, and the software queues are mapped
to the hardware queues (typically one per core, if the
device has enough queues), so the submissions and the
completions do not contend. The mapping is
configurable (the nr_requests,
scheduler, and
rq_affinity parameters in
/sys/block/<dev>/queue/), and the
administrator can tune it for the workload.
The block driver's queue_rq method is the
driver's core: it receives a request (a
struct request that describes the
operation, the blocks, and the buffer's scatter-
gather list), translates it into the device's command
format, and submits it to the device. The driver's
interrupt handler (or its polling function) detects
the completion and calls the block layer's completion
function, which processes the request and wakes the
waiting process. The block layer's role is to merge
the adjacent requests, to schedule them (the I/O
scheduler, Lesson 11.5), and to track their
completion; the driver's role is to translate them
into the device's operations.
4.6 Network device drivers
A network device is a device that
transfers data as packets and is addressed by network
address rather than by block or byte offset. Its
interface is the network stack's packet interface, and
its driver implements the
net_device_ops structure.
The network driver's interface differs from the character and block drivers' interfaces in three ways:
-
The unit of transfer is a packet, not a
stream or a block. The driver receives a
packet to transmit (through the
ndo_start_xmitmethod) and delivers a received packet to the kernel (by calling the network stack'snetif_rxornapi_gro_receivefunctions). The packet's format is defined by the network protocol (Ethernet, IP, TCP), and the driver's role is to move the packet's bytes between the device and the kernel's socket buffers. -
The interface is not a file. A
network device does not have a device node in
/dev; its interface is the socket API, and the kernel's network stack is the layer above the driver. An application does notopena network device; it creates a socket, and the kernel's network stack routes the socket's packets to the appropriate device. -
The receive path is asynchronous.
The driver does not know when a packet will arrive;
the device's interrupt (or the driver's polling, in
the NAPI scheme; Lesson 11.2) delivers the packet
to the driver, and the driver passes it to the
network stack. The transmit path is also
asynchronous: the driver's
ndo_start_xmitmethod queues the packet, and the device's completion (an interrupt or a completion descriptor) frees the packet's buffer.
The network driver's structure is shaped by these differences. The driver maintains a ring of receive buffers (the receive descriptors), which the device's DMA engine fills with the received packets; the driver's interrupt handler (or its NAPI polling function) processes the descriptors and passes the packets to the network stack. The driver also maintains a ring of transmit descriptors, which the driver fills with the packets to transmit; the device's DMA engine reads the descriptors and transmits the packets, and the driver's completion handler (an interrupt or a transmit-completion descriptor) frees the packets' buffers. The rings' design is the driver's most important performance decision, and the modern adapters' multiple queues (one per processor core, or one per traffic class) are the source of their throughput.
4.7 The Linux device model
The Linux device model is the kernel's framework for representing the system's devices and their relationships. Its purpose is to let the kernel match devices to drivers, to expose the devices' structure to user space, and to manage the devices' power and their hot-plug events. The model's abstractions are the bus, the device, the driver, and the class:
- Bus
- A bus is a type of connection that devices can be attached to: the PCI bus, the USB bus, the SCSI bus, the platform bus (for the devices that are built into the system on a chip and that have no discoverable bus). Each bus type defines how its devices are identified (the PCI vendor and device IDs, the USB class codes, the device tree's compatible strings), how they are enumerated (the PCI configuration space, the USB descriptors, the ACPI tables), and how they are matched to drivers.
- Device
-
A device is a physical or virtual component that is
attached to a bus. Its representation (the
struct device) records its parent, its bus, its driver, its power state, and its properties. The device's structure is the framework's central object: the model's other abstractions refer to it, and its registration is what makes the device visible to the kernel's other subsystems. - Driver
-
A driver is a piece of code that can operate a
device. Its representation (the
struct device_driver) records the driver's name, its bus, its probe and remove functions, and its table of supported devices (the IDs that the driver can handle). The driver's registration adds it to the bus's list, and the bus's matching logic connects it to the devices whose IDs it supports. - Class
-
A class is a type of device that provides a
particular interface to user space: the
blockclass (the block devices), thenetclass (the network interfaces), thettyclass (the terminals and serial ports), theinputclass (the keyboards, mice, and other input devices). A class groups the devices that share an interface, regardless of their bus, and it is the basis for the/sys/class/hierarchy and for theudevrules that create the device nodes.
The model's sysfs filesystem is its
user-space representation. The
/sys/ directory mirrors the model's
structure: /sys/bus/ contains the bus
types, /sys/devices/ contains the
devices' hierarchy, /sys/class/ contains
the device classes, and /sys/module/
contains the loaded modules. Each device and driver
has a directory with its attributes: the device's
name, its driver, its power state, and its
device-specific attributes (the values that the
driver exposes for the administrator's inspection
and control). The sysfs files are the administrator's
interface to the device model: a read of
/sys/class/net/eth0/speed returns the
interface's speed, a write to
/sys/block/sda/queue/scheduler changes
the device's I/O scheduler, and a read of
/sys/bus/pci/devices/0000:00:1f.2/driver
returns the name of the device's driver.
The model's matching logic is its central mechanism.
When a device is registered, the bus's
match function compares the device's ID with
each of the bus's drivers' ID tables. When a match is
found, the bus calls the driver's probe
function, which initializes the device. When a driver
is registered, the same logic runs in reverse: the
bus compares the driver's ID table with each of the
bus's devices, and calls the driver's
probe for each match. The matching is
what makes the system self-configuring: the user
plugs in a device, the bus enumerates it, the model
matches it to a driver, and the driver initializes
it. The mechanism is the reason a modern Linux
system can support thousands of devices without the
administrator's intervention.
4.8 Device tree and ACPI
The device model needs to know which devices exist before it can match them to drivers. On a discoverable bus (PCI, USB, SCSI), the hardware itself reports the devices' presence and their IDs, so the enumeration is the bus's job. On a non-discoverable bus (the platform bus, the buses on many embedded systems), the hardware does not report the devices' presence, and the system's firmware must describe them. The two firmware descriptions are the device tree and ACPI.
The device tree is a data structure
(a tree of nodes and properties) that describes the
system's hardware. It is used on the ARM, PowerPC,
and RISC-V systems (and on some x86 systems), and it
is the standard for the embedded systems and the
systems-on-chip. The tree's nodes describe the
devices: the CPUs, the memory, the buses, and the
devices attached to them. Each node has a
compatible property (a list of strings
that identify the device's type and its specific
model), and the kernel's drivers declare the
compatible strings they support. The matching between
the two is what connects the devices to their
drivers, and it is the device tree's counterpart to
the PCI ID table.
ACPI (Advanced Configuration and
Power Interface) is the x86 systems' firmware
interface. Its tables describe the system's devices,
their resources (the memory and I/O regions they
use, the interrupts they generate), and their power
management. The kernel reads the tables during its
initialization and uses them to enumerate the
platform devices and to configure their resources.
The ACPI tables' _HID and
_CID properties are the counterparts of
the device tree's compatible strings,
and the kernel's drivers declare the ACPI IDs they
support. The ACPI's power management interface (the
_PS0, _PS3, and
_DSM methods) is what the kernel uses to
suspend and resume the devices, and it is the basis
for the system's power management.
The two mechanisms serve the same purpose on different platforms, and their design reflects their platforms' histories. The device tree is a static description (the tree is built at boot time and does not change), so it is suitable for the embedded systems whose hardware is fixed. ACPI is a dynamic interface (the firmware provides methods that the kernel can call to query and control the devices), so it is suitable for the general-purpose systems whose hardware can change (a laptop with a docking station, a server with hot-plug devices). The kernel abstracts the difference with the fwnode (firmware node) interface, so a driver can be written once and used on both platforms.
4.9 The driver's practical concerns
A driver's code must obey the kernel's rules, and the rules are stricter than a user-space program's. The three most important are the locking rules, the sleeping rules, and the error-handling rules.
Locking. A driver's code can run concurrently on several processor cores, and its interrupt handlers can run concurrently with its process-context code. The shared data structures must therefore be protected. The kernel provides several primitives:
- Spinlocks protect the short critical sections that the interrupt handlers share with the process-context code. A spinlock busy- waits, so it must be held for a very short time (a few instructions to a few dozen), and the code that holds it must not sleep.
- Mutexes protect the longer critical sections that only the process-context code accesses. A mutex sleeps when it cannot be acquired, so the code that holds it may sleep and may block; the code must not hold a mutex in an interrupt handler.
- Atomic operations protect the single variables that the driver shares (a counter, a flag) without the overhead of a lock.
- Per-CPU data avoids the locks entirely for the data that each processor core accesses independently (the statistics, the queue indices).
The locking rules are the driver's most common source
of bugs. A driver that accesses a shared structure
without a lock is a driver with a race condition; a
driver that holds a spinlock for too long is a driver
with high latency; a driver that sleeps while holding
a spinlock is a driver that can deadlock the system.
The kernel's debugging tools (the
CONFIG_DEBUG_SPINLOCK,
CONFIG_DEBUG_MUTEXES, and
CONFIG_PROVE_LOCKING options, and the
lockdep validator) are designed to
catch the bugs before they reach production, and the
driver's developer should enable them.
Sleeping. The driver's code runs in
one of two contexts: the process context
(the code that runs on behalf of a system call or a
kernel thread) and the interrupt context
(the code that runs in an interrupt handler or a
softirq; Lesson 11.2). The process context can sleep
(it can block, wait for a condition, allocate memory
with GFP_KERNEL); the interrupt context
cannot (there is no process to wake, and the
scheduler's state cannot be saved). The driver's
code must therefore be written with its context in
mind: the code that runs in the interrupt context
must use only the non-blocking operations (the
GFP_ATOMIC memory allocation, the
spin_lock primitives, the
udelay function), and the code that
needs to sleep must be deferred to the process
context (a workqueue, a kernel thread).
Error handling. The driver's code
must handle the device's errors, and the errors are
common: a transfer that times out, a device that
reports an error, a device that disappears. The
driver's error handling must be robust (the device's
failure must not crash the kernel), it must be
complete (the resources that the driver allocated for
the failed operation must be released), and it must
be informative (the error must be reported to the
administrator through the kernel's log, with enough
detail to diagnose the problem). The kernel's
dev_err, dev_warn, and
dev_info macros are the standard
reporting interfaces, and their use is expected of
every driver.
4.10 User-space drivers and the alternatives
Not every driver must be in the kernel. The kernel's rules (the locking, the sleeping, the error handling) are demanding, and a buggy driver can crash the system. Several frameworks move the driver's code to user space, where a bug crashes only the driver's process and the development is easier. The frameworks differ in what they move and what they leave in the kernel:
- FUSE (Filesystem in Userspace)
- A framework that lets a user-space program implement a file system. The kernel's FUSE driver forwards the VFS's operations (the lookups, the reads, the writes) to the user-space program, which performs the operations and returns the results. The framework is used for the file systems that are easier to implement in user space (the network file systems, the archival file systems, the research prototypes), and its performance is lower than a kernel file system's because every operation requires a kernel-to-user-space round trip. The framework's advantage is its safety and its simplicity: a bug in the user-space program crashes the program, not the kernel, and the program can be written in any language.
- UIO (Userspace I/O)
- A framework that lets a user-space program drive a device. The kernel's UIO driver performs the device's minimal setup (the resource allocation, the interrupt registration) and exposes the device's registers and its interrupts to the user-space program. The program performs the device's operations (the register writes, the DMA setup), and the kernel's role is limited to the setup and the interrupt delivery. The framework is used for the devices whose drivers are easier to write in user space (the industrial I/O devices, the custom hardware), and its performance is close to a kernel driver's because the device's operations do not require a round trip (the program accesses the registers directly).
- VFIO (Virtual Function I/O)
- A framework that lets a user-space program drive a device with the IOMMU's protection (Lesson 11.3). The kernel's VFIO driver performs the device's minimal setup and the IOMMU mapping, and the user-space program drives the device with the IOMMU's isolation ensuring that the program cannot access the memory it has not been granted. The framework is used for the virtualization's device passthrough (a virtual machine's driver runs in the virtual machine's user space) and for the high-performance user-space drivers (the DPDK and SPDK frameworks). Its performance is close to a kernel driver's, and its safety is higher: the IOMMU prevents the program from corrupting the kernel's memory.
- DPDK and SPDK (Data Plane and Storage Performance Development Kits)
- Frameworks that let a user-space program drive a high-performance device (a network adapter, a storage device) with a poll-mode design. The frameworks' drivers run entirely in user space, and they poll the device's queues rather than waiting for interrupts, so the per-packet and per-request overhead is minimized. The frameworks are used for the applications that need the highest throughput and the lowest latency (the network functions, the storage servers), and their performance can exceed the kernel's because they avoid the kernel's syscall and interrupt overheads. Their cost is the dedicated processor cores (the poll-mode drivers consume the cores they run on) and the loss of the kernel's abstractions (the frameworks' applications cannot use the kernel's network stack or file systems without a transition).
The frameworks' existence is a statement about the kernel's design: the kernel's abstractions are general-purpose, and their generality costs performance. The frameworks trade the generality for the performance, and they are the right choice for the applications that need it. The kernel's own drivers remain the right choice for the devices that must be shared among the applications, for the devices that the system's other components depend on (the storage devices that hold the file systems, the network adapters that the kernel's stack uses), and for the devices whose drivers are simple enough that the kernel's rules are not a burden.
4.11 Common misconceptions
"A driver is a program."
A driver is not a program; it is a piece of the kernel. It runs in kernel mode, it shares the kernel's address space, and it has the kernel's privileges. A bug in a driver can crash the system, corrupt data, or hang the machine — the consequences are the kernel's, not a process's.
"A driver is one file."
A driver is a module: it may consist of several source files, and it may use the kernel's shared code (the PCI subsystem, the USB subsystem, the DMA API, the interrupt API). The module's boundaries are the compiled object file's, not the source files'.
"A driver can sleep whenever it wants."
The driver's code can sleep only in the process context. The interrupt context (the interrupt handlers and the softirqs) cannot sleep, because there is no process to wake and no scheduler state to save. The driver's code must be written with its context in mind: the interrupt-context code uses only the non-blocking operations, and the code that needs to sleep is deferred to a workqueue or a kernel thread.
"A driver can access user memory directly."
The kernel and the user space have separate
address spaces, and the kernel cannot access the
user space's memory directly (on most
architectures). The driver must use the kernel's
copy_to_user and
copy_from_user functions, which
validate the user-space address, handle the page
faults, and perform the copy with the necessary
protections. A driver that dereferences a
user-space pointer directly is a driver with a
security bug.
"A driver's probe can fail harmlessly."
A driver's probe can fail (the device is not present, the resources cannot be allocated, the hardware is faulty), and the failure must be handled gracefully: the resources that the probe allocated before the failure must be released, and the probe must return a negative error code. A probe that leaks its resources on failure is a probe that will exhaust the system's resources when the device is probed repeatedly.
"A driver's interrupt handler can do anything."
The interrupt handler (the top half) runs with interrupts disabled (Lesson 11.2), so it must be short and must not sleep. Its work must be limited to the urgent operations (acknowledging the device, reading its status, scheduling the rest); the substantial work belongs in the bottom half, which runs with interrupts enabled and can be scheduled.
"The device model is just bookkeeping."
The device model is the kernel's framework for matching devices to drivers, for exposing the devices' structure to user space, and for managing the devices' power and hot-plug events. Its matching logic is what makes the system self-configuring: the user plugs in a device, the bus enumerates it, the model matches it to a driver, and the driver initializes it. Without the model, the system's support for new hardware would require the administrator's intervention.
"User-space drivers are always slower."
The performance depends on the framework and the workload. A FUSE file system is slower than a kernel file system because every operation requires a kernel-to-user-space round trip, but a UIO or VFIO driver can be close to a kernel driver's performance because the device's operations do not require a round trip, and a DPDK or SPDK poll-mode driver can exceed the kernel's performance because it avoids the kernel's syscall and interrupt overheads. The trade-off is the generality and the safety, not the speed.
05 Worked example
The following example traces a character device's read and a block device's request through their drivers, shows the structure of a minimal character driver, and compares the driver's interface with the kernel's abstractions.
Tracing a character device's read
Consider an application that reads from a serial
port's device node (/dev/ttyS0). The
read passes through the following layers:
-
The application calls
read(fd, buf, 256). The kernel's VFS layer receives the call, looks up the file descriptor'sstruct file, and determines that the file is a character device with the driver'sfile_operationsstructure. -
The VFS calls the driver's
readmethod. The method receives the file's state, the application's buffer, the requested length, and the current offset. - The driver checks the device's state. The driver's data structure records whether the device is open, whether it has data available, and whether it is in an error state. If no data is available, the driver sleeps (in the process context) until the device signals that data has arrived.
-
The driver reads the device's data.
When data is available (the device's interrupt
handler has put it in the driver's buffer), the
driver copies the data from its buffer to the
application's buffer with
copy_to_user. The copy validates the application's address and handles any page faults. - The driver returns the byte count. The method returns the number of bytes transferred (256, or fewer if the device had less data), and the VFS returns the count to the application.
-
The application continues. The
application's buffer contains the serial port's
data, and the
readsystem call has returned.
The trace shows the driver's role: the
application's generic read is
translated into the device's specific operations
(the state check, the buffer's management, the
data's copy), and the device's asynchronous
behaviour (the interrupt that signals the data's
arrival) is hidden behind the synchronous
read interface. The application does
not know that the device is a serial port, that it
generates interrupts, or that the driver sleeps
while waiting for the data; it sees only the bytes.
Tracing a block device's request
Consider a file system that reads a 4 KB block from a SATA SSD. The read passes through the following layers:
-
The file system submits a block request.
The file system's read path calls the block
layer's
submit_biofunction with astruct biothat describes the operation, the block range, and the buffer's scatter-gather list. -
The block layer builds a request.
The block layer's multi-queue machinery places the
bio in a software queue (the queue of the
processor core that submitted it), merges it with
the adjacent bios if possible, and creates a
struct request. -
The block layer maps the request to a
hardware queue. The request is
dispatched to one of the device's hardware
queues, where the driver's
queue_rqmethod consumes it. The I/O scheduler (Lesson 11.5) may reorder the request relative to the other requests. -
The driver translates the request.
The driver's
queue_rqmethod receives thestruct requestand translates it into the device's command format (the AHCI command or the NVMe command). It maps the request's buffer for DMA (Lesson 11.3), builds the command's scatter-gather list, and writes the command to the device's submission queue. - The driver rings the device's doorbell. The driver writes to the device's doorbell register, which tells the device that the command is available.
- The device's controller processes the command. The controller's DMA engine reads the command and the scatter-gather list, transfers the requested block from the flash chips to the buffer's pages, and writes a completion entry.
- The device's interrupt fires. The controller raises an interrupt (a legacy IRQ or an MSI-X message), and the driver's top half runs. The top half reads the completion entry, confirms the command's success, and schedules the bottom half (a softirq, in the block layer's case).
- The driver's bottom half completes the request. The bottom half unmaps the request's buffer, calls the block layer's completion function, and (for a synchronous read) wakes the waiting process.
-
The file system's read returns.
The file system processes the completed bio, and
the application's
readreturns with the data.
The trace shows the block driver's role: the file system's generic block request is translated into the device's command, and the device's asynchronous completion is translated into the block layer's request completion. The two translations are the driver's work, and they are what the file system's and the device's abstractions depend on.
A minimal character driver
The following code is a minimal Linux character
driver. It implements a device that stores a single
string and that the user can read and write through
the device's node. The code is illustrative — it is
not production-quality (it does not handle
concurrency, it does not validate the user's
buffer's size, and it does not implement the
ioctl interface) — but it shows the
driver's structure.
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "minidev"
#define BUF_SIZE 256
static int major_number;
static char device_buffer[BUF_SIZE];
static size_t device_buffer_len;
static int minidev_open(struct inode *inode, struct file *file)
{
pr_info("minidev: open\n");
return 0;
}
static ssize_t minidev_read(struct file *file, char __user *buf,
size_t count, loff_t *offset)
{
size_t available;
if (*offset >= device_buffer_len)
return 0; /* end of data */
available = device_buffer_len - *offset;
if (count > available)
count = available;
if (copy_to_user(buf, device_buffer + *offset, count))
return -EFAULT;
*offset += count;
return count;
}
static ssize_t minidev_write(struct file *file, const char __user *buf,
size_t count, loff_t *offset)
{
if (count > BUF_SIZE)
count = BUF_SIZE;
if (copy_from_user(device_buffer, buf, count))
return -EFAULT;
device_buffer_len = count;
return count;
}
static int minidev_release(struct inode *inode, struct file *file)
{
pr_info("minidev: release\n");
return 0;
}
static const struct file_operations minidev_fops = {
.owner = THIS_MODULE,
.open = minidev_open,
.read = minidev_read,
.write = minidev_write,
.release = minidev_release,
};
static int __init minidev_init(void)
{
major_number = register_chrdev(0, DEVICE_NAME, &minidev_fops);
if (major_number < 0) {
pr_err("minidev: cannot register the device\n");
return major_number;
}
pr_info("minidev: registered with major %d\n", major_number);
return 0;
}
static void __exit minidev_exit(void)
{
unregister_chrdev(major_number, DEVICE_NAME);
pr_info("minidev: unregistered\n");
}
module_init(minidev_init);
module_exit(minidev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("COMP 3150");
MODULE_DESCRIPTION("A minimal character driver");
The code shows the driver's four parts. The
minidev_init and
minidev_exit functions are the
module's initialization and cleanup; they register
and unregister the driver with the kernel's
character-device layer. The
minidev_open, minidev_read,
minidev_write, and
minidev_release functions are the
driver's interface implementation; they are the
methods of the file_operations
structure that the VFS calls. The
minidev_fops structure is the
interface's definition; its
.owner field is the module's
reference, which the kernel uses to prevent the
module's unload while the device is open.
The code's use of copy_to_user and
copy_from_user is the driver's
safety-critical part: the functions validate the
application's addresses, handle the page faults
that the access may cause, and perform the copy
with the necessary protections. A driver that
dereferences the user's pointer directly would be
unsafe (the pointer may be invalid) and a security
bug (the pointer may point to the kernel's memory).
The driver's registration with
register_chrdev is the classical
mechanism; the modern mechanism is the
cdev interface, which allows the
driver to register a range of device numbers and
to have the kernel create the device's nodes
automatically. The modern mechanism is more
flexible (it allows the driver to support several
devices) and it is the recommended one for the new
drivers, but the classical mechanism is simpler and
it is sufficient for a single-device driver.
The driver's interface and the kernel's abstractions
The example's driver implements only four methods
(open, read, write, and release), but the
file_operations structure has many
more (the ioctl,
mmap, poll,
llseek, and fsync
methods). The kernel's VFS layer provides default
implementations for the methods that the driver
does not implement: a read on a
device whose driver does not implement the
read method returns an error
(-EINVAL), an ioctl
returns an error, and an mmap
returns an error. The defaults are what make the
interface's implementation incremental: the
driver implements the methods it needs, and the
kernel provides the rest.
The interface's structure is the kernel's contract with the driver, and it is what makes the drivers' work manageable. The driver does not need to know how the VFS manages the file descriptors, how the page cache caches the data, or how the applications' reads and writes are scheduled; it implements the operations, and the kernel's upper layers do the rest. The same pattern appears in the block and network drivers: the driver implements the class's interface, and the kernel's upper layers provide the buffering, the caching, the scheduling, and the error handling. The interface's design is the kernel's most important contribution to the drivers' ecosystem, and it is the reason a single developer can write a driver that works with the kernel's entire I/O stack.
Inspecting the drivers on a live system
The Linux tools let an administrator inspect the system's drivers, their devices, and their configuration.
# List the loaded modules
lsmod | head -20
# Show a module's information
modinfo e1000e 2>/dev/null | head -20
# The output shows the module's filename, its license,
# its author, its description, and its supported devices
# (the PCI IDs, the USB IDs, or the device tree's
# compatible strings).
# Show the devices' drivers in sysfs
ls /sys/bus/pci/drivers/ | head -20
ls /sys/bus/pci/devices/ | head -10
# Show a device's driver
cat /sys/bus/pci/devices/0000:00:1f.2/driver/../uevent 2>/dev/null | head -5
readlink /sys/bus/pci/devices/0000:00:1f.2/driver
# Show the device classes
ls /sys/class/
ls /sys/class/net/
ls /sys/class/block/
# Show the kernel's driver-related messages
dmesg | grep -iE 'driver|module' | head -20
The commands' outputs show the driver model's
structure: the loaded modules, the modules' metadata
(including the devices they support), the buses'
drivers and devices, the devices' driver
associations, and the device classes. The
/sys/bus/pci/drivers/ directory's
entries are the bus's registered drivers, and each
entry's bind and
unbind files are the administrator's
interface for manually binding and unbinding a
device to a driver.
06 Interactive activity
Activity · explore device drivers on a live system
The goal of this activity is to use the Linux command line to inspect the system's drivers, to examine the device model's structure, and to build and load a minimal kernel module (if your VM has the kernel headers and the build tools). Work through the steps in your course VM.
-
Inventory the loaded modules.
Use
lsmodandmodinfoto list the system's modules and examine their metadata.
How many modules are loaded? Which module has the most dependencies? Which modules are for the storage devices, and which are for the network devices? What does the metadata's# List the loaded modules lsmod | head -30 # Count the modules lsmod | tail -n +2 | wc -l # Show a module's information (replace e1000e with a # module from the previous output) modinfo e1000e 2>/dev/null | head -20 # The output shows the module's filename, its license, # its author, and its supported devices.aliasfield contain? -
Examine the device model's structure.
Use the
/sys/directory to see the buses, devices, drivers, and classes.
How many buses does the system have? How many devices are on the PCI bus? How many drivers are registered on it? How do the buses, devices, drivers, and classes relate to each other in the# Show the buses ls /sys/bus/ # Show the devices on the PCI bus ls /sys/bus/pci/devices/ | head -10 # Show the drivers registered on the PCI bus ls /sys/bus/pci/drivers/ | head -20 # Show the device classes ls /sys/class/ # Show the network interfaces and the block devices ls /sys/class/net/ ls /sys/class/block//sys/hierarchy? -
Examine a device's driver association.
Choose a device and trace its driver association
through
/sys/.
What is the device's driver? What are the device's attributes? How does the device's# Find a device's driver (replace with a device from # the previous output) readlink /sys/bus/pci/devices/0000:00:1f.2/driver # Show the device's attributes ls /sys/bus/pci/devices/0000:00:1f.2/ # Show the device's uevent cat /sys/bus/pci/devices/0000:00:1f.2/uevent 2>/dev/null # Show the driver's supported devices ls /sys/bus/pci/drivers/ahci/ 2>/dev/nulldriverlink relate to the driver's directory? How does the driver's ID table relate to the device's IDs? -
Examine the character and block device
nodes.
Use
ls -l /devand the sysfs interfaces to see the device nodes and their types.
Which of the devices are character devices, and which are block devices? What are their major and minor numbers? How does the sysfs's# List the device nodes ls -l /dev/ | head -30 # Show the device nodes' types ls -l /dev/null /dev/zero /dev/random /dev/tty /dev/sda 2>/dev/null # The first character of the mode shows the type: # 'c' for a character device, 'b' for a block device. # Show the device's major and minor numbers ls -l /dev/null ls -l /dev/sda 2>/dev/null # Show the sysfs representation cat /sys/dev/char/1:3/uevent 2>/dev/null cat /sys/dev/block/8:0/uevent 2>/dev/null/sys/dev/hierarchy relate to the device nodes in/dev? -
Build and load a minimal kernel
module.
If your VM has the kernel headers and the build
tools, build and load the minimal character
driver from the lesson's worked example.
Did the module build and load? What major number did the kernel assign? Did the write and read work? What messages did the kernel's log show? What does the experiment tell you about the driver's structure?# Check whether the kernel headers are available ls /lib/modules/$(uname -r)/build 2>/dev/null # Create the driver's source and its Makefile mkdir -p ~/minidev && cd ~/minidev cat > minidev.c << 'EOF' #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #define DEVICE_NAME "minidev" #define BUF_SIZE 256 static int major_number; static char device_buffer[BUF_SIZE]; static size_t device_buffer_len; static int minidev_open(struct inode *i, struct file *f) { return 0; } static ssize_t minidev_read(struct file *f, char __user *buf, size_t count, loff_t *off) { size_t available; if (*off >= device_buffer_len) return 0; available = device_buffer_len - *off; if (count > available) count = available; if (copy_to_user(buf, device_buffer + *off, count)) return -EFAULT; *off += count; return count; } static ssize_t minidev_write(struct file *f, const char __user *buf, size_t count, loff_t *off) { if (count > BUF_SIZE) count = BUF_SIZE; if (copy_from_user(device_buffer, buf, count)) return -EFAULT; device_buffer_len = count; return count; } static int minidev_release(struct inode *i, struct file *f) { return 0; } static const struct file_operations minidev_fops = { .owner = THIS_MODULE, .open = minidev_open, .read = minidev_read, .write = minidev_write, .release = minidev_release, }; static int __init minidev_init(void) { major_number = register_chrdev(0, DEVICE_NAME, &minidev_fops); if (major_number < 0) return major_number; pr_info("minidev: major %d\n", major_number); return 0; } static void __exit minidev_exit(void) { unregister_chrdev(major_number, DEVICE_NAME); } module_init(minidev_init); module_exit(minidev_exit); MODULE_LICENSE("GPL"); EOF cat > Makefile << 'EOF' obj-m += minidev.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean EOF # Build the module make # Load the module (requires root) sudo insmod minidev.ko # Check the kernel's log for the module's message dmesg | tail -5 # Create the device node (the major number is in the log) sudo mknod /dev/minidev c <major> 0 sudo chmod 666 /dev/minidev # Write to and read from the device echo "hello, driver" > /dev/minidev cat /dev/minidev # Unload the module sudo rmmod minidev dmesg | tail -3 -
Compare the driver's interfaces.
Use the kernel's documentation and the man pages
to compare the character, block, and network
drivers' interfaces.
What methods does each interface define? Which methods are common to the three? Which methods are specific to each? What does the comparison tell you about the classes' requirements?# Show the kernel's driver-related documentation ls /usr/src/linux-headers-$(uname -r)/Documentation/ 2>/dev/null | head # Show the character device's file_operations structure grep -A30 'struct file_operations' /usr/src/linux-headers-$(uname -r)/include/linux/fs.h 2>/dev/null | head -35 # Show the block device's blk_mq_ops structure grep -A20 'struct blk_mq_ops' /usr/src/linux-headers-$(uname -r)/include/linux/blk-mq.h 2>/dev/null | head -25 # Show the network device's net_device_ops structure grep -A20 'struct net_device_ops' /usr/src/linux-headers-$(uname -r)/include/linux/netdevice.h 2>/dev/null | head -25 -
Examine a user-space driver framework.
If the system has FUSE, examine its configuration
and the file systems that use it.
Is FUSE available? Which file systems use it? What does the FUSE file systems' performance suggest about the user-space drivers' trade-offs?# Check whether FUSE is available ls /dev/fuse 2>/dev/null lsmod | grep -i fuse # Show the FUSE file systems that are mounted mount | grep -i fuse # Check whether UIO or VFIO are available ls /dev/uio* 2>/dev/null ls /dev/vfio/ 2>/dev/null lsmod | grep -iE 'uio|vfio' -
Read the man pages.
Read the man pages that document the driver
interfaces.
For each, write a sentence describing its main contribution to your understanding of device drivers. Theman 2 ioctl man 4 mem man 4 null man 5 sysfs man 8 insmod man 8 rmmod man 8 modprobe man 8 lsmod man 8 modinfoman 2 ioctlpage documents the device-specific interface; theman 5 sysfspage documents the device model's user-space representation; theman 8 lsmodandman 8 modinfopages document the module tools.
Reflect: device drivers are the kernel's largest and least glamorous component, and they are the reason the kernel supports so much hardware. A driver is a piece of translation: it turns the kernel's generic requests into the device's specific operations, and the device's asynchronous completions into the kernel's generic signals. The translation is invisible when it works and catastrophic when it does not: a buggy driver can crash the system, corrupt data, or hang the machine. Which of the driver's responsibilities from this lesson do you find most challenging? What would you change about the driver's interface if you could? How would you explain the difference between a character, a block, and a network driver to someone who has never written one? What is the right balance between the kernel's abstractions and the device's capabilities?
07 Knowledge check
Check your understanding
Question 1
Define the device driver and explain its role in the kernel. What are the two interfaces that it translates between, and why is the translation necessary?
Show answer
open,
read, write,
ioctl, close) that
the VFS layer calls; for a network device, the
network stack's packet interface. The interface
is the same for all devices of the same class,
so the kernel's upper layers do not need to
know which device they are using. The interface
below the driver is the device's hardware: a
set of registers, a command format, a protocol
for reporting completion, and a set of
capabilities. The interface is different for
every device, so the driver must know the
device's details. The translation is necessary
because the two interfaces are designed for
different purposes. The kernel's abstraction is
designed for generality: it must work for every
device of the class, so it cannot expose the
device's specific details. The device's
interface is designed for the device's
operation: it exposes the registers and the
commands that the device's controller requires,
and it does not care about the kernel's
abstractions. The driver bridges the two: it
receives a generic request ("read 4 KB from
offset 1 MB") and turns it into the device's
specific sequence of register writes, DMA
descriptors, and interrupt handling. The
translation is not merely mechanical: the
driver must handle the device's quirks, its
error conditions, its timing constraints, and
its concurrency, all while obeying the kernel's
rules about locking, sleeping, and memory
allocation.
Question 2
Describe the driver's structure. What are its four parts, and how do they interact over the driver's lifecycle?
Show answer
module_init function runs when
the module is loaded (or when the kernel
boots, if the driver is built in); it registers
the driver with the kernel's device model,
allocates the driver's global data structures,
and performs any global setup. The
module_exit function runs when
the module is unloaded; it reverses the
initialization. Probe and remove:
the probe function runs when the
kernel matches the driver to a device; it is
the driver's per-device initialization: it
maps the device's registers, allocates the
per-device data structures, requests the
device's interrupts and DMA channels, registers
the device with the appropriate subsystem, and
initializes the device's hardware. The
remove function reverses the
probe's work. The interface
implementation: the driver implements
the interface that its device class requires —
the file_operations methods for a
character driver, the blk_mq_ops
methods for a block driver, the
net_device_ops methods for a
network driver. The interface is what the
kernel's upper layers call, and its methods are
the driver's actual work.
The interrupt and deferred-work
handlers: if the device generates
interrupts, the driver implements the interrupt
handler (the top half) and the deferred-work
handler (the bottom half, as a softirq, a
tasklet, or a workqueue). The four parts
interact over the driver's lifecycle. The
module's initialization registers the driver;
the kernel's device model matches the driver to
a device and calls the probe; the probe sets up
the device and registers the driver's
interface; the kernel's upper layers call the
interface's methods; the device's interrupts
and DMA completions drive the driver's
handlers; the device's removal (or the module's
unload) calls the remove; the module's cleanup
unregisters the driver. The lifecycle is the
same for every driver, regardless of the
device's class, and its structure is the
kernel's contribution to making the drivers'
work manageable.
Question 3
Compare the character, block, and network device drivers' interfaces. What is each class's unit of transfer, what interface does each driver implement, and when is each used?
Show answer
file_operations structure's
methods: open (initialize the
device), read (copy data to user
space), write (copy data from
user space), ioctl (device-
specific commands), release
(release the device), and mmap
(map the device's memory). The interface is
used for the devices whose data is a stream:
keyboards, mice, serial ports, terminals, and
the pseudo-devices /dev/null,
/dev/zero, and
/dev/random. A block
device transfers data in fixed-size
blocks and supports random access. Its driver
implements the block layer's
blk_mq_ops structure's methods:
queue_rq (submit a request),
complete (handle a completion),
init_request (per-request setup),
map_queues (queue mapping), and
timeout (handle timeouts). The
interface is used for the storage devices:
hard disks, solid-state drives, optical drives,
and the logical volumes and RAID arrays built
on them. The block device is the file system's
substrate: a file system is mounted on a block
device. A network device
transfers data as packets and is addressed by
network address rather than by block or byte
offset. Its driver implements the
net_device_ops structure's
methods: ndo_start_xmit (transmit
a packet), ndo_open (bring the
interface up), ndo_stop (bring it
down), ndo_set_rx_mode (multicast
setup), ndo_get_stats
(statistics), and ndo_tx_timeout
(transmit timeout). The interface is used for
the network adapters: Ethernet controllers,
Wi-Fi adapters, and the virtual interfaces
(loopback, bridges, tunnels). The three
classes' interfaces are the kernel's contract
with the drivers, and the class determines
which interface the driver must implement. The
character device's interface is the simplest
(the file operations), the block device's is
the most complex (the multi-queue block layer's
request queue), and the network device's is the
most asynchronous (the receive and transmit
rings).
Question 4
Describe the Linux device model. What are its abstractions, how do they interact, and what role do the device tree and ACPI play?
Show answer
block class, the
net class, the tty
class, the input class). The
abstractions interact through the model's
matching logic. When a device is registered,
the bus's match function compares the
device's ID with each of the bus's drivers' ID
tables. When a match is found, the bus calls
the driver's probe function, which
initializes the device. When a driver is
registered, the same logic runs in reverse. The
model's user-space representation is
sysfs, whose directory structure
mirrors the model's: /sys/bus/
contains the bus types, /sys/devices/
contains the devices' hierarchy,
/sys/class/ contains the classes,
and /sys/module/ contains the
loaded modules. The device tree
and ACPI are the firmware
descriptions that the model needs on the
platforms whose buses are not discoverable. The
device tree is a static data structure (a tree
of nodes and properties) that describes the
system's hardware; it is used on the ARM,
PowerPC, and RISC-V systems, and each node's
compatible property is the
counterpart of the PCI ID. ACPI is the x86
systems' firmware interface; its tables
describe the system's devices, their resources,
and their power management, and its
_HID and _CID
properties are the counterparts of the device
tree's compatible strings. The
kernel abstracts the difference with the
fwnode interface, so a driver can be
written once and used on both platforms. The
model's matching logic is what makes the system
self-configuring: the user plugs in a device,
the bus enumerates it, the model matches it to
a driver, and the driver initializes it.
Question 5
Explain the driver's practical concerns: the locking rules, the sleeping rules, and the error handling. Why are they stricter than a user-space program's, and what are the consequences of getting them wrong?
Show answer
GFP_KERNEL); the interrupt context
cannot (there is no process to wake, and the
scheduler's state cannot be saved). The
driver's code must be written with its context
in mind: the interrupt-context code uses only
the non-blocking operations (the
GFP_ATOMIC memory allocation, the
spin_lock primitives, the
udelay function), and the code
that needs to sleep is deferred to the process
context (a workqueue, a kernel thread). The
error handling arises from the
devices' fallibility: a transfer can time out,
a device can report an error, a device can
disappear. The driver's error handling must be
robust (the device's failure must not crash the
kernel), complete (the resources that the
driver allocated for the failed operation must
be released), and informative (the error must
be reported to the administrator through the
kernel's log, with enough detail to diagnose
the problem). The three concerns are stricter
than a user-space program's because the
consequences of getting them wrong are the
system's, not the program's: a race condition
can corrupt the kernel's data, a sleep in the
interrupt context can deadlock the system, and
an unreleased resource can exhaust the system's
memory. The kernel's debugging tools (the
CONFIG_DEBUG_SPINLOCK,
CONFIG_DEBUG_MUTEXES, and
CONFIG_PROVE_LOCKING options, and
the lockdep validator) are
designed to catch the bugs before they reach
production, and the driver's developer should
enable them.
08 Unit discussion
Discussion prompt
Device drivers are the kernel's largest component,
and their history is a history of standardization.
The first drivers were part of the kernel's core:
each device's code was compiled into the kernel,
and adding a device meant rebuilding the kernel.
The loadable module mechanism (introduced in the
late 1980s and refined through the 1990s) separated
the drivers from the kernel, so a device could be
supported without a rebuild. The device model
(introduced in Linux 2.6) separated the drivers'
matching from their operation, so
the kernel could match devices to drivers
automatically and expose the devices' structure to
user space. The class interfaces (the
file_operations, the
blk_mq_ops, the
net_device_ops) separated the drivers'
interface from their implementation,
so a driver could be written once for a class of
devices. Each generation reduced the amount of
code that a driver's developer had to write, and
each made the system's support for new hardware
more automatic.
The trend is toward abstraction and specialization. The abstraction is the kernel's: the interfaces are more general, the device model is more automatic, and the drivers' common code (the PCI probing, the DMA mapping, the interrupt registration) is more shared. The specialization is the drivers': as the kernel's common code grows, the driver's code shrinks to the device-specific details. An NVMe driver, for example, is mostly a description of the NVMe protocol's commands and queues, with the kernel's block layer, DMA API, and interrupt API handling the rest. The trend is a sign of the kernel's maturity: the common problems have been solved once, and the drivers' developers can focus on the devices' specifics.
The trend also raises a question about the drivers' future. If the kernel's common code handles more of the work, and the devices' controllers are becoming more capable, do the drivers need to be in the kernel at all? The user-space frameworks (FUSE, UIO, VFIO, DPDK, SPDK) suggest an alternative: the drivers' code runs in user space, the kernel's role is limited to the setup and the protection, and the devices are driven by the applications that use them. The alternative's advantages are the safety (a bug crashes the application, not the kernel) and the performance (the poll-mode drivers avoid the kernel's syscall and interrupt overheads). Its disadvantages are the loss of the kernel's abstractions (the applications cannot use the kernel's network stack or file systems without a transition) and the loss of the devices' sharing (a device driven by one application cannot be used by another).
Should the drivers remain in the kernel, or should more of them move to user space? What is the right division of labour between the kernel's abstractions and the applications' control? How should the kernel's design accommodate the devices whose controllers are becoming programmable computers in their own right? Post your response to the Unit 11 discussion forum and reply to at least two classmates. Supports CLO1, CLO6, and CLO8.
09 Practice exercise
This ungraded exercise consolidates the lesson and prepares you for Lesson 11.5. Aim to spend 60–90 minutes on it.
-
Trace an I/O request through a driver.
A program reads 8 KB from a file on a SATA SSD.
Trace the read through the driver, from the file
system's block request to the driver's completion
handler. For each step, describe:
- What the block layer does.
- What the driver does.
- What the device's controller does.
- What the interrupt handler does.
- Compare the driver interfaces. Write a short paragraph (200–250 words) comparing the character, block, and network drivers' interfaces. Your paragraph should address: (a) each class's unit of transfer; (b) each class's interface structure; (c) each class's synchronous or asynchronous behaviour; (d) the workloads for which each class is used; (e) the common patterns that the three classes share.
-
Examine the module's lifecycle.
Use the module tools to load a module, examine
its state, and unload it.
What is the module's reference count? What parameters does it have? Why can the module be unloaded only when its reference count is zero?# List the currently loaded modules lsmod | head -5 # Load a module (choose one that is not already loaded # and that your VM has; for example, the 'vfat' module) sudo modprobe vfat 2>/dev/null # Examine the module's state lsmod | grep vfat ls /sys/module/vfat/ cat /sys/module/vfat/refcnt 2>/dev/null # Show the module's parameters ls /sys/module/vfat/parameters/ 2>/dev/null # Unload the module sudo rmmod vfat 2>/dev/null lsmod | grep vfat -
Examine the device model's matching.
Choose a device and trace its driver association
through the device model's matching logic.
What is the device's driver? What are the device's vendor and device IDs? How does the driver's ID table relate to the device's IDs? What does the matching tell you about the device model's design?# Find a device's driver readlink /sys/bus/pci/devices/0000:00:1f.2/driver 2>/dev/null # Show the driver's supported devices ls /sys/bus/pci/drivers/ahci/ 2>/dev/null # Show the device's IDs cat /sys/bus/pci/devices/0000:00:1f.2/vendor 2>/dev/null cat /sys/bus/pci/devices/0000:00:1f.2/device 2>/dev/null # Show the driver's module cat /sys/bus/pci/drivers/ahci/module/uevent 2>/dev/null -
Write a minimal driver.
If your VM has the kernel headers and the build
tools, write and build a minimal character
driver (the lesson's worked example is a
starting point).
Did the driver build and load? What major number did the kernel assign? Did the read and write work? What messages did the kernel's log show? What would you add to the driver to make it more robust?# Create the driver's source and build it # (the lesson's worked example has the full code) mkdir -p ~/mydriver && cd ~/mydriver # ... create mydriver.c and Makefile ... make # Load the driver sudo insmod mydriver.ko dmesg | tail -3 # Create the device node # (use the major number from the kernel's message) sudo mknod /dev/mydriver c <major> 0 sudo chmod 666 /dev/mydriver # Test the driver echo "test" > /dev/mydriver cat /dev/mydriver # Unload the driver sudo rmmod mydriver -
Examine a driver's locking.
If you have access to a driver's source (the
kernel's source tree or a vendor's driver), find
the driver's locking primitives and analyze its
locking rules.
What locking primitives does the driver use? Which data structures do the locks protect? Are the locks held for short or long periods? Does the driver's interrupt handler use the same locks as its process-context code? What would you check to verify the driver's locking is correct?# Find a driver's source (if the kernel's source is # available) find /usr/src -name '*.c' 2>/dev/null | head -10 # Search for the locking primitives grep -n 'spin_lock\|mutex_lock' /usr/src/linux-headers-$(uname -r)/drivers/... 2>/dev/null | head -20 -
Compare the kernel and user-space
drivers.
If the system has FUSE, compare a FUSE file
system's performance with a kernel file system's.
How does the FUSE file system's performance compare with the kernel file system's? What does the difference tell you about the user-space drivers' trade-offs?# Check whether FUSE is available lsmod | grep -i fuse # If FUSE is available, mount a FUSE file system # (the sshfs and curlftpfs file systems are common) which sshfs curlftpfs 2>/dev/null # Measure the difference between the FUSE file system # and a kernel file system with the same workload # ... -
Design a driver's structure.
A device is a custom data-acquisition board that
produces a stream of 16-bit samples at 1 MHz. The
board has a 4 KB FIFO buffer, a set of control
registers, and a DMA engine. The board raises an
interrupt when the FIFO's half-full threshold is
reached. Design the driver's structure. Consider:
- Would you implement the driver as a character device, a block device, or a network device? Why?
- What would the driver's
probefunction do? - What would the driver's interrupt handler (top half) do, and what would the bottom half do?
- How would the driver use the DMA engine?
- What locking would the driver need?
- How would the driver expose the data to the applications (the read interface, the mmap interface, the poll interface)?
- Reflect. In your own words, explain why the device driver is necessary, and describe the driver's structure and the three device-class interfaces. Then describe one situation in your own experience — a device you have used or a system you have administered — where the driver's design affected the system's behaviour. What was the device, and how would a different driver design have changed the outcome?
10 Summary
- A device driver is a piece of kernel code that knows how to operate a specific device and that presents the device to the rest of the kernel through one of the kernel's standardized interfaces. The driver is the kernel's translation layer: it receives the kernel's generic requests and turns them into the device's specific operations.
- A loadable kernel module is a piece of kernel code that can be loaded and unloaded at runtime. The module mechanism gives the kernel its flexibility (a device can be supported without a rebuild), its memory efficiency (only the needed modules are loaded), and its development speed (a driver can be tested as a module).
- A driver has four parts: its initialization and cleanup (the module's init and exit functions), its probe and remove (the per-device setup and teardown), its interface implementation (the class's methods), and its interrupt and deferred-work handlers (the top half and the bottom half).
-
A character device transfers data
as a stream of bytes; its driver implements the
file_operationsstructure (theopen,read,write,ioctl,release, andmmapmethods). A block device transfers data in fixed-size blocks; its driver implements theblk_mq_opsstructure and the multi-queue block layer's request queue. A network device transfers data as packets; its driver implements thenet_device_opsstructure and the network stack's packet interface. -
The Linux device model represents
the system's devices and their relationships. Its
abstractions are the bus (the type
of connection), the device (the
component), the driver (the code
that operates it), and the class
(the interface that the device provides to user
space). Its matching logic connects the devices to
the drivers automatically, and its
sysfsfilesystem exposes the model's structure to user space. - The device tree and ACPI are the firmware descriptions that the model needs on the platforms whose buses are not discoverable. The device tree is the ARM, PowerPC, and RISC-V systems' static description; ACPI is the x86 systems' dynamic interface. The kernel abstracts the difference with the fwnode interface.
- A driver's code must obey three rules. The locking rules protect the shared data structures from the concurrent access of the several processor cores and the interrupt handlers. The sleeping rules distinguish the process context (which can sleep) from the interrupt context (which cannot). The error-handling rules require the driver to handle the devices' failures robustly, completely, and informatively.
- The user-space driver frameworks (FUSE, UIO, VFIO, DPDK, SPDK) move the driver's code out of the kernel. The frameworks trade the kernel's abstractions for the safety (a bug crashes the application, not the kernel) and, in the poll-mode cases, the performance (the applications avoid the kernel's syscall and interrupt overheads).
Looking ahead
This lesson presented device drivers: the translation layer, the kernel module mechanism, the driver's structure, the three device-class interfaces, the Linux device model, the device tree and ACPI, the practical concerns of driver development, and the user-space alternatives. It referred several times to the block layer's request queue and to the I/O scheduler that reorders the requests before the driver consumes them. Lesson 11.5 presents disk scheduling in detail: the disk's performance characteristics, the classical algorithms (FCFS, SSTF, SCAN, C-SCAN, LOOK, C-LOOK), the modern schedulers (the multi-queue block layer's mq-deadline, BFQ, and Kyber), the solid-state drives' different characteristics and the schedulers' adaptation to them, and the scheduling's effect on the system's performance. The driver is the block layer's consumer, and the scheduler is the block layer's brain: the two together determine the storage stack's performance, and the next lesson examines the scheduler's design.
11 References
- Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 12, Sections 12.5 and 12.6.
-
Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C.
Operating Systems: Three Easy Pieces, Chapter 36,
I/O Devices. Available online at
ostep.org. - Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 5, Sections 5.2–5.4.
- Corbet, J., Rubini, A., & Kroah-Hartman, G. Linux Device Drivers, 3rd ed. O'Reilly, 2005. The definitive guide to Linux driver development.
- Love, R. Linux Kernel Development, 3rd ed., Chapters 14 and 16.
- Bovet, D. P., & Cesati, M. Understanding the Linux Kernel, 3rd ed. O'Reilly, 2005. Chapter 13, I/O Architecture and Device Drivers.
- Kerrisk, M. The Linux Programming Interface. No Starch Press, 2010. Chapters 14 and 15.
-
Linux man-pages:
man 2 ioctl,man 4 mem,man 4 null,man 4 zero,man 4 random,man 4 tty,man 4 sd,man 5 sysfs,man 8 insmod,man 8 rmmod,man 8 modprobe,man 8 lsmod,man 8 modinfo,man 1 udevadm,man 8 lspci.