The LKM Framework-Free Linux kernel development course

 

Previous Lecture
Next Lecture

The LKM Framework
Chapter 4 · LKMs Part 1 · Lesson 2 of 3
Kernel Modules, Lifecycle, printk, and Your First Hello World
🎯 Beginner + Practical
⏱ 25 min read
🐧 Kernel 6.x
💻 Code Included

📌 Keywords — Rank for Free Linux Kernel Course
loadable kernel module LKM
module_init module_exit
printk kernel logging dmesg
insmod rmmod modprobe
hello world kernel module
free linux device drivers course
free embedded systems course
kernel module Makefile

You now know that the Linux kernel is monolithic but modular. In this lesson, we focus on exactly how that modularity works. We will look at what a Loadable Kernel Module is, why it was designed the way it was, how the kernel loads and unloads it, how you print messages from inside kernel code, and what the simplest possible kernel module looks like.

By the end of this lesson, you will have a clear mental model of the entire LKM lifecycle and a reference template that you can reuse to build any kernel module on Linux kernel 6.x.

1. What Is a Loadable Kernel Module?

A Loadable Kernel Module — usually just called an LKM or simply a “kernel module” — is a piece of compiled kernel code stored as a .ko file (kernel object). It can be inserted into a running kernel and removed from it without rebooting the machine.

Before LKMs existed, if you wanted to add support for a new piece of hardware, you had to recompile the entire kernel and reboot. This was impractical for both users and developers. LKMs solve this completely — you compile just the module, load it with one command, and your new driver is live.

Common use cases for kernel modules include:

🖥️ Device Drivers

USB drivers, graphics drivers, sound card drivers, network interface card (NIC) drivers — most of these ship as kernel modules.

📁 Filesystems

Support for filesystems like exFAT, NTFS, or SquashFS can be loaded on demand as modules.

🔒 Security Modules

SELinux and AppArmor hook into the kernel via the Linux Security Module (LSM) framework — itself a kind of module system.

🌐 Network Protocol Extensions

netfilter modules power iptables and nftables. eBPF programs can be loaded as modules to instrument the network stack at runtime.

2. The LKM Lifecycle

Every kernel module goes through a clear lifecycle. Understanding this lifecycle is the foundation of writing correct modules.

LKM Lifecycle — From .ko File to Running Kernel
1. Compile
make → produces mymodule.ko
2. Load
sudo insmod mymodule.ko or sudo modprobe mymodule
3. Kernel calls your init function
module_init(my_init)my_init() runs
4. Module runs
Module is active — responds to system events, hardware interrupts, etc.
5. Unload
sudo rmmod mymodule
6. Kernel calls your exit function
module_exit(my_exit)my_exit() runs, cleanup happens
7. Module removed from kernel memory
📝 insmod vs modprobe
insmod loads a .ko file directly by its path and does not automatically load dependencies. modprobe is smarter — it reads the module’s dependency information from /lib/modules/$(uname -r)/modules.dep and loads all required modules first. For production use, prefer modprobe. For development and learning, insmod is fine.

3. Your First Kernel Module — Hello World

Every programmer starts with Hello World. In the kernel world, this is especially meaningful because printing “hello” from inside the kernel means your code is running at Ring 0 — the most privileged level on the machine. Here is what the simplest possible kernel module looks like on kernel 6.x:

3.1 The Source File: hello.c

/*
 * hello.c - A minimal Linux kernel module for kernel 6.x
 *
 * This module prints a message when loaded and another
 * when removed. Nothing more, nothing less.
 *
 * Build: make (using the Makefile in the next section)
 * Load:  sudo insmod hello.ko
 * Check: dmesg | tail -5
 * Remove: sudo rmmod hello
 */

#include <linux/module.h>   /* Needed by all kernel modules        */
#include <linux/kernel.h>   /* Needed for KERN_INFO and printk()   */
#include <linux/init.h>     /* Needed for __init and __exit macros */

/* This function runs when the module is loaded */
static int __init hello_init(void)
{
    printk(KERN_INFO "hello: module loaded — welcome to kernel space!\n");
    return 0;   /* 0 = success. Returning non-zero aborts loading. */
}

/* This function runs when the module is removed */
static void __exit hello_exit(void)
{
    printk(KERN_INFO "hello: module removed — goodbye from kernel space!\n");
}

/* Tell the kernel which functions to call on load and unload */
module_init(hello_init);
module_exit(hello_exit);

/* Module metadata — required by the kernel */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A minimal Hello World kernel module");
MODULE_VERSION("1.0");

Let us walk through every single line so there are no mysteries.

3.2 Line-by-Line Explanation

Anatomy of a Kernel Module
Headers
#include <linux/module.h> — required for every module
#include <linux/kernel.h> — gives you KERN_INFO, printk
#include <linux/init.h> — gives you __init and __exit macros
Init Function
Marked static int __init. Called once at load time. If it returns 0, module loads successfully. If it returns a non-zero error code (e.g. -ENOMEM), loading fails immediately.
Exit Function
Marked static void __exit. Called once when the module is removed. Must clean up everything the init function set up — free memory, unregister devices, release locks. Cannot return an error.
Macros
module_init(hello_init) — registers your init function with the kernel.
module_exit(hello_exit) — registers your exit function with the kernel.
Metadata
MODULE_LICENSE("GPL") — must be GPL or the kernel will warn “tainted kernel”. Required.
MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_VERSION — optional but good practice.

