COMP 3150Operating Systems

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 ioctl interface.
  • 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, and close calls 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:

  1. Reads the module's metadata. The module's .modinfo section 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.
  2. 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.
  3. 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.
  4. Calls the module's initialization function. The module's module_init function 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_init function 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. The module_exit function 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 the module_init and module_exit macros.
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 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. The remove function runs when the device is removed (or when the module is unloaded); it reverses the probe's work and releases the device's resources. The probe and remove functions 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_operations structure's methods (open, read, write, ioctl, release, and so on). A block driver implements the block layer's interface (the queue_rq method of the blk_mq_ops structure). A network driver implements the net_device_ops structure'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.

The driver's position and the three device-class interfaces The diagram has two parts. The top part shows the driver's position between the kernel's upper layers (the system call interface, the VFS, the block layer, and the network stack) and the device's controller (the registers, the interrupt line, and the DMA engine). The bottom part compares the three device-class interfaces: the character device's file_operations, the block device's blk_mq_ops and request queue, and the network device's net_device_ops and net_device. THE DRIVER'S POSITION IN THE KERNEL Kernel upper layers (device-independent) System-call interface read, write, ioctl Virtual file system VFS, page cache Block layer request queue, scheduler Network stack sockets, packets generic requests Device driver (the translation layer) probe / remove / init / exit the driver's lifecycle the class interface file_operations, blk_mq_ops, net_device_ops interrupt / DMA handling top half, bottom half, descriptors device-specific operations Device controller registers (MMIO) interrupt line / MSI-X DMA engine device THE THREE DEVICE-CLASS INTERFACES Character device a stream of bytes, no random access struct file_operations .open → initialize the device .read → copy data to user space .write → copy data from user space .ioctl → device-specific commands .release → release the device .mmap → map device memory Examples: /dev/tty, /dev/null, /dev/random, serial ports Block device fixed-size blocks, random access, cached struct blk_mq_ops .queue_rq → submit a request .complete → handle a completion .init_request → per-request setup .map_queues → queue mapping .timeout → handle timeouts .poll → poll for completions Examples: /dev/sda, /dev/nvme0n1, /dev/mapper/*, the file systems' substrate Network device packets, addressed by network address struct net_device_ops .ndo_start_xmit → transmit a packet .ndo_open → bring the interface up .ndo_stop → bring the interface down .ndo_set_rx_mode → multicast setup .ndo_get_stats → statistics .ndo_tx_timeout → transmit timeout Examples: eth0, wlan0, lo, the socket API's substrate
Figure 11.4 — The driver's position and the three device-class interfaces. The top diagram shows the driver between the kernel's upper layers (the system-call interface, the VFS, the block layer, and the network stack) and the device's controller. The driver receives the kernel's generic requests and translates them into the device's specific operations. The bottom diagram compares the three device-class interfaces: the character device's 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_user function (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_user function. 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 _IOWR macros), and the method's implementation interprets the command. The ioctl interface 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 mmap system call). The method sets up the mapping, typically by using the kernel's remap_pfn_range function 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_rq method 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_xmit method) and delivers a received packet to the kernel (by calling the network stack's netif_rx or napi_gro_receive functions). 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 not open a 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_xmit method 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 block class (the block devices), the net class (the network interfaces), the tty class (the terminals and serial ports), the input class (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 the udev rules 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:

  1. The application calls read(fd, buf, 256). The kernel's VFS layer receives the call, looks up the file descriptor's struct file, and determines that the file is a character device with the driver's file_operations structure.
  2. The VFS calls the driver's read method. The method receives the file's state, the application's buffer, the requested length, and the current offset.
  3. 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.
  4. 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.
  5. 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.
  6. The application continues. The application's buffer contains the serial port's data, and the read system 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:

  1. The file system submits a block request. The file system's read path calls the block layer's submit_bio function with a struct bio that describes the operation, the block range, and the buffer's scatter-gather list.
  2. 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.
  3. 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_rq method consumes it. The I/O scheduler (Lesson 11.5) may reorder the request relative to the other requests.
  4. The driver translates the request. The driver's queue_rq method receives the struct request and 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.
  5. 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.
  6. 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.
  7. 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).
  8. 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.
  9. The file system's read returns. The file system processes the completed bio, and the application's read returns 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.

  1. Inventory the loaded modules. Use lsmod and modinfo to list the system's modules and examine their metadata.
    # 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.
    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 alias field contain?
  2. Examine the device model's structure. Use the /sys/ directory to see the buses, devices, drivers, and classes.
    # 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/
    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 /sys/ hierarchy?
  3. Examine a device's driver association. Choose a device and trace its driver association through /sys/.
    # 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/null
    What is the device's driver? What are the device's attributes? How does the device's driver link relate to the driver's directory? How does the driver's ID table relate to the device's IDs?
  4. Examine the character and block device nodes. Use ls -l /dev and the sysfs interfaces to see the device nodes and their types.
    # 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
    Which of the devices are character devices, and which are block devices? What are their major and minor numbers? How does the sysfs's /sys/dev/ hierarchy relate to the device nodes in /dev?
  5. 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.
    # 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
    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?
  6. Compare the driver's interfaces. Use the kernel's documentation and the man pages to compare the character, block, and network drivers' interfaces.
    # 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
    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?
  7. Examine a user-space driver framework. If the system has FUSE, examine its configuration and the file systems that use it.
    # 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'
    Is FUSE available? Which file systems use it? What does the FUSE file systems' performance suggest about the user-space drivers' trade-offs?
  8. Read the man pages. Read the man pages that document the driver interfaces.
    man 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 modinfo
    For each, write a sentence describing its main contribution to your understanding of device drivers. The man 2 ioctl page documents the device-specific interface; the man 5 sysfs page documents the device model's user-space representation; the man 8 lsmod and man 8 modinfo pages 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
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 two interfaces it translates between are the interface above the driver and the interface below it. The interface above the driver is the kernel's abstraction: for a storage device, the block layer's request queue; for a character device, the file operations (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
A Linux driver has four parts. Initialization and cleanup: the 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
The three device classes differ in their unit of transfer and their interface. A character device transfers data as a stream of bytes and does not support random access. Its driver implements the 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
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 hot-plug events. Its abstractions are the bus, the device, the driver, and the class. 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). Each bus type defines how its devices are identified, how they are enumerated, and how they are matched to drivers. A device is a physical or virtual component attached to a bus; its representation records its parent, its bus, its driver, its power state, and its properties. A driver is a piece of code that can operate a device; its representation records the driver's name, its bus, its probe and remove functions, and its table of supported devices. A class is a type of device that provides a particular interface to user space (the 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
A driver's code must obey the kernel's rules, and the rules are stricter than a user-space program's because the driver runs in kernel mode, shares the kernel's address space, and can crash the entire system. The locking rules arise from the driver's concurrency: its code can run on several processor cores at once, and its interrupt handlers can run concurrently with its process-context code. The shared data structures must therefore be protected. The kernel provides spinlocks (for the short critical sections that the interrupt handlers share, held for a few instructions to a few dozen, and never sleeping), mutexes (for the longer critical sections that only the process-context code accesses, which may sleep and must not be held in an interrupt handler), atomic operations (for the single variables), and per-CPU data (for the data that each core accesses independently). A driver that accesses a shared structure without a lock has a race condition; a driver that holds a spinlock for too long has high latency; a driver that sleeps while holding a spinlock can deadlock the system. The sleeping rules arise from the driver's 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). 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 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.

  1. 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.
    Assume the request is a single 8 KB read and the device uses a descriptor ring.
  2. 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.
  3. Examine the module's lifecycle. Use the module tools to load a module, examine its state, and unload it.
    # 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
    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?
  4. Examine the device model's matching. Choose a device and trace its driver association through the device model's matching logic.
    # 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
    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?
  5. 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).
    # 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
    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?
  6. 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.
    # 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
    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?
  7. 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.
    # 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
    # ...
    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?
  8. 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 probe function 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)?
    Justify your design.
  9. 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_operations structure (the open, read, write, ioctl, release, and mmap methods). A block device transfers data in fixed-size blocks; its driver implements the blk_mq_ops structure and the multi-queue block layer's request queue. A network device transfers data as packets; its driver implements the net_device_ops structure 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 sysfs filesystem 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.
👤