Device Drivers â Kernel Module Programming Guide
In this tutorial, you'll learn about Device Drivers. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
A device driver is a kernel module that acts as a translator between hardware devices and the operating system, providing a standardized interface for user-space applications to communicate with peripherals.
What You'll Learn
In this tutorial, you'll learn the fundamentals of device driver development: the difference between character and block devices, kernel module structure, the file operations interface, interrupt handling, DMA, ioctl for device control, platform drivers, and how Linux's device model works â with practical Python simulations and C code examples.
Why It Matters
Every piece of hardware â from your keyboard to your SSD to your GPU â needs a driver. Understanding drivers helps you debug hardware issues, write kernel code, and build Embedded Systems. Malware often exploits driver vulnerabilities to gain kernel access. DodaTech's Durga Antivirus Pro uses kernel-level drivers for file system filtering and real-time protection.
Real-World Use
Graphics drivers (NVIDIA, AMD) are the most complex drivers, managing GPU memory, context switching, and display output. USB drivers handle thousands of device classes. Storage drivers (NVMe, SATA) implement the block I/O interface. Antivirus software uses file system filter drivers to scan files on access.
flowchart TB
subgraph "User Space"
APP[Application]
end
subgraph "Kernel Space"
VFS[Virtual File System]
DRV[Device Driver]
HWIF[Hardware Interface]
end
subgraph "Hardware"
DEV[Device]
end
APP -->|syscall| VFS
VFS -->|file_operations| DRV
DRV -->|I/O ports / MMIO / DMA| HWIF
HWIF -->|interrupts| DRV
HWIF --> DEV
Prerequisites: C programming or Python basics. Understanding of Operating Systems fundamentals and Linux concepts helps.
Kernel Module Basics
A Linux kernel module is code that can be loaded and unloaded dynamically. It has two mandatory entry points: init_module and cleanup_module.
// simple_module.c â Minimal Linux kernel module
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init simple_init(void) {
printk(KERN_INFO "Simple module loaded!\n");
return 0;
}
static void __exit simple_exit(void) {
printk(KERN_INFO "Simple module unloaded!\n");
}
module_init(simple_init);
module_exit(simple_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("DodaTech");
MODULE_DESCRIPTION("Minimal kernel module example");
class KernelModule:
"""Simulate a kernel module lifecycle."""
def __init__(self, name):
self.name = name
self.loaded = False
self.refcount = 0
def init(self):
print(f"[KERNEL] Loading module: {self.name}")
self.loaded = True
self.refcount = 0
return 0
def cleanup(self):
if self.refcount > 0:
print(f"[KERNEL] WARNING: {self.name} still in use ({self.refcount} refs)")
print(f"[KERNEL] Unloading module: {self.name}")
self.loaded = False
def open(self):
if not self.loaded:
raise RuntimeError(f"Module {self.name} not loaded")
self.refcount += 1
print(f"[DRIVER] {self.name}: opened (refcount={self.refcount})")
def close(self):
if self.refcount > 0:
self.refcount -= 1
print(f"[DRIVER] {self.name}: closed (refcount={self.refcount})")
mod = KernelModule("chardev_example")
mod.init()
mod.open()
mod.open()
mod.close()
mod.close()
mod.cleanup()
Expected output:
[KERNEL] Loading module: chardev_example
[DRIVER] chardev_example: opened (refcount=1)
[DRIVER] chardev_example: opened (refcount=2)
[DRIVER] chardev_example: closed (refcount=1)
[DRIVER] chardev_example: closed (refcount=0)
[KERNEL] Unloading module: chardev_example
Character vs Block Devices
| Type | Access | Buffering | Examples |
|---|---|---|---|
| Character device | Byte stream, sequential | No kernel buffering | Keyboard, serial port, mouse |
| Block device | Block-level, random access | Kernel buffer cache | SSDs, HDDs, USB drives |
| Network device | Packet-based | Socket buffers | Ethernet, Wi-Fi |
class CharacterDevice:
"""Simulate a character device driver."""
def __init__(self, name, buffer_size=1024):
self.name = name
self.buffer = bytearray(buffer_size)
self.pos = 0
def read(self, count):
data = bytes(self.buffer[self.pos:self.pos + count])
self.pos += len(data)
print(f"[{self.name}] read {len(data)} bytes at offset {self.pos - len(data)}")
return data
def write(self, data):
length = min(len(data), len(self.buffer) - self.pos)
self.buffer[self.pos:self.pos + length] = data[:length]
self.pos += length
print(f"[{self.name}] wrote {length} bytes at offset {self.pos - length}")
return length
def seek(self, offset, whence=0):
if whence == 0:
self.pos = offset
elif whence == 1:
self.pos += offset
elif whence == 2:
self.pos = len(self.buffer) + offset
print(f"[{self.name}] seek to {self.pos}")
chardev = CharacterDevice("/dev/fakechar")
chardev.write(b"Hello from kernel!")
chardev.seek(0)
data = chardev.read(10)
print(f" Read back: {data}")
Expected output:
[/dev/fakechar] wrote 17 bytes at offset 0
[/dev/fakechar] seek to 0
[/dev/fakechar] read 10 bytes at offset 0
Read back: b'Hello from'
The File Operations Interface
In Linux, character devices expose a file_operations struct with open, release, read, write, ioctl, and more.
// chardev.c â Character device driver with file operations
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/module.h>
#define DEVICE_NAME "mychardev"
#define BUFFER_SIZE 1024
static int major_num;
static char device_buffer[BUFFER_SIZE];
static int device_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "mychardev: device opened\n");
return 0;
}
static int device_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "mychardev: device closed\n");
return 0;
}
static ssize_t device_read(struct file *file, char __user *buffer,
size_t length, loff_t *offset) {
size_t bytes = min(length, (size_t)(BUFFER_SIZE - *offset));
if (copy_to_user(buffer, device_buffer + *offset, bytes))
return -EFAULT;
*offset += bytes;
return bytes;
}
static ssize_t device_write(struct file *file, const char __user *buffer,
size_t length, loff_t *offset) {
size_t bytes = min(length, (size_t)(BUFFER_SIZE - *offset));
if (copy_from_user(device_buffer + *offset, buffer, bytes))
return -EFAULT;
*offset += bytes;
return bytes;
}
static struct file_operations fops = {
.open = device_open,
.release = device_release,
.read = device_read,
.write = device_write,
};
static int __init chardev_init(void) {
major_num = register_chrdev(0, DEVICE_NAME, &fops);
if (major_num < 0) return major_num;
printk(KERN_INFO "mychardev: registered with major %d\n", major_num);
return 0;
}
class FileOperations:
"""Simulate Linux file_operations struct."""
def __init__(self):
self.handlers = {}
def register(self, name, open_fn, release_fn, read_fn, write_fn):
self.handlers[name] = {
"open": open_fn, "release": release_fn,
"read": read_fn, "write": write_fn,
}
print(f"[VFS] Registered driver: {name}")
def open(self, name):
if name in self.handlers:
return self.handlers[name]["open"]()
return -1
fops = FileOperations()
fops.register("mychardev",
open_fn=lambda: print(" open() called") or 0,
release_fn=lambda: print(" release() called") or 0,
read_fn=lambda buf, len: print(f" read({len}) called") or b"data",
write_fn=lambda buf: print(f" write({len(buf)}) called") or 0,
)
fops.open("mychardev")
Expected output:
[VFS] Registered driver: mychardev
open() called
Interrupt Handling
Drivers register interrupt handlers that run in interrupt context when hardware events occur.
import time
import threading
class InterruptController:
def __init__(self):
self.handlers = {}
def request_irq(self, irq_number, handler, name):
self.handlers[irq_number] = {"handler": handler, "name": name}
print(f"[IRQ] Registered handler for IRQ {irq_number} ({name})")
def trigger_interrupt(self, irq_number):
if irq_number in self.handlers:
print(f"[IRQ] !!! Interrupt {irq_number} received ({self.handlers[irq_number]['name']})")
self.handlers[irq_number]["handler"](irq_number)
else:
print(f"[IRQ] Unhandled interrupt {irq_number}")
class KeyboardDriver:
def __init__(self, irq_ctrl):
self.buffer = []
irq_ctrl.request_irq(1, self.keyboard_isr, "keyboard")
def keyboard_isr(self, irq):
key = f"key_{len(self.buffer)}"
self.buffer.append(key)
print(f" [ISR] Key pressed: {key}")
irq = InterruptController()
kbd = KeyboardDriver(irq)
irq.trigger_interrupt(1)
irq.trigger_interrupt(1)
print(f" Buffered keys: {kbd.buffer}")
Expected output:
[IRQ] Registered handler for IRQ 1 (keyboard)
[IRQ] !!! Interrupt 1 received (keyboard)
[ISR] Key pressed: key_0
[IRQ] !!! Interrupt 1 received (keyboard)
[ISR] Key pressed: key_1
Buffered keys: ['key_0', 'key_1']
DMA (Direct Memory Access)
DMA allows hardware devices to transfer data directly to/from RAM without CPU involvement, freeing the CPU for other tasks.
class DMAController:
def __init__(self):
self.busy = False
def transfer(self, source, dest, size):
self.busy = True
print(f"[DMA] Starting transfer: {size} bytes from {source} to {dest}")
time.sleep(0.001)
print(f"[DMA] Transfer complete!")
self.busy = False
return size
class NVMeDriver:
def __init__(self, dma):
self.dma = dma
self.buffer = bytearray(4096)
def read_sector(self, sector_num):
print(f"[NVMe] Reading sector {sector_num} via DMA...")
self.dma.transfer(f"sector_{sector_num}", self.buffer, 4096)
print(f"[NVMe] Sector {sector_num} data ready in kernel buffer")
return bytes(self.buffer)
dma = DMAController()
nvme = NVMeDriver(dma)
data = nvme.read_sector(42)
Expected output:
[NVMe] Reading sector 42 via DMA...
[DMA] Starting transfer: 4096 bytes from sector_42 to <bytearray>
[DMA] Transfer complete!
[NVMe] Sector 42 data ready in kernel buffer
Common Mistakes
1. Not Handling Concurrency
Drivers run in kernel context where multiple processes can call read/write simultaneously. Always use locks (mutex, spinlock) for shared data.
2. Sleeping in Interrupt Context
Interrupt handlers cannot sleep â no mutexes, no memory allocation with GFP_KERNEL. Use bottom halves (tasklets, workqueues) for deferred work.
3. Buffer Overflows in Kernel Code
There is no memory protection in kernel space. A buffer overflow in a driver crashes the entire system, not just one Process.
4. Forgetting to Check copy_to_user / copy_from_user
These functions can fail if the user-space pointer is invalid. Always check return values.
5. Memory Leaks in Module Cleanup
Every allocation in init must be freed in cleanup. Unreferenced kmalloc memory is lost until reboot.
Practice Questions
1. What is the difference between a character device and a block device? Character devices provide a byte stream interface (read/write sequentially). Block devices provide block-level random access with kernel buffering. Character devices are used for serial interfaces; block devices for storage.
2. What does the file_operations struct contain? It contains function pointers that the VFS calls for operations: open, release, read, write, ioctl, mmap, llseek, poll, and more. Each driver implements a subset.
3. Why can't you sleep in an interrupt handler? Interrupt handlers run in atomic context with interrupts disabled. Sleeping would block the entire system. Use bottom halves (tasklets, workqueues, threaded IRQs) for non-urgent processing.
4. What is DMA and why is it important? Direct Memory Access lets hardware transfer data directly to RAM without CPU involvement. This frees the CPU for other tasks during large data transfers (disk I/O, network packets).
5. Challenge: Write the skeleton of a Linux platform driver for a temperature sensor on I2C. Include probe, remove, a sysfs interface to read temperature, and proper power management.
Mini Project: Driver Simulator
class DeviceDriverSimulator:
def __init__(self):
self.devices = {}
def probe(self, name, device_id):
dev = {"id": device_id, "buffer": bytearray(256), "open_count": 0}
self.devices[name] = dev
print(f"[PROBE] Device {name} (id={device_id}) initialized")
return dev
def remove(self, name):
if name in self.devices:
print(f"[REMOVE] Device {name} removed")
del self.devices[name]
def ioctl(self, name, cmd, arg=None):
dev = self.devices.get(name)
if not dev:
return -1
cmds = {"GET_INFO": (lambda: f"Device {name}"), "RESET": (lambda: dev["buffer"].clear() or "Buffer reset"), "GET_STATUS": (lambda: f"open_count={dev['open_count']}")}
handler = cmds.get(cmd)
return handler() if handler else -1
sim = DeviceDriverSimulator()
sim.probe("temperature_sensor", 0x4A)
print(sim.ioctl("temperature_sensor", "GET_INFO"))
print(sim.ioctl("temperature_sensor", "GET_STATUS"))
sim.remove("temperature_sensor")
FAQ
Related Concepts
What's Next
You now understand device drivers! Next, explore OS Security for kernel protection mechanisms, and learn about Distributed Systems for distributed operating system concepts.
- Practice daily â Run
lsmodto see loaded modules,cat /proc/devicesto see registered drivers - Build a project â Write a simple character driver in C for a Raspberry Pi GPIO
- Explore related topics â Check out Linux Device Drivers, 3rd Edition (free book)
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro