JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Kernel Synchronization, Preemption, and Modules

You already know how user threads coordinate with locks and semaphores. Now meet the kernel coordinating with itself — where an interrupt can fire mid-update, where the scheduler can yank the kernel out from under its own feet, and where you can plug new code into a running kernel without rebooting.

The kernel is concurrent too

Up the ladder you learned how user threads avoid stepping on each other — a critical section guarded by a mutex lock or a semaphore, so that a race condition never corrupts shared data. It is tempting to think the kernel itself is somehow above all that: the all-powerful building manager who never bumps into anyone. The opposite is true. The kernel holds the most heavily shared data in the whole machine — the ready queue, the page tables, the open-file tables, every device's buffers — and many things touch that data seemingly at once. The kernel is the most concurrency-stressed program on the system, and it must synchronize itself with the same care, plus a few hazards user code never faces.

Where does the concurrency come from? Three sources, and they stack on top of each other. First, on a multicore machine, several CPUs are literally running kernel code at the same instant — true parallelism, not just interleaving. Second, even on one core, an interrupt (the doorbell from a previous guide) can fire while the kernel is halfway through updating a list, and the handler may want to touch that very same list. Third, in a modern kernel the scheduler can suspend the kernel itself mid-operation to run a more urgent task. Each source is a different way for two pieces of kernel code to reach the same data at an awkward moment, and each needs its own defense.

Why the kernel cannot just sleep

When a user thread cannot get a lock, the kindest thing is to put it to sleep and let the scheduler run someone else — exactly the blocking semaphore you already met. But some kernel code is not allowed to sleep, and this single rule reshapes everything. The classic example is interrupt context: when the doorbell rings, the handler must run, finish, and get out fast so the next interrupt can be served. If a handler tried to block waiting for a lock, there is nothing sensible to switch to and the whole machine could wedge. So in those contexts the kernel cannot afford a blocking lock. It needs a lock whose waiter stays awake and simply spins.

That lock is the spinlock, and inside the kernel it earns its keep precisely where it would be wasteful in user space. A spinlock's waiter does busy waiting — it loops, checking the lock over and over, burning a CPU core the whole time. In user code that is usually a sin. But a kernel spinlock guards only a handful of instructions (update a counter, splice a node into a list) and is released in nanoseconds, so spinning briefly is cheaper than the two context switches sleeping would cost. The rule of thumb is brutal and clear: hold a spinlock only for a few instructions, never do anything that could sleep while holding one, and on a single CPU the spinlock by itself does almost nothing — its real job is to keep two different cores out of the same critical section.

But a spinlock alone does not solve the interrupt hazard on one core. Picture it: a CPU takes a spinlock to edit a list, and before it finishes, an interrupt fires and the handler tries to take the very same spinlock. The handler spins, waiting for a lock that the interrupted code can never release — because that code is frozen, waiting for the handler to return. That is a self-inflicted deadlock, and the standard fix is to disable interrupts on this CPU for the brief moment the lock is held. So the real kernel idiom is: disable interrupts, grab the spinlock, do the tiny critical section, release the spinlock, re-enable interrupts. The doorbell is muted just long enough to finish writing one note.

Preemption: when the kernel runs out from under itself

There is a deeper design choice hiding here, and it is the difference between an old kernel and a modern one. The question is: while a thread is running inside the kernel (in the middle of a system call, say), is the scheduler allowed to stop it and run a more urgent thread? That is kernel preemption, and it is just the preemptive scheduling idea you met for processes, now turned inward on the kernel itself. A non-preemptible kernel says no: once you enter the kernel, you run until you either finish or voluntarily give up the CPU. A preemptible kernel says yes: even kernel code can be paused at almost any safe point.

Why bother? Responsiveness. Imagine a media-playback thread that must refill the audio buffer every few milliseconds or you hear a glitch. If a background process wanders into a long system call inside a non-preemptible kernel, the audio thread cannot run until that call finishes, however urgent it is — the audible result is a stutter. The delay between deciding to switch and actually running the new thread is the dispatch latency, and a preemptible kernel exists to make it small and predictable. This matters enormously for a real-time operating system, where a missed deadline is not a glitch but a failure.

Preemption is a gift that creates its own bill. If the kernel can be paused mid-operation, then two threads can be interleaved inside the kernel on a single core, reviving the same races multicore already had. So a preemptible kernel must be sprinkled with the very synchronization we have been discussing, marking the spots where it is unsafe to be preempted. Conveniently, the spinlock idiom already does this: holding a spinlock disables preemption on that CPU, so the same primitive that keeps other cores out also keeps the scheduler from interrupting you mid-critical-section. Lock acquisition and 'do not preempt me now' turn out to be two faces of the same coin.

