Skip to content

Linux Kernel Modules — Complete Guide to LKMs, Device Drivers & Module Programming

DodaTech Updated 2026-06-23 9 min read

In this tutorial, you'll learn about Linux Kernel Modules. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Linux kernel modules are pieces of code that can be loaded into the running kernel to extend its functionality without rebooting — the foundation of device drivers, file systems, and security modules in Linux.

What You'll Learn & Why It Matters

In this tutorial, you'll learn how to write, compile, load, and debug Linux kernel modules (LKMs). Kernel modules power every hardware interaction on Linux — from your Wi-Fi card to your SSD. Understanding LKMs lets you write device drivers, build security modules, and debug kernel-level issues.

Real-world use: When you plug a USB drive into a Linux system, the kernel detects the device and loads the appropriate driver module (usb-storage, uas, or nvme). Durga Antivirus Pro uses kernel modules for real-time file scanning at the VFS layer, intercepting file operations before they reach user space.

graph LR
    subgraph "User Space"
        APP[Application
ls, cat, open] LIBC[libc / glibc] end subgraph "Kernel Space" SYSCALL[System Call Interface] VFS[Virtual File System] LKM[Kernel Module
Device Driver] HARDWARE[Hardware] end APP --> LIBC --> SYSCALL --> VFS --> LKM --> HARDWARE style LKM fill:#f97316,color:#fff

What Is a Kernel Module?

A kernel module is object code that can be inserted into the Linux kernel at runtime. Unlike user-space programs, modules run in kernel space (ring 0) with full access to system memory and hardware.

Key characteristics:

Property Description
Extension .ko (kernel object)
Location /lib/modules/$(uname -r)/
Lifecycle Loaded/removed at runtime
Privilege Kernel space (ring 0)
Debugging dmesg, /proc, ftrace

Module Lifecycle

class KernelModule:
    def __init__(self, name, author, license='GPL'):
        self.name = name
        self.author = author
        self.license = license
        self.state = 'unloaded'
        self.refcount = 0
        self.dependents = []

    def init(self):
        print(f'[{self.name}] init_module() called')
        print(f'[{self.name}] License: {self.license}')
        print(f'[{self.name}] Allocating resources...')
        self.state = 'loaded'
        self.refcount = 1
        print(f'[{self.name}] Module loaded successfully')
        return 0

    def cleanup(self):
        print(f'[{self.name}] cleanup_module() called')
        print(f'[{self.name}] Releasing resources...')
        self.state = 'unloaded'
        self.refcount = 0
        print(f'[{self.name}] Module unloaded')

    def add_dependency(self, module):
        self.dependents.append(module)
        module.refcount += 1

    def __repr__(self):
        return (f'Module({self.name}, state={self.state}, '
                f'refcnt={self.refcount}, deps={len(self.dependents)})')

# Simulate module lifecycle
module = KernelModule('dodatech_usb', 'DodaTech', 'GPL v2')
module.init()
print(module)
module.cleanup()
print(module)

Expected output:

[dodatech_usb] init_module() called
[dodatech_usb] License: GPL v2
[dodatech_usb] Allocating resources...
[dodatech_usb] Module loaded successfully
Module(dodatech_usb, state=loaded, refcnt=1, deps=0)
[dodatech_usb] cleanup_module() called
[dodatech_usb] Releasing resources...
[dodatech_usb] Module unloaded
Module(dodatech_usb, state=unloaded, refcnt=0, deps=0)

Writing a Simple Kernel Module

A minimal LKM needs two functions: init_module (called on load) and cleanup_module (called on unload).

// helloworld.c — Minimal Linux Kernel Module
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("DodaTech Tutorials");
MODULE_DESCRIPTION("A simple hello world kernel module");

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, kernel! Module loaded.\n");
    printk(KERN_INFO "Current jiffies: %lu\n", jiffies);
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel! Module unloaded.\n");
    printk(KERN_INFO "Uptime at unload: %lu jiffies\n", jiffies);
}

module_init(hello_init);
module_exit(hello_exit);

Build it with a Makefile:

obj-m += helloworld.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
# Build, load, and test the module
make
sudo insmod helloworld.ko
lsmod | grep helloworld
sudo rmmod helloworld
dmesg | tail -5

Expected output:

[ 1234.567890] Hello, kernel! Module loaded.
[ 1234.567895] Current jiffies: 4294956789
[ 1240.123456] Goodbye, kernel! Module unloaded.
[ 1240.123460] Uptime at unload: 4294959789 jiffies

Module Parameters

Modules can accept parameters passed at load time.

// param_demo.c — Kernel Module with Parameters
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>

