Skip to content

Device Drivers — Kernel Module Programming Guide

DodaTech Updated 2026-06-21 9 min read

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
â„šī¸ Info

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

What is the difference between kernel module and kernel driver?

A kernel module is a loadable piece of kernel code. A device driver is a specific type of kernel module that controls hardware. All drivers are modules, but not all modules are drivers (e.g., filesystem modules).

What is ioctl and when is it used?

ioctl (I/O control) is a system call for device-specific operations that don't fit read/write. Examples: configuring serial port baud rate, ejecting a CD-ROM, setting GPU resolution. ioctl commands pass an integer request code and optional argument.

How do platform drivers differ from PCI drivers?

Platform drivers are for devices on non-discoverable buses (Embedded Systems, device tree). PCI drivers handle PCI/PCIe devices which are self-discoverable. Platform drivers match via device tree or ACPI; PCI drivers match via vendor/device IDs.

File Systems
OS Security
Interprocess Communication

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 lsmod to see loaded modules, cat /proc/devices to 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