3.3 The __init and __exit Macros

These are not just decorative. __init tells the kernel linker to place the function in a special section of the binary called .init.text. After the module has finished loading and the init function has returned, the kernel frees the memory occupied by everything marked __init. This saves RAM at runtime.

Similarly, __exit places the function in a section that is only included when the module can be unloaded. If you build a module permanently into the kernel (not as a loadable module), the exit function is never called and the compiler can completely discard it.

4. Kernel Logging with printk()

Inside kernel code, printk() is the primary way to produce output. Unlike printf(), it does not go to your terminal. It writes to the kernel ring buffer — a circular buffer in kernel memory that stores recent log messages. You can read those messages with the dmesg command.

4.1 Log Levels

Every printk() call includes a log level prefix that controls how urgent the message is and whether it appears on the console:

printk Log Levels — Most Urgent to Least Urgent
KERN_EMERG
Level 0
System is unusable — imminent crash
KERN_ALERT
Level 1
Immediate action required
KERN_CRIT
Level 2
Critical condition
KERN_ERR
Level 3
An error occurred
KERN_WARNING
Level 4
Warning — something might be wrong
KERN_NOTICE
Level 5
Normal but significant event
KERN_INFO
Level 6
Informational — use this for normal module messages
KERN_DEBUG
Level 7
Debug messages — only shown if dynamic debug is on

In practice, use KERN_INFO for normal module messages and KERN_ERR when something goes wrong. Starting from kernel 4.x, there are convenient wrapper macros that combine the level and the call:

/* Modern preferred style in kernel 4.x and later */
pr_info("hello: module loaded successfully\n");
pr_err("hello: failed to allocate memory, error %d\n", ret);
pr_warn("hello: unexpected state encountered\n");
pr_debug("hello: debug checkpoint reached\n");

These pr_* macros are shorthand for printk(KERN_INFO ...) etc. and are the recommended style in modern kernel code (including all of kernel 6.x). They also automatically prepend your module name if you define pr_fmt.

4.2 Reading printk Output

# After loading your module, read the kernel log:
dmesg | tail -20

# With timestamps (kernel 6.x dmesg supports -T flag):
dmesg -T | tail -20

# Watch live kernel messages as they arrive:
dmesg --follow

# On systems using systemd-journald, you can also check:
sudo journalctl -k --since "5 minutes ago"

5. The Kernel Module Makefile

A kernel module cannot be built with a plain gcc hello.c -o hello. It must be built using the kernel’s own build system (called kbuild), which knows the exact compiler flags, include paths, and linker options needed to produce a valid .ko file. Here is the standard two-part Makefile for any kernel module:

5.1 The Makefile

# Makefile for hello.ko kernel module
#
# Usage:
#   make          - build the module
#   make clean    - remove build artifacts

# Name of the kernel object to build (without .ko)
obj-m += hello.o

# Path to the currently running kernel's build directory.
# The kernel headers installed on your system live here.
KDIR := /lib/modules/$(shell uname -r)/build

# Default target: invoke the kernel's kbuild system
all:
	make -C $(KDIR) M=$(PWD) modules

# Clean target: remove all generated files
clean:
	make -C $(KDIR) M=$(PWD) clean
💡 How This Makefile Works
The make -C $(KDIR) command tells make to change into the kernel build directory and run the kernel’s own top-level Makefile. The M=$(PWD) argument tells the kernel’s build system to look back into your current directory for the module source. The kernel build system then finds your obj-m variable, compiles hello.c, and produces hello.ko.

5.2 What You Need Installed

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

# On Fedora / RHEL:
sudo dnf install kernel-devel kernel-headers gcc make

# On Arch Linux:
sudo pacman -S linux-headers base-devel

5.3 Complete Build and Test Workflow

# 1. Create your source directory
mkdir ~/my_first_module
cd ~/my_first_module

# 2. Create hello.c and Makefile (contents shown above)

# 3. Build the module
make

# You should see output ending with:
# LD [M]  /home/user/my_first_module/hello.ko

# 4. Check the module info before loading
modinfo hello.ko

# 5. Load the module
sudo insmod hello.ko

# 6. Check the kernel log for your printk output
dmesg | tail -5

# 7. Verify the module is loaded
lsmod | grep hello

# 8. Unload the module
sudo rmmod hello

# 9. Check the log again — exit message should appear
dmesg | tail -5

6. Module Information and Common Commands

The kernel tracks all loaded modules and their details. Here is a reference of the most commonly used commands when working with kernel modules:

