Linux Kernel Modules

Chapter 18: Linux Kernel Modules

Introduction

Linux kernel modules are pieces of code that can be loaded and unloaded into the kernel dynamically, without rebooting. They extend kernel functionality - adding device drivers, file systems, network protocols, or system call handlers. Understanding kernel modules is essential for Linux kernel development and driver programming.

Why This Matters

Most Linux kernel development happens through modules. Device drivers, file systems, and many kernel features are implemented as loadable modules. Learning kernel module programming allows you to extend Linux, write device drivers, debug kernel issues, and contribute to Linux development without modifying the core kernel.

How to Study This Chapter

  1. Use a VM - Never test kernel modules on your main system
  2. Start simple - Begin with "Hello World" module
  3. Read kernel code - Study existing drivers in drivers/
  4. Check dmesg - Kernel messages go to kernel log
  5. Be careful - Kernel bugs can crash the system

Development Environment Setup

Installing Kernel Headers

# Ubuntu/Debian
sudo apt-get install build-essential linux-headers-$(uname -r)

# Fedora/RHEL
sudo dnf install kernel-devel kernel-headers

# Verify installation
ls /lib/modules/$(uname -r)/build

Required Tools

# Install development tools
sudo apt-get install gcc make git

# Useful for debugging
sudo apt-get install kmod crash

Hello World Kernel Module

Simple Module

hello.c:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");
MODULE_VERSION("1.0");

static int __init hello_init(void) {
    printk(KERN_INFO "Hello, Kernel!\n");
    return 0;  // 0 means success
}

static void __exit hello_exit(void) {
    printk(KERN_INFO "Goodbye, Kernel!\n");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile:

obj-m += hello.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

install:
	sudo insmod hello.ko

remove:
	sudo rmmod hello

test: all install
	dmesg | tail

.PHONY: all clean install remove test

Building and Loading

# Build module
make

# Load module
sudo insmod hello.ko

# Check if loaded
lsmod | grep hello

# View kernel messages
dmesg | tail

# Remove module
sudo rmmod hello

# Clean build files
make clean

Expected output in dmesg:

[12345.678] Hello, Kernel!
[12350.789] Goodbye, Kernel!

Module Parameters

Modules can accept parameters at load time.

param_module.c:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>

MODULE_LICENSE("GPL");

static int count = 1;
static char *name = "World";

// Define parameters
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of times to print");

module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "Name to print");

static int __init param_init(void) {
    int i;
    for (i = 0; i < count; i++) {
        printk(KERN_INFO "Hello, %s! (%d/%d)\n", name, i + 1, count);
    }
    return 0;
}

static void __exit param_exit(void) {
    printk(KERN_INFO "Goodbye!\n");
}

module_init(param_init);
module_exit(param_exit);

Loading with parameters:

sudo insmod param_module.ko count=3 name="Linux"

# Check parameters
cat /sys/module/param_module/parameters/count
cat /sys/module/param_module/parameters/name

Character Device Driver

Creating a simple character device.

chardev.c:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");

#define DEVICE_NAME "chardev"
#define BUF_LEN 1024

static int major_number;
static char message[BUF_LEN] = {0};
static short message_len = 0;

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

// Device read
static ssize_t dev_read(struct file *filep, char __user *buffer,
                        size_t len, loff_t *offset) {
    int bytes_read = 0;

    if (*offset >= message_len)
        return 0;  // EOF

    if (*offset + len > message_len)
        len = message_len - *offset;

    if (copy_to_user(buffer, message + *offset, len))
        return -EFAULT;

    *offset += len;
    bytes_read = len;

    printk(KERN_INFO "chardev: Sent %d bytes\n", bytes_read);
    return bytes_read;
}

// Device write
static ssize_t dev_write(struct file *filep, const char __user *buffer,
                         size_t len, loff_t *offset) {
    if (len > BUF_LEN - 1)
        len = BUF_LEN - 1;

    if (copy_from_user(message, buffer, len))
        return -EFAULT;

    message_len = len;
    message[len] = '\0';

    printk(KERN_INFO "chardev: Received %zu bytes\n", len);
    return len;
}

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

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

