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
- Use a VM - Never test kernel modules on your main system
- Start simple - Begin with "Hello World" module
- Read kernel code - Study existing drivers in
drivers/ - Check dmesg - Kernel messages go to kernel log
- 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
- Not checking return values - Always check for errors
- Memory leaks - Free all allocated memory in exit function
- Sleeping in atomic context - Don't use GFP_KERNEL in interrupts
- Not cleaning up - Unregister everything in reverse order
- Module refcounting - Don't unload while in use
- Concurrency issues - Use proper locking
- 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
- Create "Hello World" kernel module
- Implement module with parameters
- Create character device driver
- Write /proc file that shows system information
- Implement kernel timer that prints every second
- Create linked list of kernel structures
- Implement simple kernel thread
- Add sysfs entries for module
- Create work queue example
- Implement simple block device driver
Review Questions
- What's the difference between printk and printf?
- How do you pass parameters to kernel modules?
- What are the file_operations callbacks?
- When should you use GFP_ATOMIC vs GFP_KERNEL?
- 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.