Linux Kernel Module – Part 3
module_exit()
__init macro
__exit macro
static qualifier
0/-E convention
errno
insmod failure
In the last tutorial we saw what module macros are. Now let us look at the real heart of a kernel module — the init function and exit function. Every kernel module must have these two functions. They are the entry and exit points of your module’s life cycle.
We will also learn an important kernel convention — the 0/-E return rule — which you will use throughout your kernel development career.
When you write a normal C program, execution always starts from main(). That is the rule in user-space applications.
Kernel modules are different. They are not applications. They run directly inside the kernel — in privileged mode, with full access to hardware and kernel data structures. Because of this, the concept of main() simply does not apply here.
Instead, a kernel module has two specific functions:
| insmod / modprobe | → | init function runs | Module is now active in kernel |
| ↕ Module does its job (handles devices, intercepts calls, etc.) | |||
| rmmod | → | exit function runs | Module is unloaded from kernel |
You register these two functions using the module_init() and module_exit() macros. You pass the name of your function to each macro:
module_init(my_init_function);
module_exit(my_exit_function);
Think of these like a constructor and destructor pair from C++. When your module is loaded (insmod), the init function runs. When it is removed (rmmod), the exit function runs.
The kernel expects a very specific signature for both functions. Here is the standard form:
static int __init modulename_init(void);
static void __exit modulename_exit(void);
Let us break down each keyword:
| Keyword | What it means |
|---|---|
static |
Makes the function private to this module. It will not be visible or accessible from other modules. This is good practice — these functions are meant only for this module’s own lifecycle. |
__init |
Tells the linker to place this function in a special memory section called .init.text. After the module is fully loaded and the init function has run, the kernel can free that section of memory. Smart memory optimization. |
__exit |
Similar idea — places exit function in .exit.text. If the module is compiled directly into the kernel (built-in, not loadable), this function is completely dropped at link time because built-in code can never be removed. |
int vs void |
Init returns int (success=0, failure=negative errno). Exit returns void — it cannot fail, it must always complete cleanup. |
(void) |
Neither function takes any parameters. The kernel calls them with no arguments. |
Let us understand __init more clearly with a diagram. Kernel memory is divided into sections. The __init macro places code in a special section that is freed after use:
| Kernel/Module Memory Layout | ||
| .text Normal code that stays forever |
→ | module_register_etc() Stays loaded |
.init.text__init functions go here |
→ freed after init! | my_init() Freed once module loads |
.exit.text__exit functions go here |
→ dropped if built-in | my_exit() Dropped for built-in builds |
This is a memory-saving technique. The init function only runs once when the module loads. After that, there is no reason to keep that code in memory. The kernel is clever enough to discard it. Same with exit — if the driver is compiled into the kernel image and can never be removed, the exit function is dead code and the linker drops it entirely.
For a loadable module, the effect is smaller but still real. Every byte matters at scale in embedded systems.
The kernel community follows a consistent naming style for these functions:
static int __init mydriver_init(void);
static void __exit mydriver_exit(void);
The pattern is: <modulename>_init and <modulename>_exit.
This is just a convention — technically you can name these functions anything you want since you pass the function pointer to module_init() and module_exit(). But following the convention makes the code easy to read and maintain, especially in a large team or in the upstream kernel.
Examples from real kernel drivers:
/* From a USB driver */
module_init(usb_storage_driver_init);
module_exit(usb_storage_driver_exit);
/* From a network driver */
module_init(e1000_init_module);
module_exit(e1000_exit_module);
This is one of the most important conventions in Linux kernel programming. The init function returns an int. What value should it return?
The Linux kernel follows a very clear rule called the 0/-E convention:
Return 0 |
✅ Success — module is loaded |
Return -ERRNO |
❌ Failure — module load is aborted |
The negative errno value is very specific. You return the negative of the error code you want user-space to see. Here are common ones:
return -ENOMEM; /* memory allocation failed */
return -ENODEV; /* device not found */
return -EINVAL; /* invalid parameter */
return -EPERM; /* permission denied */
return -EIO; /* input/output error */
These error codes are defined in the kernel source under:
include/uapi/asm-generic/errno-base.h
include/uapi/asm-generic/errno.h
When your init function returns a non-zero negative value, a chain of events happens involving the kernel and glibc. Let us trace this flow:
| Step | Who | What happens |
| 1 | Your init function | Returns -ENOMEM to the kernel |
| 2 | Linux Kernel | Aborts module loading. The finit_module() syscall returns -1 to user-space |
| 3 | glibc glue code | Multiplies the kernel’s error by -1, sets the global variable errno = ENOMEM |
| 4 | insmod tool | Reads errno, prints an error message like insmod: ERROR: could not insert module hello.ko: Cannot allocate memory |
Notice something important: kernel space returns -ENOMEM (negative), but user-space errno is set to ENOMEM (positive). The glibc layer handles that sign flip. You do not need to worry about this in your kernel code — just return the negative value.
Here is a practical example in code:
static int __init demo_init(void)
{
void *ptr;
/* Try to allocate 512 bytes of kernel memory */
ptr = kmalloc(512, GFP_KERNEL);
if (!ptr) {
pr_err("demo_mod: memory allocation failed\n");
return -ENOMEM; /* tell caller: out of memory */
}
pr_info("demo_mod: loaded successfully\n");
return 0; /* success */
}
static void __exit demo_exit(void)
{
/* cleanup goes here */
pr_info("demo_mod: unloaded\n");
/* exit returns void — no return value needed */
}
In user-space C programming, errno is a global variable that stores the error code of the last failed system call. It lives inside the user process’s memory — specifically in the uninitialized data segment (BSS segment).
When a Linux system call fails, it returns -1 and sets errno to a positive error code. Functions like perror() and strerror() translate this error code into a human-readable message by looking it up in an internal error message table.
| Location | Value | Type |
| Kernel code | -ENOMEM (negative) |
Returned to kernel framework |
| glibc | flips sign → ENOMEM |
Sets user-space errno |
| User-space program | errno == ENOMEM (positive) |
Read by perror() / strerror() |
This design keeps things clean: kernel returns negative codes to indicate error type, user-space reads positive codes via errno. As a kernel developer you always work with the negative form.
Here is a complete, correct kernel module that uses everything we have learned across these tutorials — macros, init, exit, and the proper return value:
// SPDX-License-Identifier: GPL-2.0
/*
* hello_lkm.c - Minimal Hello World Kernel Module
* Tested on Linux Kernel 6.6 / 6.8 / 6.12 LTS
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
MODULE_AUTHOR("Your Name <your@email.com>");
MODULE_DESCRIPTION("Minimal Hello World LKM - EmbeddedPathashala");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.0");
/*
* hello_lkm_init - called when module is loaded via insmod/modprobe
* Returns: 0 on success, negative errno on failure
*/
static int __init hello_lkm_init(void)
{
pr_info("hello_lkm: module loaded! Hello from kernel space.\n");
return 0;
}
/*
* hello_lkm_exit - called when module is removed via rmmod
*/
static void __exit hello_lkm_exit(void)
{
pr_info("hello_lkm: module removed. Goodbye!\n");
}
module_init(hello_lkm_init);
module_exit(hello_lkm_exit);
And the Makefile to build it:
obj-m += hello_lkm.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Build and test:
# Build
make
# Load the module
sudo insmod hello_lkm.ko
# Check kernel log
dmesg | tail -5
# Remove the module
sudo rmmod hello_lkm
# Verify log again
dmesg | tail -5
Answer: main() is a concept from user-space C programming — it is the entry point the C runtime calls when the OS launches a process. Kernel modules are not processes; they are pieces of code that get loaded directly into the running kernel’s address space. The kernel itself calls the init function at load time using the pointer registered with module_init(). There is no process, no C runtime, and no main().
Answer: __init is a compiler hint that places the annotated function into a special ELF section called .init.text. After the init function finishes running, the kernel can reclaim (free) that section of memory because the function will never be called again. This saves physical RAM — important in embedded and resource-constrained systems. For built-in drivers (compiled into the kernel image rather than as a loadable module), the memory savings are even more significant.
Answer: The kernel aborts the module loading process. The module is NOT inserted into the kernel. The finit_module() or init_module() system call returns -1 to user-space, glibc sets errno to the positive version of the error code, and insmod prints an error message. Importantly, the exit function is NOT called in this case — so the init function itself must clean up any resources it partially allocated before returning the error.
Answer: The exit function returns void because module removal is expected to always succeed. Once rmmod is called and the kernel decides to unload the module (after checking the reference count is zero), the exit function MUST complete all cleanup. There is no mechanism to “cancel” the unload by returning an error. The kernel design philosophy here is: if you allowed the exit to fail, you could end up with a partially-removed module and leaked resources in the kernel.
Answer: The kernel follows a clear convention for init function return values: return 0 on success, return a negative errno value on failure. For example, return -ENOMEM for memory allocation failure, return -ENODEV if a device is not found. The negative sign is important — the kernel and glibc work together to convert this into a positive errno value in user-space. The user-space tool (insmod) then reads errno and prints a meaningful error message. This convention is used throughout the kernel, not just in modules.
Answer: Marking both functions static limits their scope to the current translation unit (the .c file). This means these functions cannot be called or referenced from other modules or other kernel code. Since the init and exit functions are only meant to be called by the kernel’s module framework via the registered function pointers, there is no reason to make them globally visible. Using static is good practice — it avoids naming conflicts across the kernel and follows the principle of minimum visibility.
