A Linux 2.4 Module with a Clean Exit
I unloaded a test module and discovered that its timer was still quite enthusiastic. The machine survived, but only because the callback lost its race with removal. That is not a synchronization strategy I want to defend.
A small 2.4 module has an initialization function and an exit function:
#include <linux/init.h>
#include <linux/module.h>
static int __init sample_init(void)
{
printk(KERN_INFO "sample: loaded\n");
return 0;
}
static void __exit sample_exit(void)
{
printk(KERN_INFO "sample: unloaded\n");
}
module_init(sample_init);
module_exit(sample_exit);
MODULE_LICENSE("GPL");
Initialization should acquire resources in a known order. If the third registration fails, it must release the first two before returning an error. The exit path then releases every successfully acquired resource in the reverse order. This includes interrupt handlers, timers, task queues, proc entries, device numbers, and allocated memory.
The subtle part is activity that may still enter the module. Before freeing data, I stop new work and wait or synchronize with work already in progress. For a timer, del_timer_sync() is preferable when the callback might be running on another processor. For an interrupt, I disable the device’s source as appropriate and call free_irq(), which synchronizes with handlers before returning.
Module use counts provide another guard in 2.4 code. An open file operation commonly increments the count and release decrements it, preventing removal while file operations can still call module text. The exact helper depends on the code and kernel version, but the principle is stable: every path that lends out an active reference must take and return it symmetrically.
Registration order can reduce cleanup work. I initialize private locks and memory before exposing a device entry that another process can open. The externally visible registration comes last. During removal I reverse that idea: withdraw the entry first so no new caller arrives, then drain existing work and release private state. An object should not become public while it is only half constructed.
I keep a small state table while reviewing the code: which resources exist after each possible failure, and which callbacks can still run. If two exits require different cleanup, explicit labels in reverse order are clearer than one flag for every allocation.
I test the unhappy path too. Repeated insmod and rmmod, an intentional allocation failure, and unloading while a user process has the device open find more defects than one successful load at boot.
My preference is an exit routine so boring that each line is the mirror of initialization. The caveat is that reversal alone does not solve concurrency; first make callbacks impossible, then free what they used. Memory is patient. A callback into unloaded text is less so.