static int __init chardev_init(void) {
    major_number = register_chrdev(0, DEVICE_NAME, &fops);

    if (major_number < 0) {
        printk(KERN_ALERT "chardev: Failed to register\n");
        return major_number;
    }

    printk(KERN_INFO "chardev: Registered with major number %d\n", major_number);
    printk(KERN_INFO "chardev: Create device with: mknod /dev/%s c %d 0\n",
           DEVICE_NAME, major_number);

    return 0;
}

static void __exit chardev_exit(void) {
    unregister_chrdev(major_number, DEVICE_NAME);
    printk(KERN_INFO "chardev: Unregistered\n");
}

module_init(chardev_init);
module_exit(chardev_exit);

Using the device:

# Load module
sudo insmod chardev.ko

# Check major number
dmesg | tail

# Create device node (replace 240 with actual major number)
sudo mknod /dev/chardev c 240 0
sudo chmod 666 /dev/chardev

# Write to device
echo "Hello from userspace" > /dev/chardev

# Read from device
cat /dev/chardev

# Remove device
sudo rm /dev/chardev
sudo rmmod chardev

Kernel Memory Allocation

kmalloc and kfree

#include <linux/slab.h>

static int __init mem_init(void) {
    char *buffer;

    // Allocate memory
    buffer = kmalloc(1024, GFP_KERNEL);
    if (!buffer) {
        printk(KERN_ERR "Failed to allocate memory\n");
        return -ENOMEM;
    }

    sprintf(buffer, "Hello from kernel memory");
    printk(KERN_INFO "%s\n", buffer);

    // Free memory
    kfree(buffer);

    return 0;
}

GFP flags:

  • GFP_KERNEL - Normal allocation (can sleep)
  • GFP_ATOMIC - Cannot sleep (for interrupt handlers)
  • GFP_DMA - DMA-capable memory

vmalloc

For large allocations (not physically contiguous):

#include <linux/vmalloc.h>

static int __init vmem_init(void) {
    char *buffer;

    buffer = vmalloc(1024 * 1024);  // 1 MB
    if (!buffer) {
        return -ENOMEM;
    }

    // Use buffer...

    vfree(buffer);
    return 0;
}

Kernel Linked Lists

Linux kernel provides doubly-linked list implementation.

#include <linux/list.h>
#include <linux/slab.h>

struct person {
    char name[64];
    int age;
    struct list_head list;
};

static LIST_HEAD(person_list);

static int __init list_init(void) {
    struct person *p;

    // Add entries
    p = kmalloc(sizeof(*p), GFP_KERNEL);
    strcpy(p->name, "Alice");
    p->age = 25;
    list_add(&p->list, &person_list);

    p = kmalloc(sizeof(*p), GFP_KERNEL);
    strcpy(p->name, "Bob");
    p->age = 30;
    list_add(&p->list, &person_list);

    // Iterate
    list_for_each_entry(p, &person_list, list) {
        printk(KERN_INFO "%s, age %d\n", p->name, p->age);
    }

    return 0;
}

static void __exit list_exit(void) {
    struct person *p, *tmp;

    // Free entries
    list_for_each_entry_safe(p, tmp, &person_list, list) {
        list_del(&p->list);
        kfree(p);
    }
}

Timers

Kernel timers for delayed execution.

#include <linux/timer.h>

static struct timer_list my_timer;
static int count = 0;

void timer_callback(struct timer_list *t) {
    printk(KERN_INFO "Timer fired! Count: %d\n", count++);

    // Re-arm timer (fire again in 1 second)
    mod_timer(&my_timer, jiffies + HZ);
}

static int __init timer_init(void) {
    timer_setup(&my_timer, timer_callback, 0);
    mod_timer(&my_timer, jiffies + HZ);  // Fire in 1 second

    printk(KERN_INFO "Timer module loaded\n");
    return 0;
}

static void __exit timer_exit(void) {
    del_timer(&my_timer);
    printk(KERN_INFO "Timer module unloaded\n");
}

Work Queues

For deferring work to kernel threads.

#include <linux/workqueue.h>