Essential Kernel Module Commands — Quick Reference
lsmod
List all currently loaded kernel modules, their size, and use count
modinfo hello.ko
Show module metadata: license, author, description, version, parameters
sudo insmod hello.ko
Load the module from the specified .ko file path
sudo modprobe hello
Load module by name (auto-resolves dependencies from /lib/modules/)
sudo rmmod hello
Remove (unload) the module — calls exit function first
sudo modprobe -r hello
Remove module and auto-remove unused dependencies
dmesg | tail -20
Read recent kernel log messages including printk output
cat /proc/modules
Same info as lsmod but in raw text format — also shows module state
ls /sys/module/hello/
The kernel creates a sysfs directory per module with runtime info

7. GPL License and the Tainted Kernel

You might wonder why MODULE_LICENSE("GPL") is so important. The Linux kernel is licensed under the GNU General Public License version 2. When you load a module, the kernel checks its license. If the module declares a non-GPL-compatible license — or no license at all — the kernel marks itself as “tainted”.

A tainted kernel is not broken, but it means the kernel developers may refuse to help you debug issues. The taint flag is shown in kernel oops and panic messages. As a developer, you should always use MODULE_LICENSE("GPL") for educational and open-source modules. For commercial modules, "GPL v2" and "Dual MIT/GPL" are also common.

/* Valid MODULE_LICENSE values */
MODULE_LICENSE("GPL");          /* Most common */
MODULE_LICENSE("GPL v2");
MODULE_LICENSE("GPL and additional rights");
MODULE_LICENSE("Dual MIT/GPL");
MODULE_LICENSE("Dual BSD/GPL");
⚠️ Kernel 6.x: Non-GPL Symbols
The kernel exports some functions specifically for GPL modules only, marked with EXPORT_SYMBOL_GPL(). A non-GPL module cannot call these functions. Attempting to do so will cause the module to fail to load with a symbol not found error. More and more internal kernel APIs are being moved to GPL-only in recent kernel versions.

🎯 Interview Questions & Answers

Q1. What is an LKM and how is it different from a statically compiled kernel feature?
An LKM (Loadable Kernel Module) is a compiled kernel code file (.ko) that can be inserted into or removed from a running kernel without rebooting. A statically compiled feature is built directly into the kernel binary (vmlinuz) — it is always present and cannot be removed at runtime. LKMs offer flexibility and reduce base kernel size; static compilation offers slightly faster function calls since there is no module loading overhead.
Q2. What is the purpose of module_init() and module_exit()?
module_init(fn) registers your function fn as the entry point that the kernel calls when the module is loaded. module_exit(fn) registers the function that runs when the module is removed. These macros expand to code that places your function pointers in special ELF sections of the .ko binary that the kernel’s module loader reads.
Q3. What does the return value of the init function mean?
Returning 0 from the init function tells the kernel that the module loaded successfully. Returning a negative error code (like -ENOMEM for out of memory, or -ENODEV for device not found) tells the kernel the module failed to initialise. The kernel aborts loading, calls no exit function, and insmod reports an error to the user.
Q4. Why can’t the exit function return an error?
Once the kernel has decided to unload a module, there is no safe way to cancel that operation. If the exit function could fail, it might leave the kernel in an inconsistent state — for example, half-cleaned-up data structures that other kernel code might still try to access. Therefore the exit function is declared void and must always complete its cleanup unconditionally.
Q5. What is printk() and how does it differ from printf()?
printk() is the kernel’s logging function. It writes to the kernel ring buffer — a circular buffer in kernel memory. Unlike printf(), it does not write to stdout and it takes a log-level prefix (e.g. KERN_INFO) that determines message severity. Output can be read with dmesg or via /proc/kmsg. printf() is a user-space C library function that does not exist in kernel context.
Q6. What does MODULE_LICENSE(“GPL”) do? What happens if you omit it?
MODULE_LICENSE embeds the license string in the .ko binary. The kernel reads this on load. If the license is not GPL-compatible, the kernel marks itself as “tainted” — a flag that persists until reboot and indicates the kernel may be running non-open-source code. Additionally, GPL-only exported kernel symbols become inaccessible to non-GPL modules. Omitting the macro entirely is equivalent to declaring a proprietary license.
Q7. What is the difference between insmod and modprobe?
insmod takes an exact path to a .ko file and loads it directly without resolving dependencies. modprobe takes a module name, looks up the module and its dependencies in /lib/modules/$(uname -r)/modules.dep, and loads all required modules in the correct order. modprobe -r also removes a module and any no-longer-needed dependencies.
Q8. What is the use count of a kernel module (seen in lsmod)?
The use count (third column in lsmod) shows how many other modules or kernel components are currently using the module. If the use count is non-zero, rmmod will refuse to unload the module to prevent removing code that something else depends on. For example, a USB storage driver’s use count will be greater than zero while a USB drive is mounted.

📚 EmbeddedPathashala — Free Linux Kernel Development Course

Great job! You now understand the LKM framework and can write a basic kernel module. In the next lesson, we cover kernel build troubleshooting — the common errors you will hit when building the kernel and how to fix them fast.

Previous Lecture
Next Lecture

Leave a Reply

Your email address will not be published. Required fields are marked *