Reads that almost never lock: RCU and the top/bottom split

Locks are expensive when data is read constantly but written rarely — think of a routing table that thousands of packets consult every second but that changes only when the network reconfigures. Forcing every reader to take a lock just to look would throttle the common case to protect the rare one. The kernel's elegant answer is read-copy-update (RCU): readers take no lock at all and pay almost nothing, while a writer never edits the data in place. Instead a writer copies the structure, modifies the copy, and then atomically swaps a single pointer so new readers see the new version. Old readers keep using the old version safely; only once the last of them has finished is the old copy reclaimed. Readers are fast precisely because writers do all the hard work.

One more piece ties this back to interrupts. Recall the top-half / bottom-half split from the interrupt guide: the top half is the tiny urgent handler that runs with interrupts disabled and must finish in a flash, while the bottom half is the deferrable work scheduled to run later with interrupts back on. This split is itself a synchronization strategy. By keeping the interrupts-off window vanishingly small, the kernel minimizes how long it mutes the doorbell, and by pushing the heavy work to a context that is allowed to sleep, it lets that work use ordinary blocking locks. Knowing which context your code runs in — can it sleep or not — is the single most important question before you choose a synchronization primitive at all.

Loadable modules: extending a kernel that is already running

Now switch from how the kernel coordinates to how it grows. A kernel must understand every device, file system, and protocol in existence — but baking all of that into one giant binary would make it bloated and force a rebuild for every new gadget. The answer is the loadable kernel module: a chunk of kernel code, most often a device driver, that can be compiled separately and slotted into a running kernel on demand, then removed when no longer needed. Plug in a USB camera and the matching module loads itself; the kernel grows a new capability without a single reboot. It is the building manager hiring a specialist contractor for an afternoon rather than keeping every trade on permanent staff.

Here is the part beginners often miss, and it is important to be honest about: a loadable module is not a sandboxed plugin. Once loaded, it runs in full kernel mode with the same total power as the rest of the kernel — no protection boundary stands between it and the core. A bug in a module is a bug in the kernel: a bad pointer can corrupt anything, and a module crash can take the whole system down. Modularity here is about build-time and load-time convenience, not about isolation. This is exactly the tension the final guide explores: a monolithic kernel like Linux runs drivers as trusted in-kernel modules for speed, while a microkernel pushes drivers out into separate isolated processes for robustness, paying for that safety in communication cost.

  insert a module:   user runs `insmod camera.ko`
     |  module's init function runs in kernel mode
     |  it registers itself (e.g. adds a driver to a table)
     v  kernel now has the new capability -- no reboot
  ...module handles its device, sharing all kernel locks/RCU...
     |  user runs `rmmod camera`  (only if nobody is using it)
     v  module's exit function unregisters + frees, then unloads
The life of a module. Note it must use the kernel's own spinlocks, RCU, and preemption rules — it is not a guest, it is part of the kernel while loaded.

Putting it together: the discipline that holds it all up

Step back and the whole picture is one theme in many costumes. Whether it is two cores, an interrupt, a preemption, or a freshly loaded module, the danger is always the same: two pieces of code reaching shared kernel data at an awkward moment. The defenses form a small, honest toolbox, and choosing among them is the real skill. The deciding question, every time, is the context: can this code sleep?

  1. Can this code sleep? If it runs in interrupt context or while holding a spinlock, the answer is no — never call anything that might block there.
  2. Short critical section that cannot sleep? Use a spinlock — and if an interrupt handler also touches the data, disable interrupts on this CPU while you hold it.
  3. Longer work that may sleep? Use a blocking lock (a kernel mutex or semaphore), and make sure you are in a context that is allowed to sleep — typically a bottom half or a system-call path.
  4. Read-mostly data, rare writes? Reach for RCU so readers pay nothing; let the writer copy, swap one pointer, and defer reclaiming the old version.

And the same brutal honesty from the synchronization rung still applies, now with even higher stakes. A kernel lock protects data only if every path — every driver, every module, every interrupt handler — uses it correctly; one careless module that skips the lock reopens the race for the entire system. Hold locks in inconsistent orders and you invite deadlock; let a low-priority holder block a high-priority waiter and you get priority inversion, the very bug that nearly lost the Mars Pathfinder mission. The kernel is the most powerful program on the machine, and that power is precisely why its discipline must be flawless. Next, the final guide steps all the way back to ask whether all this should live in one big kernel at all.