static struct workqueue_struct *my_wq;

struct work_data {
    struct work_struct work;
    int data;
};

static void work_handler(struct work_struct *work) {
    struct work_data *wd = container_of(work, struct work_data, work);
    printk(KERN_INFO "Work handler: data = %d\n", wd->data);
    kfree(wd);
}

static int __init wq_init(void) {
    struct work_data *wd;

    // Create workqueue
    my_wq = create_workqueue("my_queue");
    if (!my_wq) {
        return -ENOMEM;
    }

    // Queue work
    wd = kmalloc(sizeof(*wd), GFP_KERNEL);
    INIT_WORK(&wd->work, work_handler);
    wd->data = 42;
    queue_work(my_wq, &wd->work);

    return 0;
}

static void __exit wq_exit(void) {
    flush_workqueue(my_wq);
    destroy_workqueue(my_wq);
}

Interrupt Handlers

Handling hardware interrupts.

#include <linux/interrupt.h>

#define IRQ_NUM 1  // Example: keyboard IRQ

static irqreturn_t irq_handler(int irq, void *dev_id) {
    printk(KERN_INFO "Interrupt %d received\n", irq);
    return IRQ_HANDLED;
}

static int __init irq_init(void) {
    int result;

    result = request_irq(IRQ_NUM, irq_handler, IRQF_SHARED,
                         "my_irq_handler", (void *)irq_handler);
    if (result) {
        printk(KERN_ERR "Failed to register IRQ %d\n", IRQ_NUM);
        return result;
    }

    printk(KERN_INFO "IRQ handler registered\n");
    return 0;
}

static void __exit irq_exit(void) {
    free_irq(IRQ_NUM, (void *)irq_handler);
    printk(KERN_INFO "IRQ handler unregistered\n");
}

Proc File System

Creating entries in /proc for debugging.

#include <linux/proc_fs.h>
#include <linux/seq_file.h>

static int show_proc(struct seq_file *m, void *v) {
    seq_printf(m, "Hello from /proc/mymodule\n");
    seq_printf(m, "Jiffies: %lu\n", jiffies);
    return 0;
}

static int proc_open(struct inode *inode, struct file *file) {
    return single_open(file, show_proc, NULL);
}

static const struct proc_ops proc_fops = {
    .proc_open = proc_open,
    .proc_read = seq_read,
    .proc_lseek = seq_lseek,
    .proc_release = single_release,
};

static int __init proc_init(void) {
    proc_create("mymodule", 0, NULL, &proc_fops);
    printk(KERN_INFO "Created /proc/mymodule\n");
    return 0;
}

static void __exit proc_exit(void) {
    remove_proc_entry("mymodule", NULL);
    printk(KERN_INFO "Removed /proc/mymodule\n");
}

Reading the proc entry:

cat /proc/mymodule

Debugging Kernel Modules

Using printk

// Log levels (from highest to lowest priority)
printk(KERN_EMERG "Emergency!\n");     // System unusable
printk(KERN_ALERT "Alert!\n");         // Action must be taken
printk(KERN_CRIT "Critical!\n");       // Critical conditions
printk(KERN_ERR "Error\n");            // Error conditions
printk(KERN_WARNING "Warning\n");      // Warning conditions
printk(KERN_NOTICE "Notice\n");        // Normal but significant
printk(KERN_INFO "Info\n");            // Informational
printk(KERN_DEBUG "Debug\n");          // Debug messages

// View with dmesg
// dmesg -l err  (show only errors)
// dmesg -w      (follow mode)

Using pr_* macros

pr_info("Information message\n");
pr_warn("Warning message\n");
pr_err("Error message\n");
pr_debug("Debug message\n");  // Only with DEBUG defined

Debugging Tools

# View loaded modules
lsmod

# Module information
modinfo mymodule.ko

# Check dependencies
modprobe --show-depends mymodule

# Kernel log
dmesg
journalctl -k

# Kernel symbols
cat /proc/kallsyms | grep mymodule

Kernel Module Best Practices

Error Handling

