The first time I wrote a kernel module, I stared at two tiny functions — one marked __init, the other __exit — and assumed they were just formalities, like a main() function nobody really thinks about. It took a production bug involving a module that wouldn’t unload cleanly for me to appreciate just how much responsibility those two functions actually carry. They’re not boilerplate. They’re the entire contract between your module and the kernel’s lifecycle management system.
This article digs into why entry and exit points matter so much, what happens under the hood when they run, and the mistakes that turn a harmless module into a source of kernel panics or memory leaks.
What the Entry and Exit Points Are
Every loadable kernel module (LKM) in Linux defines, at minimum, two functions:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_module_init(void)
{
printk(KERN_INFO "my_module: initialized\n");
return 0;
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "my_module: cleaned up\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
module_init() registers the entry point — the function that runs the moment the module finishes loading. module_exit() registers the exit point — the function that runs right before the module is removed from the kernel.
Unlike a user-space program, a kernel module has no main() that runs top-to-bottom and then exits. Once my_module_init() returns, the module doesn’t “run” continuously — it just sits resident in kernel memory, waiting to be invoked through whatever hooks, device files, or callbacks it registered during initialization. The entry point’s job is entirely about setup; the exit point’s job is entirely about teardown.
Why the Entry Point Matters So Much
It’s the only chance to register everything the module will ever do
Whatever the module wants to offer the rest of the kernel — a character device, a network protocol handler, a filesystem, a sysfs attribute, a timer — has to be registered inside (or as a direct consequence of) the entry point. If you forget to register a callback here, the module is loaded but functionally inert.
Its return value determines whether the module loads at all
This is the detail that trips people up the most. If __init returns a negative error code, the kernel treats the entire module load as having failed — even though some initialization code might have already run. The module is then immediately unloaded, but note: the exit function is not called in this case, because the module was never considered “successfully loaded.” This means any resources allocated before the failure point must be manually unwound inside the init function itself, usually with a chain of goto labels:
static int __init my_init(void)
{
int ret;
ret = register_chrdev(MAJOR_NUM, "mydev", &fops);
if (ret < 0)
goto fail_chrdev;
ret = create_proc_entry();
if (ret < 0)
goto fail_proc;
return 0;
fail_proc:
unregister_chrdev(MAJOR_NUM, "mydev");
fail_chrdev:
return ret;
}
This pattern — cascading goto cleanup — exists specifically because the entry point is an all-or-nothing gate.
It runs in a very particular context
The __init function runs in process context (usually the context of insmod/modprobe/kmod calling into init_module()), with interrupts enabled, and generally without any locks held. That gives it freedom to sleep, allocate memory with GFP_KERNEL, and call blocking functions — freedom the module might not have later on if some of its later code runs in interrupt context.
Why the Exit Point Matters So Much
It’s the kernel’s only guarantee of cleanup
The kernel does not automatically know what a module has allocated. It doesn’t track every kmalloc(), every registered device number, every timer, every workqueue, every proc entry your module created. The __exit function is the single place responsible for undoing everything the __init function (and any code running afterward) set up. If you skip freeing something here, it’s a leak that lasts until reboot — or worse, a dangling pointer that some other part of the kernel might still try to use after your module is gone.
Ordering matters critically
Cleanup has to happen in the reverse order of setup, mirroring how you’d unwind a stack. Unregistering a device before disabling its interrupt handler, for example, could allow an interrupt to fire into code that no longer exists in memory, causing an immediate kernel panic.
static void __exit my_exit(void)
{
free_irq(IRQ_NUM, NULL);
del_timer_sync(&my_timer);
remove_proc_entry("mydev", NULL);
unregister_chrdev(MAJOR_NUM, "mydev");
printk(KERN_INFO "my_module: unloaded cleanly\n");
}
It can be prevented from running by reference counts
The kernel tracks how many things are actively using a module via its reference count (visible in lsmod as the “Used by” column). If the count isn’t zero, rmmod will refuse to unload the module (or block, depending on flags), and the exit function simply won’t run until it’s safe. This is why forgetting try_module_get()/module_put() pairs in code paths that use a module’s resources can either cause premature unloads or modules that can never be removed.
What Happens Under the Hood
When you run insmod mymodule.ko, roughly this sequence happens:
- The
init_module()(or newerfinit_module()) syscall is invoked. - The kernel allocates memory for the module, copies in its code and data sections, and relocates/resolves symbols against the running kernel’s exported symbol table.
- Any
__init-section code that isn’t the entry point itself (module parameter parsing, etc.) executes. - The registered
module_initfunction is called. - If it returns 0, the module is marked live; if it returns a negative value, the kernel unwinds the load and frees the module’s memory — again, without calling
__exit. - Critically, memory marked with the
__initattribute (including the init function’s own code, if the kernel is built withCONFIG_MODULE_UNLOADoptimizations for freeing init memory) can be discarded after initialization completes, freeing up a small amount of memory. This is the same mechanism the kernel itself uses to reclaim boot-time-only code (__initon the whole kernel is whydmesgshows “Freeing unused kernel memory” at boot).
On rmmod mymodule, the reverse happens: the kernel checks the reference count, and if it’s safe, calls the module_exit function, then frees the module’s memory entirely.
A Simple Diagram
insmod/modprobe
|
v
init_module() syscall
|
v
+----------------+
| __init func |---- returns 0 ----> module LIVE, waiting for
| (entry point) | callbacks/registered hooks
+----------------+
|
returns < 0
|
v
module unloaded immediately
(exit function NEVER called)
... later ...
rmmod mymodule
|
v
refcount == 0 ?
|
yes
v
+----------------+
| __exit func |
| (exit point) |
+----------------+
|
v
module memory freed
Comparisons Across Operating Systems
- Windows drivers have a conceptually identical pair:
DriverEntry()as the entry point and anUnloadroutine registered viaDriverObject->DriverUnload. Just like Linux, ifDriverEntryfails, Windows won’t call the unload routine for work that never completed. - macOS/iOS kernel extensions (kexts) use
start()andstop()routines (or, in the C++/IOKit model, constructor/destructor-likeinit()/free()andstart()/stop()methods on anIOServicesubclass). - Android, running a Linux kernel, uses exactly the same
module_init/module_exitmechanism as any other Linux system, though many production Android kernels disable module loading entirely in favor of statically built-in drivers for security and verified-boot reasons.
Practical Example: A Faulty Exit Point
Here’s a bug pattern I’ve actually seen cost someone a very confusing afternoon of debugging:
static struct timer_list my_timer;
static void __exit my_exit(void)
{
unregister_chrdev(MAJOR_NUM, "mydev");
// forgot: del_timer_sync(&my_timer);
}
Because the timer was never cancelled, it kept firing after the module’s memory was freed — calling into code that no longer existed. The result was a kernel oops with a garbage instruction pointer, minutes after the module appeared to unload “successfully.” The lesson: the exit point must account for everything asynchronous the module set in motion, not just the obviously visible resources.
Troubleshooting Tips
- If
rmmodhangs or fails with “Device or resource busy,” checklsmodfor a non-zero usage count — something still holds a reference. - If you see a kernel oops shortly after unloading a module, suspect an asynchronous callback (timer, workqueue, interrupt handler, notifier) that wasn’t properly cancelled in the exit function.
- Use
dmesgimmediately afterinsmod/rmmod— most__init/__exitbugs announce themselves there. - Build with
CONFIG_DEBUG_KMEMLEAKenabled in a development kernel to catch memory allocated in__initbut never freed in__exit.
Best Practices
- Always unwind partially completed initialization with
goto-based cleanup chains. - Mirror your
__exitfunction as the exact reverse of your__initfunction’s steps. - Use
del_timer_sync(),cancel_work_sync(), and similar synchronous cancellation functions in exit paths rather than their non-blocking variants, so you know the asynchronous work is truly finished before continuing teardown. - Never assume a return value of 0 from a subsystem registration call means “and it will never call back into now-freed memory” — always explicitly unregister.
- Keep
__initand__exitfunctions focused purely on setup/teardown; don’t put ongoing logic in them.
Deferred and Asynchronous Work: The Exit Point’s Hardest Problem
The trickiest exit-point bugs I’ve encountered all trace back to the same root cause: something the init function set in motion doesn’t actually stop the moment the exit function calls the function that’s supposed to cancel it. The kernel offers several deferred-work mechanisms, and each has its own correct cancellation pattern that the exit point has to use precisely:
- Timers (
struct timer_list) must be cancelled withdel_timer_sync(), not the plaindel_timer(), if there’s any chance the timer’s callback could currently be executing on another CPU.del_timer()only removes the timer from the pending list; it doesn’t wait for an in-flight callback to finish, which means the exit function could return — and the module’s memory could be freed — while the timer callback is still mid-execution on another core. - Workqueues need
cancel_work_sync()orflush_workqueue()for the same reason: a queued or currently-running work item references code inside the module, and the exit function has to guarantee that work has fully completed (not just been requested to stop) before allowing the module to be unloaded. - Tasklets (an older, softirq-based deferred mechanism, largely being phased out in favor of workqueues in modern kernels) require
tasklet_kill(), with similar synchronous-completion guarantees. - Kthreads spawned by the module need to be signalled to exit (commonly via a flag checked in a loop, combined with
kthread_should_stop()) and then joined withkthread_stop(), which blocks until the thread function actually returns.
The unifying principle across every one of these: the exit function must not return until every piece of asynchronous activity the module ever started has definitively, synchronously finished. Anything less leaves a window where kernel code can execute against memory that either has already been freed, or is about to be freed the instant the exit function returns.
The __initdata and __exitdata Attributes
Beyond __init and __exit for functions, the kernel provides matching attributes for data: __initdata and __exitdata. Data marked __initdata is only needed during initialization (a table of initial configuration values, for instance) and, like __init code, can be discarded from memory after the init function completes — a small but real memory saving multiplied across every module and, more significantly, across the many __init-marked subsystems of the core kernel itself at boot.
static struct config_entry default_config[] __initdata = {
{ .name = "mode", .value = 1 },
{ .name = "timeout", .value = 30 },
};
Using these attributes correctly requires discipline: nothing outside the init/exit path should ever reference __initdata/__exitdata structures, because that memory may no longer exist by the time such a reference is evaluated. The kernel’s build system includes checks (modpost) specifically to catch cases where non-init code incorrectly references init-only sections, precisely because this class of bug is subtle and dangerous enough to warrant automated detection.
Practical Debugging Walkthrough
Suppose rmmod mymodule completes without error, but the system later oopses with a stack trace pointing into freed module memory. Here’s the debugging sequence that tends to work:
- Check
dmesgimmediately for the exact faulting address and any accompanying stack trace — often the function name will still be symbolized if the crash happens quickly enough after unload, before the freed memory is reused. - Review the exit function against everything the init function (and any code running after it) set up: every registered callback, every timer, every workqueue item, every interrupt handler.
- Specifically look for asynchronous mechanisms cancelled with a non-synchronous variant (
del_timer()instead ofdel_timer_sync(), for example) — this is the single most common root cause of “clean unload, later crash” bugs. - If available, reproduce with
CONFIG_DEBUG_KMEMLEAKorKASANenabled in a test kernel, which can catch use-after-free access much closer to the actual moment it happens rather than an arbitrary time later.
Comparing Module Parameters at Entry Time Across Systems
Since the entry point is also where a module’s configurable behavior gets locked in, it’s worth looking at how different systems handle this “configure at load time” step, because it shapes how flexible entry-point logic needs to be. Linux, as shown earlier, uses module_param() declarations parsed automatically before module_init runs, letting administrators pass values like modprobe e1000e InterruptThrottleRate=3000. Windows drivers more commonly read configuration from the registry during DriverEntry, querying specific keys under the driver’s service entry rather than accepting command-line-style parameters, reflecting Windows’s broader preference for centralized, persistent configuration storage over ephemeral load-time arguments. FreeBSD’s KLD modules can similarly accept sysctl-tunable parameters, often checked during module initialization to decide which features to enable. In every case, the underlying principle is the same one at the heart of this whole topic: the entry point is the only natural moment to consult configuration and make decisions that will govern the module’s behavior for its entire resident lifetime, since there’s no equivalent “reconfigure on the fly” hook most modules can rely on without additional, deliberately-built infrastructure like sysfs attributes or ioctl-based runtime reconfiguration.
What Happens When Entry and Exit Points Are Missing Entirely
It’s worth briefly addressing what happens for the (now rare, but historically real) case of extremely old-style Linux modules that didn’t use module_init()/module_exit() macros at all, relying instead on functions literally named init_module() and cleanup_module() directly. The kernel module loader has always looked for these specific symbol names as a fallback if the newer macro-based registration isn’t present, which is itself a small illustration of how much backward compatibility the kernel module ABI has had to preserve over decades of active development. Modern module code should always use the macros rather than these legacy direct names, since the macros additionally handle module aliasing, parameter registration, and version compatibility metadata that the bare function-name approach doesn’t provide — but understanding that this fallback exists explains some genuinely old third-party driver source code you might still encounter if working with legacy embedded systems.
The Interaction Between Exit Points and Kernel Panics
One nuance worth calling out explicitly: exit functions are only ever invoked during an orderly, requested unload. If the kernel itself panics — whether due to a bug in this module, a different module entirely, or a hardware fault — no exit function anywhere runs at all, orderly or otherwise; the machine simply halts or reboots depending on panic configuration (panic_on_oops, watchdog settings, and similar). This means exit-point cleanup logic should never be relied upon as the only mechanism protecting against data loss or corruption — anything genuinely critical (flushing data to persistent storage, for instance) needs to happen proactively during normal operation, not deferred to a cleanup path that a sufficiently severe failure will simply never reach. This is a subtle but important distinction from user-space cleanup patterns like atexit() handlers or destructors, which at least have a fighting chance of running even during many abnormal termination scenarios (though not all) — kernel exit points offer no such guarantee once things have gone sufficiently wrong.
Summary
The entry and exit points aren’t ceremonial boilerplate — they’re the entire lifecycle contract a kernel module has with the operating system. The entry point is a one-shot, all-or-nothing gate: fail it, and the kernel assumes nothing was left behind, so it never calls your cleanup code, putting the burden of manual unwinding squarely on you. The exit point is the kernel’s only opportunity to release everything a module owns, and getting its ordering or completeness wrong is one of the most common causes of kernel panics after a module unload. Treating these two functions with real care is, honestly, most of what separates a stable driver from a flaky one.
FAQs
What happens if __init returns a nonzero value? The kernel treats the load as failed, discards the module immediately, and does not call the exit function — any partial setup must be cleaned up manually inside the init function itself.
Can a module have multiple entry or exit functions? No — module_init() and module_exit() each register exactly one function, though that function can of course call out to as many helper functions as needed.
Why can’t I unload a module sometimes? The kernel tracks a per-module reference/usage count; if anything is still actively depending on the module, rmmod will refuse (or block) until that count reaches zero.
Is __init memory really freed after the module loads? Yes, in many configurations the memory backing the init function itself is freed after execution completes, similar to how the kernel frees its own boot-time init code.
Do built-in (non-loadable) kernel drivers also have entry/exit points? They have an entry point of sorts (also __init, invoked at boot rather than at module-load time), but no exit point ever runs, since they can’t be unloaded.
References
- Linux Kernel Documentation, “Building External Modules” — https://www.kernel.org/doc/html/latest/kbuild/modules.html
- Linux Kernel Source,
include/linux/module.handinclude/linux/init.h - “Linux Device Drivers, 3rd Edition” (Corbet, Rubini, Kroah-Hartman) — https://lwn.net/Kernel/LDD3/
- Microsoft Docs, “DriverEntry Routine” — https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nc-wdm-driver_initialize
- Apple Developer Documentation, “IOKit Fundamentals” — https://developer.apple.com/documentation/kernel/iokit
