AlgoMaster Logo

Spinlocks, Preemption, and Interrupt Context

18 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A device interrupt arrives while the kernel is updating a queue that the interrupt handler also uses. The handler cannot wait on a normal blocking mutex: it is not running as an ordinary schedulable thread, and the interrupted code may be the very code that must release the mutex.

The kernel needs a synchronization primitive that never puts the current execution context to sleep. A spinlock provides that property. If the lock is unavailable, the contender repeatedly checks until the owner releases it.

Spinning deliberately consumes CPU time, so it is appropriate only under strict conditions: the critical section must be short, the owner must be able to continue running, and no operation inside the section may sleep.

Interrupts add another requirement. A spinlock can coordinate CPUs, but it cannot by itself stop an interrupt handler on the current CPU from interrupting the lock holder and attempting to acquire the same lock. Kernel code therefore combines spinlocks with preemption control or local interrupt masking according to which execution contexts share the data.

What a Spinlock Does

A spinlock has two logical states:

A successful atomic acquisition moves the lock from unlocked to locked, and the owner releasing it moves the lock back.

If acquisition fails, a contender remains active:

The failure path loops rather than sleeping. The waiting thread keeps its CPU the entire time, which is only reasonable when the holder will release very soon.

A conceptual implementation might resemble:

This is explanatory pseudocode, not a production lock implementation. Real spinlocks use architecture-specific atomic operations and memory barriers. Contended implementations may also use more scalable queueing techniques to reduce cache traffic.

cpu_relax() represents an architecture hint used inside a spin loop. It can reduce pressure on processor resources, but it does not block the task or return the CPU to the scheduler.

The execution context that acquires a spinlock must release it. Spinlocks are not recursive; attempting to acquire the same lock again before releasing it cannot make progress.

Spinning vs. Blocking Tradeoffs

A blocked waiter stops consuming CPU but pays for a kernel scheduling path and later resumption. A spinning waiter avoids that sleep-and-wakeup path but consumes execution capacity for the entire wait.

Spinning is useful only when the expected wait is shorter than the cost and disruption of blocking, and when the owner is guaranteed a chance to release the lock.

There is no universal time threshold. The trade-off depends on processor speed, CPU count, current load, virtualization, cache topology, critical-section behavior, and scheduling policy.

Kernel spinlocks are mainly justified by execution contexts that are forbidden to sleep or by extremely short low-level critical sections. A normal application mutex is a better default for user-space code.

Loading simulation...

Why Preemption Matters

Suppose Task A owns a spinlock on CPU 0. If the kernel preempts A and schedules Task B on the same CPU, B may try to acquire that lock:

Each task is waiting on something only the other can provide, and on a single CPU neither can make progress. Disabling preemption while the lock is held is what prevents this.

Task B is consuming the only CPU on which the preempted owner could continue. Progress now depends on the scheduler eventually preempting B and restoring A.

Under conventional non-real-time Linux semantics, acquiring a kernel spinlock disables preemption on the local CPU. The lock holder remains the current task until it releases the lock, except for execution contexts such as interrupts that obey separate rules.

OperationWhat it does
spin_lockDisables local task preemption, then acquires the global lock
spin_unlockReleases the global lock, then re-enables local task preemption when permitted

Disabling preemption is local. It prevents another task from replacing the holder on that CPU; it does not stop code running on other CPUs. The spinlock's atomic state provides inter-CPU exclusion.

Keeping preemption disabled for too long increases scheduler latency. The protected work must therefore remain bounded and small.

Process Context and Interrupt Context

Process context means kernel code is executing on behalf of a schedulable task. A system call is a typical example. The current task has a process identity and can normally block at points where kernel rules permit.

Hard interrupt context runs because hardware delivered an asynchronous interrupt:

A task executes kernel or user code, a device interrupt arrives, the hard interrupt handler runs, and when it returns the interrupted execution continues.

The hard interrupt handler is not an independent ordinary task that the scheduler can put to sleep and resume later at will. It runs on top of interrupted execution and must finish promptly.

Consequently, hard interrupt context cannot use operations that may block:

  • Sleeping mutex acquisition
  • Waiting on a condition or completion
  • Explicit calls into the scheduler
  • Memory allocation modes that may sleep
  • Accesses that can fault and require a sleeping path

If a handler needs substantial work, it records the urgent state and arranges for suitable deferred or threaded processing. The hard interrupt portion remains short.

A threaded interrupt handler is different. Its work runs in a kernel thread and can use facilities permitted in thread context. Code must know which kind of handler it is in rather than assuming that every function associated with an interrupt has identical restrictions.

Why a Plain Spinlock Is Not Enough for Interrupts

Consider state shared by a system call and a device's hard interrupt handler.

