In the last lecture of this free linux kernel development course we fixed a self-deadlock by switching from spin_lock() to spin_lock_irq(). That fix has a hidden assumption baked into it: that it’s fine to unconditionally re-enable all interrupts when the lock is released. On a real system with multiple subsystems touching the interrupt mask, that assumption can quietly break someone else’s carefully set up configuration. spin_lock_irqsave() is the API that removes the assumption entirely.
What You Will Learn
- Why blindly re-enabling all interrupts on unlock can silently break other parts of the system
- How
spin_lock_irqsave()/spin_unlock_irqrestore()save and restore the exact previous interrupt mask - A working, original kernel 6.x driver example demonstrating the pattern correctly
- A first look at
spin_lock_bh(), for when you’re racing a softirq or tasklet instead of a hardirq
Prerequisites
- The previous lecture on
spin_lock_irq()/spin_unlock_irq() - Basic familiarity with spinlocks and critical sections
The Hidden Assumption in spin_lock_irq()
Think about a system where some other piece of code — maybe written by a colleague, maybe in a completely different subsystem — has deliberately configured the interrupt mask so that certain interrupts are enabled and others are deliberately kept disabled. That configuration exists for a reason: maybe a particular interrupt source is being intentionally quieted during some sensitive operation.
Now your driver comes along and calls spin_lock_irq() to protect its own critical section. That call disables all interrupts on the local core, not just the ones your driver cares about. When you call spin_unlock_irq(), it re-enables all interrupts — including the ones your colleague had intentionally left masked. Their careful configuration has just been silently wiped out.
The Fix: spin_lock_irqsave() / spin_unlock_irqrestore()
Instead of assuming what the interrupt mask should be afterward, this API pair simply remembers what it was beforehand and puts it back exactly:
#include <linux/spinlock.h>
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
/* ... critical section ... */
spin_unlock_irqrestore(&my_lock, flags);
Two details matter here. First, flags must be a plain local variable of type unsigned long — it’s used purely to carry the saved mask value from the lock call to the matching unlock call, so it must live on the stack of the function doing the locking, not in a struct or global. Second, spin_lock_irqsave() is technically implemented as a macro (it needs to capture the caller’s local flags variable directly), even though it’s conventionally described as if it were a function.
Original Demo: A Driver That Respects an Existing Interrupt Mask
The example below is an original teaching module for kernel 6.x. It doesn’t touch real hardware interrupt mask registers directly (that’s arch-specific and outside the scope of a portable teaching example) — instead it demonstrates the pattern the correct way, using a spinlock protecting a shared counter accessed from both a process-context path and a simulated interrupt path (again standing in with an hrtimer, as in the previous lecture).
/* ep_irqsave_demo.c — teaching module, kernel 6.x
* Demonstrates the correct spin_lock_irqsave() /
* spin_unlock_irqrestore() pattern for a critical section
* that can be entered from process context while a hardirq-like
* path can also touch the same data.
*/
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>
static DEFINE_SPINLOCK(ep_slock);
static struct hrtimer ep_htimer;
static long ep_shared_value;
/* Simulated hardirq-context path */
static enum hrtimer_restart ep_timer_cb(struct hrtimer *t)
{
unsigned long flags;
spin_lock_irqsave(&ep_slock, flags);
ep_shared_value += 1;
spin_unlock_irqrestore(&ep_slock, flags);
hrtimer_forward_now(t, ms_to_ktime(5));
return HRTIMER_RESTART;
}
/* Process-context path, e.g. a driver's write() method */
static void ep_process_context_update(long delta)
{
unsigned long flags;
spin_lock_irqsave(&ep_slock, flags);
ep_shared_value += delta;
spin_unlock_irqrestore(&ep_slock, flags);
}
static int __init ep_irqsave_init(void)
{
hrtimer_init(&ep_htimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
ep_htimer.function = ep_timer_cb;
hrtimer_start(&ep_htimer, ms_to_ktime(5), HRTIMER_MODE_REL);
ep_process_context_update(100);
pr_info("ep_irqsave_demo: loaded, value=%ld\n", ep_shared_value);
return 0;
}
static void __exit ep_irqsave_exit(void)
{
hrtimer_cancel(&ep_htimer);
pr_info("ep_irqsave_demo: unloaded, final value=%ld\n", ep_shared_value);
}
module_init(ep_irqsave_init);
module_exit(ep_irqsave_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala: spin_lock_irqsave demo");
Notice that both call sites use spin_lock_irqsave() — including the simulated interrupt path. That’s deliberate: whichever context happens to run first saves whatever the current mask is, and the matching unlock restores exactly that, regardless of which context got there first. That’s the whole point — it works correctly no matter what the interrupt mask looked like before either call site ran.
spin_lock_irq() vs spin_lock_irqsave()
| Aspect | spin_lock_irq() | spin_lock_irqsave() |
|---|---|---|
| Interrupt mask on unlock | Always fully re-enabled | Restored to whatever it was before lock |
| Safe when some other code has set a custom mask | No — can silently overwrite it | Yes |
| Extra parameter needed | None | unsigned long flags local variable |
| General recommendation | Fine when you know the mask starts “all enabled” | Safer default in most real driver code |
A First Look at spin_lock_bh()
Hardware interrupts (hardirqs) aren’t the only asynchronous code paths that can race with your process-context critical section. Softirqs and tasklets — the kernel’s “bottom half” mechanisms that run after a hardirq handler returns — can also touch shared data. For that situation the kernel provides a related but distinct API:
#include <linux/spinlock.h>
void spin_lock_bh(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
spin_lock_bh() disables bottom-half processing (softirqs and tasklets) on the local core before taking the lock, the same way spin_lock_irq() disables hardware interrupts. We’ll build a full working example of this, including what a softirq/tasklet race actually looks like, in the next lecture.
Frequently Asked Questions
Q1. Can I reuse the same flags variable across two different spinlocks?
No — each spinlock needs its own flags variable, since it holds the state specific to that particular lock/unlock pair.
Q2. Does spin_lock_irqsave() disable preemption too?
Yes, exactly like spin_lock_irq() — disabling local hardware interrupts implicitly disables kernel preemption on that core for the duration of the critical section.
Q3. Is spin_lock_irqsave() slower than spin_lock_irq()?
The overhead of saving/restoring the mask is small and generally considered acceptable; correctness against an unknown prior mask state is usually worth far more than the tiny extra cost.
Q4. When is it actually safe to use plain spin_lock_irq() instead of spin_lock_irqsave()?
Only when you can guarantee, for the entire lifetime of the code, that interrupts are always fully enabled before that lock is ever taken. In practice this is hard to guarantee in shared or evolving codebases, which is why irqsave is the safer default.
Q5. Is spin_lock_irqsave() really a macro, not a function?
Yes. It needs direct access to the caller’s local flags variable, which is why it’s implemented as a macro even though it’s commonly written about as if it had a normal function signature.
Q6. What happens if I forget to declare flags as unsigned long?
The build will typically warn or fail, since the macro expects to store the saved processor flags into a variable of that exact type.
Coming Up Next
Next, we look at races with softirqs and tasklets specifically, build a working spin_lock_bh() example, and compare it side by side with the hardirq-focused APIs covered in this lecture and the last.
More kernel synchronization, device driver, and embedded Linux lectures at EmbeddedPathashala.
Explore the Course
2 Comments