Loading a kernel module is the primary way to extend the Linux kernel’s functionality without recompiling the entire kernel itself.

Here’s a simple character device driver, my_char_driver.c, that we’ll write and load:

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

#define DEVICE_NAME "mydevice"
#define BUF_LEN 80

static int major_number;
static char message[BUF_LEN];
static int open_count = 0;

static int device_open(struct inode *inode, struct file *file);
static int device_release(struct inode *inode, struct file *file);
static ssize_t device_read(struct file *file, char __user *buffer, size_t length, loff_t *offset);
static ssize_t device_write(struct file *file, const char __user *buffer, size_t length, loff_t *offset);

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write
};

static int __init my_char_driver_init(void) {
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0) {
        printk(KERN_ALERT "my_char_driver: failed to register a major number\n");
        return major_number;
    }
    printk(KERN_INFO "my_char_driver: registered correctly with major number %d\n", major_number);
    return 0;
}

static void __exit my_char_driver_exit(void) {
    unregister_chrdev(major_number, DEVICE_NAME);
    printk(KERN_INFO "my_char_driver: unregistered from the system.\n");
}

module_init(my_char_driver_init);
module_exit(my_char_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");

To compile this, you’ll need a Makefile:

obj-m += my_char_driver.o

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

all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

Run make in the directory containing these two files. This will produce my_char_driver.ko, your loadable kernel module.

Now, let’s load it. You’ll need root privileges:

sudo insmod my_char_driver.ko

You can verify it’s loaded with lsmod | grep my_char_driver. You should see:

my_char_driver 16384 0

The first column is the module name, and the second is its size in bytes. The 0 indicates it has no other modules depending on it.

The register_chrdev function in my_char_driver_init is the key. It tells the kernel, "Hey, I want to handle I/O for a character device named mydevice. Give me a major number." If 0 is passed as the major number, the kernel assigns a dynamic one. This assignment is crucial because user-space programs use the major and minor numbers to identify which device driver should handle their I/O requests. When you later create a device node (e.g., /dev/mydevice), you’ll associate it with this major number, allowing the system to route file operations to your module.

After loading, you’ll likely want to create a device node so user-space programs can interact with it. You’ll need the major number assigned. You can find it in /proc/devices:

cat /proc/devices | grep mydevice

This might output something like:

123 mydevice (where 123 is the major number).

Then, create the node using mknod:

sudo mknod /dev/mydevice c 123 0

Here, c signifies a character device, 123 is the major number, and 0 is the minor number.

Now, you can write to and read from /dev/mydevice. For example, from another terminal:

echo "Hello from user space" > /dev/mydevice

And to read:

cat /dev/mydevice

You’ll see "Hello from user space" printed. The device_write and device_read functions in your module handle these operations. copy_to_user and copy_from_user are essential for safely moving data between user space and kernel space, preventing security vulnerabilities and crashes.

To unload the module:

sudo rmmod my_char_driver

The unregister_chrdev call in my_char_driver_exit reverses the registration, making the major number available again.

The most surprising thing about kernel modules is how simple the interface is, yet how much power and risk it entails; you’re literally adding code that runs with the same privileges as the operating system’s core.

If you try to read from /dev/mydevice after writing to it without explicitly clearing the internal buffer, you’ll get an empty read because the current implementation doesn’t handle multiple reads or seek operations gracefully.

Want structured learning?

Take the full Linux & Systems Programming course →