The system-call path acquires a plain spinlock:

Before it releases the lock, the device interrupts CPU 0:

The handler spins, but the interrupted lock holder cannot resume until the handler returns. The handler cannot return until it acquires the lock. Neither path can make progress.

The handler cannot be descheduled and the owner cannot be resumed, so this CPU is stuck. Masking the interrupt while holding the lock is what avoids it.

The repair is to prevent the relevant local interrupt from interrupting a section that holds a lock the handler may acquire.

Local Interrupt Masking

Linux provides spinlock variants that combine lock acquisition with local interrupt control.

The most general conventional form is:

spin_lock_irqsave:

  1. Saves the current local interrupt-enable state in flags.
  2. Disables maskable interrupts on the current CPU.
  3. Acquires the spinlock.

spin_unlock_irqrestore releases the lock and restores the saved interrupt state.

Saving and restoring is important because the caller may already have interrupts disabled. Blindly enabling interrupts at the end could violate the caller's execution context.

Local interrupt disabling does not stop interrupts on other CPUs:

The interrupt mask prevents same-CPU reentry. The spinlock coordinates concurrent access from other CPUs.

Interrupt masking must be brief. Leaving local interrupts disabled delays timers, device service, and other interrupt-driven work on that CPU.

A Kernel-Style Shared-State Example

The following simplified driver fragment has state used from both process context and a hard interrupt handler:

This is a synchronization illustration rather than a complete driver. Real interrupt handlers must acknowledge the specific device correctly, manage teardown, and coordinate the lifetime of any deferred work.

Both paths use the same lock and protect the same invariant. Their critical sections contain only bounded memory operations. They do not sleep, allocate through a sleeping path, or call an unknown function while the lock is held.

Static kernel spinlocks can instead use:

Application code cannot use kernel spinlock APIs. They are internal kernel interfaces compiled as part of kernel or module code.

Soft Interrupt Context

Linux also performs some deferred work in soft interrupt, or softirq, context. Softirq code is not ordinary process context and must not call sleeping operations.

If shared state is accessed from process context and softirq context, Linux provides:

The _bh form disables bottom-half processing on the local CPU while acquiring the lock. This prevents a local softirq from interrupting the process-context holder and trying to acquire the same lock. The spinlock still coordinates other CPUs.

spin_lock_bh does not provide the same hard-interrupt protection as an IRQ-disabling variant. The choice must match every context that accesses the state:

Contexts that touch the stateLock to use
Process context only, and sleeping is acceptableA mutex
Non-sleeping task contextsspin_lock
Process and softirq contextspin_lock_bh
A hard interrupt may use the statespin_lock_irqsave

These are conceptual defaults, not a substitute for subsystem-specific locking rules.

Non-maskable interrupts are outside the protection of ordinary local IRQ disabling. State accessed from NMI context requires primitives and designs explicitly permitted there.

Preemption Control vs. Global Locking

Disabling preemption can protect some per-CPU data from task migration and task-context reentry on a conventional kernel:

With preemption disabled on CPU 2, the current task remains on CPU 2.

It does not stop:

  • Another CPU from accessing globally shared data
  • A local hard interrupt from running
  • A local softirq from running where permitted

Likewise, disabling local interrupts does not stop another CPU.

This leads to a useful separation:

MechanismWhat it controls
Preemption or interrupt controlConcurrency sources on this CPU
A spinlockExclusion between participating CPUs

Linux also provides named local-lock interfaces for per-CPU protection. They document the protected scope and integrate with kernel lock validation more clearly than scattered raw preemption-disable calls.

Sleeping Restrictions for Spinlock Holders

A true spinlock protects an atomic context: a region in which blocking is forbidden.

If the owner sleeps, other CPUs can spin for an unbounded interval. On a single CPU, no contender can make the owner run by continuing to spin. Kernel scheduling assumptions and interrupt state may also make the attempted sleep invalid before any progress issue appears.

Operations that can sleep must remain outside a true spinlocked section:

  1. Prepare the potentially blocking work first.
  2. Acquire the spinlock, update the small shared state, and release the spinlock.
  3. Perform the remaining sleepable work.

Kernel allocation APIs make this distinction visible. An allocation allowed to reclaim memory or wait is not valid in hard interrupt or true atomic context. Selected non-sleeping allocation modes exist, but they have tighter resource constraints and can fail more readily.

A call is not safe merely because it is usually fast. Its contract must guarantee that it cannot sleep on any relevant path.

Loading simulation...

Memory Ordering from Spinlocks

The lock word is only part of the contract. Acquiring and releasing a spinlock also impose memory-ordering constraints on the protected state.

Conceptually:

spin_lock provides acquire ordering and spin_unlock provides release ordering, so the ordinary loads and stores between them stay inside the critical section.

A CPU that successfully acquires the lock after another CPU releases it observes the protected updates according to the lock's memory-ordering rules.

Protected fields therefore do not need volatile, and individually making them atomic does not replace the lock when an invariant spans several fields.

The interrupt-disabling operation has a different purpose. It controls local interrupt delivery. It should not be treated as the inter-CPU memory-ordering mechanism; the lock primitives provide the locking and ordering contract.

What Happens on a Single-CPU Kernel?

A literal spin loop cannot wait efficiently for another task on the same CPU. The owner cannot execute while the waiter consumes that CPU.

Kernel locking APIs still need to preserve correctness on uniprocessor builds. Under conventional non-real-time semantics, spinlock operations can reduce to the local preemption or interrupt controls required by the sharing context, while unnecessary inter-CPU atomic work can be compiled away.

This illustrates why kernel code uses the provided lock API rather than open-coding a spin loop. The implementation can adapt to:

  • Uniprocessor and multiprocessor builds
  • Preemptible and non-preemptible kernels
  • Architecture-specific atomic instructions
  • Debugging and lock-validation configurations

The source-level protection rule remains valid even when a particular build does not need a physical inter-CPU spin.

The PREEMPT_RT Qualification

The classic rules in this lesson describe spinlock_t on a non-PREEMPT_RT Linux kernel and raw_spinlock_t where strict spinning semantics are required.

On a PREEMPT_RT kernel, ordinary spinlock_t is transformed into a lock based on a real-time mutex:

  • Acquisition can block rather than spin in task context.
  • Preemption remains enabled.
  • IRQ-related suffixes do not have the same direct hard-interrupt-masking semantics as on a non-RT kernel.
  • The holder remains pinned against CPU migration while it owns the lock.

raw_spinlock_t remains a true spinning lock on all kernel configurations. It disables preemption and may be combined with interrupt masking, so its critical section must obey the strict no-sleeping rules.

Low-level interrupt, scheduler, and hardware-state code uses raw spinlocks where those semantics are genuinely required. Ordinary code should not switch to raw_spinlock_t merely to preserve assumptions from a non-RT build. It should follow the locking contract of its subsystem and remain correct under the kernel configurations it supports.

This distinction is also why “spinlock always means busy-waiting” is too broad when discussing current Linux source. The exact type and kernel configuration matter.

Risks of User-Space Spinlocks

POSIX environments can expose pthread_spin_lock, but user-space code does not normally control when the scheduler preempts the lock holder.

Suppose one application thread owns a user-space spinlock and is descheduled:

The waiter consumes CPU but cannot cause the owner to release the lock. Oversubscription, virtual-machine scheduling, and CPU quotas can make this much worse.

A user-space spinlock may be reasonable only after measurement, with extremely short critical sections and scheduling conditions that keep owners running. General backend code should prefer a blocking mutex and reduce contention or shared state if performance is inadequate.

Diagnosing Atomic-Context and Spinlock Problems

Development kernels can enable debugging facilities that validate locking and atomic-context rules.

Kernel logs may report messages resembling:

The exact output varies. Such a warning means a potentially sleeping operation was reached while the context required non-sleeping behavior.

Lockdep, the kernel's runtime locking validator, tracks lock classes and context usage. It can detect cases such as a lock being used inconsistently between normal and hard-interrupt contexts before the rare failing interleaving occurs.

Inspect recent warnings with:

Permissions and available diagnostics depend on the system and kernel configuration.

To inspect interrupt distribution and volume:

On a system with symbols and performance-monitoring permission, kernel profiles may show time in functions with names such as _raw_spin_lock or queued_spin_lock_slowpath:

Time in a slow-path symbol can indicate contention, but it is only a starting point. The investigation must identify the protected state, critical-section duration, CPU placement, and callers responsible for the lock traffic.

Summary

A true spinlock provides non-sleeping mutual exclusion. Contenders consume CPU while waiting, so critical sections must be short, bounded, and free of every potentially sleeping operation. Under conventional Linux semantics, spinlock acquisition also disables local task preemption so the owner can continue to the release point.

Interrupt context cannot use ordinary sleeping locks. When a hard interrupt can access the protected state, spin_lock_irqsave combines inter-CPU exclusion with local interrupt masking and later restores the caller's prior interrupt state. Softirq sharing uses bottom-half-aware protection, while preemption or interrupt disabling alone controls only local concurrency.

Linux spinlock_t has classic spinning behavior on non-PREEMPT_RT kernels, while PREEMPT_RT changes its implementation and scheduling semantics. raw_spinlock_t remains a strict spinlock across configurations and carries the strongest no-sleeping constraints.

Quiz

Spinlocks, Preemption, and Interrupt Context Quiz

5 quizzes