static int debug_level = 0;
static char *device_name = "dodatech0";
static int array_param[4] = {0, 0, 0, 0};
static int array_count;

module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Device name string");
module_param_array(array_param, int, &array_count, 0644);
MODULE_PARM_DESC(array_param, "Sample integer array");

static int __init param_init(void)
{
    int i;
    printk(KERN_INFO "Module loaded with:\n");
    printk(KERN_INFO "  debug_level = %d\n", debug_level);
    printk(KERN_INFO "  device_name = %s\n", device_name);
    for (i = 0; i < array_count; i++)
        printk(KERN_INFO "  array[%d] = %d\n", i, array_param[i]);
    return 0;
}

static void __exit param_exit(void)
{
    printk(KERN_INFO "Param module unloaded\n");
}

module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL");
# Load with parameters
sudo insmod param_demo.ko debug_level=2 device_name="scanner0" array_param=10,20,30,40
dmesg | tail -6

Expected output:

[ 1234.567890] Module loaded with:
[ 1234.567891]   debug_level = 2
[ 1234.567892]   device_name = scanner0
[ 1234.567893]   array[0] = 10
[ 1234.567894]   array[1] = 20
[ 1234.567895]   array[2] = 30
[ 1234.567896]   array[3] = 40

Character Device Driver

A more practical module that creates a character device readable from user space.

// chardev.c — Simple Character Device Driver
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#define DEVICE_NAME "dodatech_dev"
#define CLASS_NAME "dodatech"
#define BUFFER_SIZE 256

static int major_num;
static struct class *device_class = NULL;
static struct device *device = NULL;
static char device_buffer[BUFFER_SIZE];
static int buffer_len = 0;

static int dev_open(struct inode *inodep, struct file *filep)
{
    printk(KERN_INFO "Device opened\n");
    return 0;
}

static int dev_release(struct inode *inodep, struct file *filep)
{
    printk(KERN_INFO "Device closed\n");
    return 0;
}

static ssize_t dev_read(struct file *filep, char __user *buffer,
                        size_t len, loff_t *offset)
{
    int bytes_to_copy;
    if (*offset >= buffer_len)
        return 0;
    bytes_to_copy = min(len, (size_t)(buffer_len - *offset));
    if (copy_to_user(buffer, device_buffer + *offset, bytes_to_copy))
        return -EFAULT;
    *offset += bytes_to_copy;
    return bytes_to_copy;
}

static ssize_t dev_write(struct file *filep, const char __user *buffer,
                         size_t len, loff_t *offset)
{
    int bytes_to_copy = min(len, (size_t)BUFFER_SIZE);
    if (copy_from_user(device_buffer, buffer, bytes_to_copy))
        return -EFAULT;
    buffer_len = bytes_to_copy;
    printk(KERN_INFO "Received %d bytes: %s\n", buffer_len, device_buffer);
    return bytes_to_copy;
}

static struct file_operations fops = {
    .open = dev_open,
    .read = dev_read,
    .write = dev_write,
    .release = dev_release,
};

static int __init chardev_init(void)
{
    major_num = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_num < 0) {
        printk(KERN_ALERT "Failed to register device\n");
        return major_num;
    }
    device_class = class_create(THIS_MODULE, CLASS_NAME);
    device = device_create(device_class, NULL,
                          MKDEV(major_num, 0), NULL, DEVICE_NAME);
    printk(KERN_INFO "Device registered: major=%d\n", major_num);
    return 0;
}

static void __exit chardev_exit(void)
{
    device_destroy(device_class, MKDEV(major_num, 0));
    class_destroy(device_class);
    unregister_chrdev(major_num, DEVICE_NAME);
    printk(KERN_INFO "Device unregistered\n");
}

module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
# Test the character device
sudo insmod chardev.ko
MAJOR=$(awk '/dodatech_dev/ {print $1}' /proc/devices)
sudo mknod /dev/dodatech_dev c $MAJOR 0
echo "Hello from user space" | sudo tee /dev/dodatech_dev
sudo cat /dev/dodatech_dev
sudo rm /dev/dodatech_dev
sudo rmmod chardev.ko

Expected output:

Hello from user space

Common Mistakes

1. Dereferencing Null Pointers in Kernel Space

A NULL pointer dereference in kernel space causes a kernel panic (not just a segfault). Always check pointers with if (!ptr) return -EFAULT;.

2. Not Setting MODULE_LICENSE

Without MODULE_LICENSE("GPL"), the kernel taints itself and certain symbols are unavailable. Proprietary modules get a "tainted" kernel warning.

3. Memory Leaks in Module Code

Kernel memory is not reclaimed on module unload unless you free it. Use kfree() for every kmalloc(). Use kmemleak to detect leaks.

