Skip to content
Home
Device Drivers: How OS Kernels Interface with Hardware

Device Drivers: How OS Kernels Interface with Hardware

Operating Systems Operating Systems 8 min read 1501 words Beginner ExcellentWiki Editorial Team

Device drivers are the kernel modules that mediate between the operating system and hardware peripherals. Every piece of hardware connected to a computer — from the storage controller to the network interface to the USB keyboard — requires a driver that understands the device’s protocol and translates it into a uniform interface the OS can consume. The Linux kernel alone contains over 15 million lines of driver code, approximately 70 percent of the total kernel codebase. Understanding driver architecture is essential for systems programmers, embedded developers, and anyone working with custom hardware.

The Linux Device Driver Model

The Linux kernel organizes devices, drivers, and buses into a standardized model grounded in sysfs, the in-memory filesystem that exposes kernel objects to user space. Every device in the system is represented by a struct device, every driver by a struct device_driver, and every bus (PCI, USB, SPI, I2C, platform) by a struct bus_type. The driver core, defined in drivers/base/, provides the infrastructure for device discovery, driver binding, power management, and hot-plug events.

When hardware is detected — either during boot or at hot-plug time — the bus driver creates a device object and reads identifying information from the hardware: vendor ID, device ID, subsystem IDs, and revision number. The kernel’s driver matching logic searches through registered drivers for one whose ID table matches the device. When a match is found, the driver’s probe function is called, and the driver initializes the device.

Character, Block, and Network Drivers

Drivers fall into three fundamental categories based on the data transfer model they support.

Character Drivers

Character drivers transfer data as a continuous stream of bytes, one character at a time. They implement the file_operations structure — the kernel’s interface for open, read, write, release, and unlocked_ioctl operations — and expose devices as nodes under /dev with type c (character). The keyboard, serial ports, mice, sound cards, and many IoT sensor interfaces are character devices.

The character driver interface is simple: the kernel’s VFS layer translates user-space system calls into calls to the driver’s registered file_operations callbacks. Each callback runs in process context and can sleep if necessary, making mutexes and wait queues appropriate synchronization primitives.

static struct file_operations fops = {
    .owner          = THIS_MODULE,
    .open           = my_open,
    .release        = my_release,
    .read           = my_read,
    .write          = my_write,
    .unlocked_ioctl = my_ioctl,
---;

Block Drivers

Block drivers transfer data in fixed-size blocks, typically 512 bytes or 4 KB. They interface with the kernel’s block I/O layer through a request queue, where I/O operations can be merged, reordered, and cached. The block layer handles scheduling (elevators like CFQ, Deadline, or mq-deadline), ensuring that the physical characteristics of the storage device — seek times on hard disks, erase blocks on SSDs — are respected.

Block devices include hard drives, SSDs, USB mass storage, and loop devices. NVMe drivers, for example, are block drivers that submit I/O commands through submission queues and completion queues directly to the NVMe controller over PCIe, bypassing the older SCSI/SATA translation layer.

Network Drivers

Network devices do not conform to the file abstraction. Instead, they use a packet-based model through the struct net_device interface. The kernel’s network stack (the inet and inet6 families) processes packets through protocol layers. The driver implements net_device_ops: open, stop, start_xmit, get_stats, and (for modern drivers) the NAPI poll function for interrupt mitigation.

NAPI (New API) is a kernel framework for high-performance packet processing. It combines interrupts with polling: when a network interrupt fires, the driver disables further interrupts and switches to polling mode, processing packets in batches. This reduces interrupt overhead under high packet rates, which is critical for achieving 10 Gbps and higher line rates.

Device Discovery and Driver Binding

Modern hardware buses support plug-and-play enumeration. PCI devices, for example, expose configuration space registers — mandatory at offsets 0x00–0x3F — that include vendor ID, device ID, class code, and subsystem identifiers. The Linux kernel reads these registers during PCI bus enumeration and creates struct pci_dev entries in the device model.

The driver declares supported devices through a device ID table, annotated with MODULE_DEVICE_TABLE. For PCI drivers:

static const struct pci_device_id my_ids[] = {
    { PCI_DEVICE(0x10EC, 0x8168) },  /* Realtek RTL8168 */
    { }
---;
MODULE_DEVICE_TABLE(pci, my_ids);

This table is compiled into the module and also extracted by depmod to build the module alias database. When udevd detects new hardware, it queries the database and loads the matching module via modprobe.

DMA and Data Transfer Methods

Direct Memory Access (DMA) is the mechanism by which hardware devices transfer data directly to or from system memory without CPU involvement. The DMA engine — either a dedicated DMA controller or an intelligent device (most modern hardware) — is programmed with source and destination addresses and a transfer count. When the transfer completes, the device raises an interrupt.

The kernel provides the DMA API (dma_alloc_coherent, dma_map_single, dma_unmap_single) for driver authors. Drivers must use these functions rather than allocating memory directly because not all memory is accessible to devices. On systems with an IOMMU, the DMA API handles address translation transparently.

Two types of DMA exist: coherent (consistent) DMA, where the CPU and device see the same memory — used for control structures like descriptor rings — and streaming DMA, used for bulk data transfers. Streaming DMA requires explicit cache synchronization (dma_map/unmap) because CPU caches may contain stale data after a device write.

Writing and Building a Kernel Module

A minimal Linux kernel module consists of an initialization function (module_init), a cleanup function (module_exit), and a license declaration. The build system uses the kernel’s Makefile infrastructure through the Kbuild framework.

Drivers must handle concurrency carefully. Multiple processes can call open, read, and write simultaneously. The Linux kernel provides spinlocks (for short critical sections where sleeping is forbidden), mutexes (for longer sections that may sleep), atomic operations (for simple counters), and RCU (for read-mostly data accessed on hot paths).

Debugging kernel modules requires specialized tools. printk (kernel’s printf) with log levels (KERN_INFO, KERN_ERR, KERN_DEBUG) is the diagnostic workhorse. Ftrace provides dynamic function tracing: specific kernel functions can be instrumented at runtime without recompilation. KASAN (Kernel Address Sanitizer) detects use-after-free and buffer overflow bugs. Kgdb allows source-level debugging over a serial connection or network (kgdboe).

Ioctl: Beyond Read and Write

The ioctl system call (or its unlocked variant, unlocked_ioctl) allows drivers to implement operations that do not fit the read/write model — configuring device parameters, retrieving hardware status, or triggering special commands. The ioctl handler receives a command number (constructed with _IO, _IOW, _IOR, or _IOWR macros) and an argument that is typically a pointer to user-space memory.

long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct my_config __user *ucfg = (void __user *)arg;
    struct my_config kcfg;
    switch (cmd) {
    case MY_SET_CONFIG:
        if (copy_from_user(&kcfg, ucfg, sizeof(kcfg)))
            return -EFAULT;
        /* apply configuration */
        break;
    case MY_GET_STATUS:
        /* read hardware status register */
        break;
    }
---

The copy_from_user and copy_to_user functions are mandatory for data transfer — the kernel must never dereference user-space pointers directly, as they may point to invalid, swapped-out, or malicious memory.

FAQ

What is the difference between a kernel module and a device driver?

A kernel module is any dynamically loadable code that extends the kernel’s functionality — this includes device drivers, filesystem implementations, security modules, and network protocol handlers. A device driver is a specific type of kernel module that manages a hardware device. Not all kernel modules are drivers; for example, the nfnetlink module provides netlink socket support without controlling hardware.

How do I find which driver handles a specific device on Linux?

Use lspci -k (for PCI devices), lsusb -t (for USB), or lspci -k -s <address> for a specific device. The output shows the kernel module associated with each device. The /sys/bus/pci/devices/ directory tree provides detailed information about driver binding and device parameters.

Can device drivers be written in languages other than C?

The Linux kernel has a C-only interface for drivers because it directly accesses kernel APIs and data structures. Rust support is being introduced experimentally (starting with Linux 6.1) for certain classes of drivers. Windows KMDF (Kernel-Mode Driver Framework) supports C++.

What causes a kernel panic in a driver?

Common causes: null pointer dereference (often from failing to validate user-provided addresses), using sleeping locks in atomic context, memory corruption from off-by-one errors in buffer handling, DMA to invalid memory, and race conditions on shared data structures.

How do I debug a device driver without crashing the system?

Start with printk logging and check dmesg. Use ftrace to trace function calls with trace-cmd. Run with KASAN enabled (CONFIG_KASAN=y) to catch memory bugs. For interactive debugging, use kgdb with a second machine over serial or use QEMU/GDB for virtual hardware. Test on a non-production system first.


The Kernel Architecture Guide explains the kernel-space/user-space boundary and system call dispatch in greater depth. The I/O Systems Guide covers buffering, caching, and interrupt handling in the broader I/O subsystem context. The definitive reference for Linux device drivers is Corbet, Rubini, and Kroah-Hartman’s Linux Device Drivers, 3rd Edition, freely available at lwn.net/Kernel/LDD3/.

Section: Operating Systems 1501 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top