static int __init mymodule_init(void) {
    int ret;

    // Allocate resources
    my_data = kmalloc(sizeof(*my_data), GFP_KERNEL);
    if (!my_data) {
        return -ENOMEM;
    }

    // Register device
    ret = register_chrdev(0, "mydev", &fops);
    if (ret < 0) {
        goto err_chrdev;
    }
    major = ret;

    // Create proc entry
    proc_entry = proc_create("myproc", 0, NULL, &proc_fops);
    if (!proc_entry) {
        ret = -ENOMEM;
        goto err_proc;
    }

    return 0;

err_proc:
    unregister_chrdev(major, "mydev");
err_chrdev:
    kfree(my_data);
    return ret;
}

Module Cleanup

static void __exit mymodule_exit(void) {
    // Cleanup in reverse order of initialization
    remove_proc_entry("myproc", NULL);
    unregister_chrdev(major, "mydev");
    kfree(my_data);

    printk(KERN_INFO "Module unloaded\n");
}

Advanced: Kernel Threading

#include <linux/kthread.h>

static struct task_struct *thread;

static int thread_fn(void *data) {
    while (!kthread_should_stop()) {
        printk(KERN_INFO "Thread running\n");
        ssleep(1);  // Sleep 1 second
    }
    return 0;
}

static int __init thread_init(void) {
    thread = kthread_create(thread_fn, NULL, "my_thread");
    if (IS_ERR(thread)) {
        return PTR_ERR(thread);
    }

    wake_up_process(thread);
    return 0;
}

static void __exit thread_exit(void) {
    if (thread) {
        kthread_stop(thread);
    }
}

Key Concepts

  • Kernel modules extend kernel functionality without reboot
  • module_init/module_exit define entry/exit points
  • printk is kernel's printf (output to kernel log)
  • Character devices provide file-like interface
  • kmalloc/kfree allocate kernel memory
  • Kernel lists provide doubly-linked list implementation
  • Timers schedule delayed work
  • Work queues defer work to kernel threads
  • /proc provides debugging interface

Common Mistakes

  1. Not checking return values - Always check for errors
  2. Memory leaks - Free all allocated memory in exit function
  3. Sleeping in atomic context - Don't use GFP_KERNEL in interrupts
  4. Not cleaning up - Unregister everything in reverse order
  5. Module refcounting - Don't unload while in use
  6. Concurrency issues - Use proper locking
  7. Testing on production - Always use VM for testing

Debugging Tips

  • Use printk liberally - Debug with kernel log
  • Check dmesg - Kernel messages explain crashes
  • Use KASAN - Kernel Address Sanitizer detects bugs
  • Enable debugging - Compile kernel with DEBUG options
  • Use /proc and /sys - Export debugging information
  • GDB with QEMU - Debug kernel modules in emulator
  • Check return values - Most kernel functions return errors

Mini Exercises

  1. Create "Hello World" kernel module
  2. Implement module with parameters
  3. Create character device driver
  4. Write /proc file that shows system information
  5. Implement kernel timer that prints every second
  6. Create linked list of kernel structures
  7. Implement simple kernel thread
  8. Add sysfs entries for module
  9. Create work queue example
  10. Implement simple block device driver

Review Questions

  1. What's the difference between printk and printf?
  2. How do you pass parameters to kernel modules?
  3. What are the file_operations callbacks?
  4. When should you use GFP_ATOMIC vs GFP_KERNEL?
  5. How do you handle errors during module initialization?

Reference Checklist

By the end of this chapter, you should be able to:

  • Create and load basic kernel modules
  • Use module parameters
  • Implement character device drivers
  • Allocate kernel memory (kmalloc, vmalloc)
  • Use kernel linked lists
  • Create kernel timers
  • Use work queues
  • Handle interrupts in kernel space
  • Create /proc entries
  • Debug kernel modules with printk and dmesg

Next Steps

With kernel module experience, the final chapter explores graphics programming, compiler internals, debugging techniques, and security concepts. You'll learn about the broader ecosystem of system programming tools and advanced topics.


Key Takeaway: Linux kernel modules provide a safe way to extend kernel functionality. Understanding module programming is essential for device driver development and kernel contributions. Always test in VMs and follow kernel coding standards.