4. Using Busy Waiting Instead of Timers

Never use while (1); in kernel code. Use kernel timers, workqueues, or wait queues instead. Busy waiting locks the entire system.

5. Ignoring Kernel Concurrency

Multiple processes can call your module's functions simultaneously. Protect shared data with spinlocks or mutexes. The kernel does not serialize driver calls.

6. Returning Wrong Error Codes

Return negative errno values (-ENOMEM, -EINVAL, -EFAULT). Returning 0 on failure causes undefined behavior. Check the kernel's errno-base.h for valid codes.

Practice Questions

1. What is the difference between a kernel module and a user-space program? A kernel module runs in kernel space (ring 0) with full hardware access, loaded at runtime. A user-space program runs in ring 3, restricted to virtual memory and system calls. Module bugs can crash the entire system; user-space bugs only crash the Process.

2. How does printk differ from printf? printk outputs to the kernel log buffer (viewable via dmesg), not stdout. It uses log levels like KERN_INFO, KERN_ALERT. It can be called from interrupt context. printf outputs to stdout in user space.

3. What is the purpose of __init and __exit macros? __init marks code that runs only during module initialization; the kernel frees that memory after init completes. __exit marks code used only during cleanup; it is discarded if the module is built into the kernel (not loadable).

4. Challenge: Write a kernel module that creates a /proc/dodatech_stats entry. When read, it returns the number of times the module has been opened and the current uptime in seconds. Use proc_create() and remove_proc_entry().

5. Real-World Task: Examine all currently loaded modules on your system with lsmod. Trace which module provides your network driver (ethtool -i eth0), then inspect its parameters via modinfo -p <module>. Unload and reload the module to see the kernel log messages.

Mini Project: Process Monitor Module

Build a kernel module that periodically logs the top CPU-consuming processes:

import subprocess
import time

class KernelProcMonitor:
    """Simulate a kernel module that monitors processes"""

    def __init__(self, interval=5):
        self.interval = interval
        self.running = False
        self.snapshots = []

    def collect_snapshot(self):
        result = subprocess.run(
            ['ps', '-eo', 'pid,comm,%cpu,%mem,etime',
             '--sort=-%cpu', '--no-headers'],
            capture_output=True, text=True
        )
        lines = result.stdout.strip().split('\n')[:5]
        snapshot = {
            'timestamp': time.time(),
            'top_processes': lines
        }
        self.snapshots.append(snapshot)
        return snapshot

    def start_monitoring(self, duration=30):
        self.running = True
        start = time.time()
        while time.time() - start < duration:
            snapshot = self.collect_snapshot()
            print(f'\n[Monitor] Top processes at {time.ctime(snapshot["timestamp"])}:')
            for line in snapshot['top_processes']:
                print(f'  {line}')
            time.sleep(self.interval)
        self.running = False

monitor = KernelProcMonitor(interval=3)
monitor.start_monitoring(duration=12)

Expected output:

[Monitor] Top processes at Mon Jun 23 12:00:05 2026:
  1234 firefox      12.5  3.2  02:34:12
  5678 python3       8.1  1.5  00:12:34
  9011 Xorg          5.2  1.8  12:45:01

[Monitor] Top processes at Mon Jun 23 12:00:08 2026:
  1234 firefox      11.8  3.2  02:34:15
  5678 python3       9.0  1.5  00:12:37
  9011 Xorg          5.0  1.8  12:45:04

FAQ

What is kernel tainting and why does it happen?

A kernel is "tainted" when a proprietary or GPL-incompatible module is loaded. Tainted kernels disable support from kernel developers because the source of potential bugs cannot be audited. Check taint status: cat /proc/sys/kernel/tainted.

Can kernel modules be debugged with GDB?

Yes, but only over a serial connection or with kgdb (kernel GDB). You need two machines or a VM: one running the target kernel, one running GDB. For simpler debugging, use printk, ftrace, and trace-cmd.

What is the difference between insmod and modprobe?

insmod loads a single module file by path, ignoring dependencies. modprobe loads a module by name and automatically resolves and loads all dependencies. Always use modprobe for production; use insmod only for development.

System Calls
Device Drivers
Linux Namespaces

What's Next

You now understand Linux kernel modules. Next, learn about system calls to see how user-space programs communicate with the kernel, then explore device drivers for deeper hardware interaction.

  • Practice daily — Run lsmod and explore loaded modules. Pick one and read its source in the kernel tree.
  • Build a project — Write a simple character driver and write a user-space program to communicate with it via ioctl.
  • Explore related topics — Check out Linux Security Modules (LSMs) like SELinux and AppArmor.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro