Loading content...
Writing device drivers is fundamentally different from writing user-space applications. There's no standard library to fall back on, no memory protection to save you from pointer errors, and a single bug can crash the entire system. Yet driver development is also deeply rewarding—you're working at the boundary between hardware and software, bringing physical devices to life.
Driver development demands a unique combination of skills: understanding of hardware interfaces, mastery of kernel programming conventions, meticulous attention to error handling, and a systematic approach to testing. This page equips you with the knowledge and practices needed to develop reliable, maintainable, and high-performance device drivers.
By the end of this page, you will understand the driver development environment and toolchain, essential coding patterns and conventions for kernel code, memory management in drivers, error handling strategies, testing and debugging methodologies, and the development workflow used by professional kernel developers. This knowledge forms the practical foundation for writing production-quality drivers.
Setting up a proper development environment is crucial for productive driver development. Unlike application development where you can iterate quickly, kernel crashes require reboots, making efficient workflows essential.
Essential Components:
1. Kernel Source Tree: Driver development requires kernel headers and often the full source tree. The headers define data structures, macros, and function prototypes. The source enables understanding how the kernel works and studying existing drivers as references.
2. Cross-Compilation Toolchain: For embedded systems, you'll compile on a development host for a different target architecture. Even for native development, understanding cross-compilation is valuable.
3. Virtualization: Virtual machines (QEMU, VirtualBox, VMware) allow rapid testing without rebooting physical hardware. They also enable debugging with host-side tools.
123456789101112131415161718192021222324252627
# Install kernel development packages (Debian/Ubuntu)sudo apt-get install build-essential linux-headers-$(uname -r)sudo apt-get install libelf-dev libssl-dev bc flex bison # For full kernel builds (optional but recommended)sudo apt-get install git fakeroot libncurses-dev # Get kernel source (match your running kernel)cd /usr/srcsudo apt-get source linux-image-$(uname -r)# Or for latest from kernel.org:git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git # Set up QEMU for testing (highly recommended)sudo apt-get install qemu-system-x86 # Create a test VM imageqemu-img create -f qcow2 test-vm.qcow2 20G # Install buildroot for minimal test systemsgit clone https://github.com/buildroot/buildroot.git # Static analysis toolssudo apt-get install sparse coccinelle # Debugging toolssudo apt-get install crash gdb| Component | Purpose | Recommendation |
|---|---|---|
| Kernel Headers | Type definitions and function prototypes | Match exactly to target kernel version |
| Build System | Compile modules with correct flags | Use provided Kbuild/Makefile infrastructure |
| Test VM | Safe environment for testing | QEMU with minimal rootfs (buildroot) |
| Hardware Setup | Test on actual devices | Start with VM, graduate to real hardware |
| Serial Console | Capture kernel messages during crashes | Essential for kernel debugging |
| Version Control | Track changes, bisect bugs | Git with good commit hygiene |
Develop with this cycle: edit code on host → compile → copy module to VM → load and test → review dmesg. This cycle should take seconds, not minutes. Set up NFS or 9p filesystem sharing between host and VM for instant access to rebuilt modules.
The Linux kernel uses a custom build system called Kbuild. While you could compile drivers manually with gcc, Kbuild handles the complex task of matching compiler flags, include paths, and configuration to the target kernel. Using Kbuild is not optional—drivers built without it will fail to load.
Why Kbuild Matters:
12345678910111213141516171819202122232425262728293031323334353637383940414243
# Simple external module Makefile# Place in your driver source directory # Module name (without .ko extension)obj-m := mydriver.o # If driver has multiple source files:# mydriver-y := main.o hardware.o interrupt.o # Kernel build directory (adjust for your system)KDIR ?= /lib/modules/$(shell uname -r)/build # Enable extra warnings for developmentccflags-y := -Wall -Werror # Enable additional debug symbolsccflags-y += -g -DDEBUG # Default target: build moduleall: $(MAKE) -C $(KDIR) M=$(PWD) modules # Clean target: remove built filesclean: $(MAKE) -C $(KDIR) M=$(PWD) clean # Install module to system directoryinstall: $(MAKE) -C $(KDIR) M=$(PWD) modules_install # Check coding stylecheckpatch: $(KDIR)/scripts/checkpatch.pl --file *.c *.h # Static analysis with sparsesparse: $(MAKE) -C $(KDIR) M=$(PWD) C=2 modules # Generate compile_commands.json for IDE supportcompile_commands: $(MAKE) -C $(KDIR) M=$(PWD) compile_commands.json .PHONY: all clean install checkpatch sparse compile_commands1234567891011121314151617181920212223242526272829
# Kconfig file for driver that will be merged into kernel tree config MY_DRIVER tristate "My Example Device Driver" depends on PCI select FW_LOADER help This driver supports the Example Device from ACME Corporation. The driver supports: - Automatic device detection - DMA data transfers - Power management To compile this driver as a module, choose M here: the module will be called mydriver. If unsure, say N. config MY_DRIVER_DEBUG bool "Enable debug logging for My Driver" depends on MY_DRIVER help Enable verbose debug logging for the My Driver. This increases code size and reduces performance. Only enable for debugging purposes. If unsure, say N.Module Loading and Unloading:
# Build the module
make
# Load module (as root)
sudo insmod mydriver.ko
# Load with parameters
sudo insmod mydriver.ko debug=1 buffer_size=4096
# Check if loaded
lsmod | grep mydriver
# View kernel messages from driver
dmesg | tail -20
# Unload module
sudo rmmod mydriver
# Or use modprobe for dependency handling
sudo modprobe mydriver # Load
sudo modprobe -r mydriver # Unload
Modules must be compiled against headers that exactly match the running kernel. Loading a module compiled for a different kernel version causes 'Invalid module format' errors. The MODVERSIONS feature adds checksum-based verification of kernel symbols to catch mismatches.
Memory management in kernel code differs fundamentally from user-space. There's no malloc/free—instead, you use kernel allocators that understand the system's memory constraints and context requirements. Understanding these allocators is essential for writing correct, efficient drivers.
Key Differences from User Space:
| Function | Use Case | Can Sleep? | Output |
|---|---|---|---|
| kmalloc(size, flags) | Small allocations (<128KB typical) | Depends on flags | Physically contiguous |
| kzalloc(size, flags) | Same as kmalloc, but zero-initialized | Depends on flags | Zeroed, physically contiguous |
| kcalloc(n, size, flags) | Array allocation with overflow check | Depends on flags | Zeroed, contiguous |
| vmalloc(size) | Large allocations (any size) | Yes (always) | Virtually contiguous only |
| kvmalloc(size, flags) | Try kmalloc, fall back to vmalloc | Depends on flags | Best effort contiguity |
| devm_kmalloc(dev, size, flags) | Device-managed allocation | Depends on flags | Auto-freed on device removal |
| alloc_pages(flags, order) | Page-granular allocation | Depends on flags | 2^order contiguous pages |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
#include <linux/slab.h>#include <linux/vmalloc.h>#include <linux/gfp.h> /* GFP Flags - control allocation behavior */ /* In process context - can sleep if memory is tight */void *ptr1 = kmalloc(size, GFP_KERNEL); /* In interrupt context or with locks held - cannot sleep */void *ptr2 = kmalloc(size, GFP_ATOMIC); /* For DMA - allocate from DMA-capable zone */void *ptr3 = kmalloc(size, GFP_DMA); /* Don't report failure to console (for optional allocations) */void *ptr4 = kmalloc(size, GFP_KERNEL | __GFP_NOWARN); /* Strongly prefer to succeed, might kill processes */void *ptr5 = kmalloc(size, GFP_KERNEL | __GFP_HIGH); /* Common allocation patterns */struct my_struct *device_data; /* Pattern 1: Basic allocation with error check */device_data = kzalloc(sizeof(*device_data), GFP_KERNEL);if (!device_data) return -ENOMEM; /* Pattern 2: Device-managed (freed automatically on remove) */device_data = devm_kzalloc(dev, sizeof(*device_data), GFP_KERNEL);if (!device_data) return -ENOMEM;/* No corresponding kfree needed! */ /* Pattern 3: Large allocation - use vmalloc */large_buffer = vmalloc(1024 * 1024); /* 1MB */if (!large_buffer) return -ENOMEM;/* Must use vfree() to release */ /* Pattern 4: Array with overflow checking */items = kcalloc(count, sizeof(struct item), GFP_KERNEL);if (!items) return -ENOMEM;/* kcalloc checks for size_t overflow in count * size */ /* Pattern 5: Per-CPU allocations for performance */percpu_stats = alloc_percpu(struct driver_stats);if (!percpu_stats) return -ENOMEM;/* Access with per_cpu_ptr() or this_cpu_ptr() */ /* ALWAYS check allocation results! */Kernel memory leaks are critical bugs—unlike user processes, the kernel never exits to clean up. Enable KMEMLEAK during development to automatically detect leaks. Every kmalloc must have a corresponding kfree on every code path. Consider using devm_* functions to let the kernel manage cleanup.
The devm_ (Device-Managed) Pattern:*
Device-managed functions tie resource lifetime to device lifetime. When the device is removed (driver unload, hot-unplug), all devm-allocated resources are automatically freed in reverse order. This dramatically simplifies cleanup code and prevents leaks.
static int mydev_probe(struct pci_dev *pdev, ...)
{
struct mydev_data *data;
void __iomem *regs;
/* All these are automatically freed if probe fails or on remove */
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
regs = devm_ioremap(&pdev->dev, bar_addr, bar_size);
if (!regs)
return -EIO;
ret = devm_request_irq(&pdev->dev, irq, handler, 0, "mydev", data);
if (ret)
return ret;
/* No cleanup code needed on error - devm handles it! */
return 0;
}
static void mydev_remove(struct pci_dev *pdev)
{
/* devm automatically frees everything - often this is empty! */
}
Robust error handling separates professional drivers from amateur code. In kernel space, errors that would merely crash a user process can bring down the entire system. Every operation that can fail must have its return code checked and handled appropriately.
Error Handling Principles:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
/* Pattern 1: Traditional goto-based cleanup */static int mydev_probe(struct pci_dev *pdev, ...){ struct mydev_data *data; int ret; data = kzalloc(sizeof(*data), GFP_KERNEL); if (!data) { ret = -ENOMEM; goto err_alloc; } ret = pci_enable_device(pdev); if (ret) { dev_err(&pdev->dev, "Failed to enable device: %d\n", ret); goto err_enable; } ret = pci_request_regions(pdev, DRIVER_NAME); if (ret) { dev_err(&pdev->dev, "Failed to request regions: %d\n", ret); goto err_regions; } data->regs = pci_iomap(pdev, 0, 0); if (!data->regs) { dev_err(&pdev->dev, "Failed to map BAR0\n"); ret = -EIO; goto err_iomap; } ret = request_irq(pdev->irq, mydev_isr, IRQF_SHARED, DRIVER_NAME, data); if (ret) { dev_err(&pdev->dev, "Failed to request IRQ %d: %d\n", pdev->irq, ret); goto err_irq; } pci_set_drvdata(pdev, data); return 0; /* Cleanup in reverse order of initialization */err_irq: pci_iounmap(pdev, data->regs);err_iomap: pci_release_regions(pdev);err_regions: pci_disable_device(pdev);err_enable: kfree(data);err_alloc: return ret;} /* Pattern 2: Using devm_* (preferred for most cases) */static int mydev_probe_devm(struct pci_dev *pdev, ...){ struct mydev_data *data; int ret; data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; ret = pcim_enable_device(pdev); /* Managed PCI enable */ if (ret) return dev_err_probe(&pdev->dev, ret, "Enable failed\n"); ret = pcim_iomap_regions(pdev, BIT(0), DRIVER_NAME); if (ret) return dev_err_probe(&pdev->dev, ret, "Map failed\n"); data->regs = pcim_iomap_table(pdev)[0]; ret = devm_request_irq(&pdev->dev, pdev->irq, mydev_isr, IRQF_SHARED, DRIVER_NAME, data); if (ret) return dev_err_probe(&pdev->dev, ret, "IRQ failed\n"); pci_set_drvdata(pdev, data); return 0; /* No cleanup needed - devm handles everything! */}| Error | Value | Meaning | Common Cause |
|---|---|---|---|
| -ENOMEM | -12 | Out of memory | kmalloc/vmalloc failure |
| -EINVAL | -22 | Invalid argument | Bad parameter from user space |
| -EBUSY | -16 | Resource busy | Device already in use |
| -EIO | -5 | I/O error | Hardware communication failure |
| -ENODEV | -19 | No such device | Device not found or removed |
| -EAGAIN | -11 | Try again | Non-blocking I/O, data not ready |
| -ETIMEDOUT | -110 | Operation timed out | Hardware didn't respond |
| -EFAULT | -14 | Bad address | Invalid user-space pointer |
The dev_err_probe() function combines error logging with return. It handles -EPROBE_DEFER specially (suppressing the message, since deferred probe is normal), making probe functions cleaner. Use it instead of dev_err() followed by return in modern drivers.
The Linux kernel has strict coding style requirements documented in Documentation/process/coding-style.rst. Following these conventions isn't just about aesthetics—it ensures consistency across millions of lines of code from thousands of contributors, making the code maintainable by anyone in the community.
Key Style Requirements:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
/* Good: Function naming and layout */static int enable_device_feature(struct my_device *dev, int feature_id){ u32 reg_val; int ret; if (feature_id < 0 || feature_id >= MAX_FEATURES) return -EINVAL; reg_val = readl(dev->regs + FEATURE_REG); reg_val |= BIT(feature_id); writel(reg_val, dev->regs + FEATURE_REG); /* Wait for feature to become active */ ret = wait_for_feature_active(dev, feature_id); if (ret) dev_err(dev->dev, "Feature %d activation failed\n", feature_id); return ret;} /* Good: Structure initialization */static const struct file_operations mydev_fops = { .owner = THIS_MODULE, .open = mydev_open, .release = mydev_release, .read = mydev_read, .write = mydev_write, .unlocked_ioctl = mydev_ioctl,}; /* Good: Comments explain the non-obvious */ /* * We must disable interrupts before entering low-power mode * because the interrupt controller loses state during power * transitions. Interrupts are re-enabled in the resume path. */disable_irq(dev->irq); /* Bad: Comment restates the obvious code *//* Increment the counter */counter++; /* Good: Readable complex conditions */if (data->flags & FLAG_ENABLED && data->state == STATE_READY && time_after(jiffies, data->timeout)) { process_data(data);} /* Good: Use kernel helper macros */#define DRIVER_NAME "mydriver" BUILD_BUG_ON(sizeof(struct packet) != 64); /* Compile-time check */WARN_ON(ptr == NULL); /* Runtime check */ /* Use kernel types */u8 byte_val; /* Not: unsigned char */u16 short_val; /* Not: unsigned short */u32 word_val; /* Not: unsigned int */u64 quad_val; /* Not: unsigned long long */ /* Use __iomem for memory-mapped I/O pointers */void __iomem *regs;u32 value = readl(regs + OFFSET); /* Not: *(u32 *)(regs + OFFSET) */Using checkpatch.pl:
The kernel includes a script that checks patches and files for style violations:
# Check a patch before submitting
./scripts/checkpatch.pl my-changes.patch
# Check source files directly
./scripts/checkpatch.pl --file drivers/mydriver/*.c
# Common output:
WARNING: line length of 92 exceeds 80 columns
#42: FILE: mydriver.c:42:
+ very_long_function_name(parameter1, parameter2, parameter3, parameter4);
ERROR: space required after ',' (ctx:VxV)
#55: FILE: mydriver.c:55:
+ function(arg1,arg2);
Address all ERRORs and most WARNINGs before submitting patches upstream.
You might prefer 4-space indentation or 100-column lines. That doesn't matter. The kernel's style exists for maintainability across a massive codebase. Follow the style of the file you're editing. When starting new files, follow Documentation/process/coding-style.rst exactly.
Testing kernel code is challenging—you can't just run a debugger interactively, and bugs crash the system. A systematic testing strategy combining multiple approaches is essential for quality driver development.
Testing Layers:
| Tool | Type | What It Catches | When to Use |
|---|---|---|---|
| sparse | Static Analysis | Type errors, null derefs, context violations | Every build |
| smatch | Static Analysis | Logic errors, resource leaks | Every build |
| coccinelle | Semantic Patching | API misuse patterns | Periodic checks |
| KASAN | Runtime | Memory safety (use-after-free, overflow) | Development builds |
| KMSAN | Runtime | Uninitialized memory use | Development builds |
| KCSAN | Runtime | Data races | SMP testing |
| lockdep | Runtime | Lock ordering, deadlocks | Always enabled |
| kmemleak | Runtime | Memory leaks | Development builds |
| syzkaller | Fuzzing | System call interface bugs | Pre-release testing |
1234567891011121314151617181920212223242526272829303132
# In kernel .config for development: # Memory debuggingCONFIG_KASAN=y # Kernel Address SanitizerCONFIG_KASAN_INLINE=y # Inline instrumentation (faster)CONFIG_KMEMLEAK=y # Memory leak detectorCONFIG_DEBUG_SLAB=y # SLAB debugging # Lock debugging CONFIG_PROVE_LOCKING=y # Lock dependency validator (lockdep)CONFIG_DEBUG_LOCK_ALLOC=y # Lock allocation debuggingCONFIG_DEBUG_SPINLOCK=y # Spinlock debuggingCONFIG_DEBUG_MUTEXES=y # Mutex debugging # General debuggingCONFIG_DEBUG_KERNEL=y # Enable debug infrastructureCONFIG_DEBUG_INFO=y # Include debug symbolsCONFIG_DEBUG_BUGVERBOSE=y # Verbose BUG messagesCONFIG_DYNAMIC_DEBUG=y # Runtime debug print control # Race detectionCONFIG_KCSAN=y # Concurrency sanitizer # Runtime checksCONFIG_BUG=yCONFIG_DEBUG_MISC=y # Example: Running sparse on your drivermake C=1 M=drivers/mydriver # Example: Running with KASAN messages visibledmesg | grep -i kasanWriting KUnit Tests:
KUnit is the kernel's unit testing framework, allowing you to test kernel code in isolation:
#include <kunit/test.h>
static void test_buffer_init(struct kunit *test)
{
struct my_buffer *buf;
buf = my_buffer_alloc(1024);
KUNIT_ASSERT_NOT_NULL(test, buf);
KUNIT_EXPECT_EQ(test, buf->size, 1024);
KUNIT_EXPECT_EQ(test, buf->used, 0);
my_buffer_free(buf);
}
static void test_buffer_write(struct kunit *test)
{
struct my_buffer *buf = my_buffer_alloc(100);
int ret;
ret = my_buffer_write(buf, "hello", 5);
KUNIT_EXPECT_EQ(test, ret, 5);
KUNIT_EXPECT_EQ(test, buf->used, 5);
my_buffer_free(buf);
}
static struct kunit_case my_buffer_test_cases[] = {
KUNIT_CASE(test_buffer_init),
KUNIT_CASE(test_buffer_write),
{}
};
static struct kunit_suite my_buffer_test_suite = {
.name = "my_buffer",
.test_cases = my_buffer_test_cases,
};
kunit_test_suite(my_buffer_test_suite);
Test your driver across: multiple kernel versions, with and without debug options, on UP (single CPU) and SMP, with different memory sizes, under memory pressure. Many bugs only appear under specific conditions. Automated CI testing with QEMU can run this matrix efficiently.
Professional driver development requires comprehensive documentation. This serves multiple audiences: users who need to configure the driver, administrators who troubleshoot issues, and future developers who maintain or extend the code.
Documentation Components:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
/** * struct my_device - Device-specific data structure * @dev: Parent device structure * @regs: Memory-mapped register base address * @irq: Assigned interrupt number * @lock: Spinlock protecting hardware access * @status: Current device status (enum my_dev_status) * * This structure represents a single instance of the My Device * hardware. One instance is allocated for each device detected * during probe. The structure lifetime matches the device lifetime. */struct my_device { struct device *dev; void __iomem *regs; int irq; spinlock_t lock; enum my_dev_status status;}; /** * mydev_read_data - Read data from device FIFO * @dev: Device to read from * @buffer: Buffer to store read data * @len: Maximum bytes to read * * Reads up to @len bytes from the device's receive FIFO into * @buffer. The function may return fewer bytes if the FIFO * contains less data. * * Context: May be called from process context only. Acquires * the device spinlock internally. * * Return: Number of bytes read, or negative errno on error. * Returns -EAGAIN if FIFO is empty and O_NONBLOCK is set. * Returns -EIO if the device is in error state. */ssize_t mydev_read_data(struct my_device *dev, void *buffer, size_t len){ /* Implementation */} /** * DOC: Power Management * * The driver implements full runtime power management. The device * enters low-power mode after @idle_timeout_ms milliseconds of * inactivity. Resume is automatic upon any I/O operation. * * System-wide suspend is supported. The device state is saved * before entering suspend and restored on resume. */123456789101112131415161718
mydriver: fix race condition in interrupt handler The interrupt handler was reading the status register and then clearingit in two separate operations. On SMP systems, a second interrupt couldarrive between these operations, with its status being inadvertentlycleared by the first handler. Fix by using a single read-to-clear register access. This matches thehardware specification which states the status register is cleared onread. This race manifested as occasional missed interrupts under high load,causing DMA transfers to timeout. Fixes: 1234abcd5678 ("mydriver: add interrupt support")Reported-by: Jane User <jane@example.com>Tested-by: John Tester <john@example.com>Signed-off-by: Your Name <you@example.com>Kernel-doc comments serve as API documentation. If behavior deviates from the documented behavior, that's a bug. Keep documentation synchronized with code. The kernel provides scripts/kernel-doc tool to extract documentation and check formatting.
We've explored the practical aspects of device driver development. Let's consolidate the key takeaways:
What's Next:
With development practices covered, we'll explore driver loading—how modules are loaded into the kernel, the initialization sequence, symbol resolution, and the mechanisms for loading drivers automatically at boot or device hotplug.
You now understand the practical aspects of device driver development—from setting up your environment to testing and documentation. This knowledge enables you to develop, debug, and maintain production-quality device drivers.