Writing an interrupt handler is one of the trickiest parts of free Linux device drivers course material because the rules are very different from ordinary kernel or user-space code. This lesson in our free Linux kernel development course explains exactly what an interrupt handler is allowed to do, why those restrictions exist, and how modern kernels (6.x) help you move heavy work out of the interrupt context safely.
Prerequisites
- Understanding of request_irq() / devm_request_irq()
- Basic idea of kernel process scheduling
- Familiarity with kmalloc() and GFP flags
What Is Atomic Context?
When your interrupt handler runs, it executes in what the kernel calls atomic context (sometimes called interrupt context). In this mode, the kernel cannot call the scheduler, meaning your code cannot be put to sleep and cannot be preempted by another process the normal way. This is fundamentally different from the process context that a regular system call handler or your driver’s probe() function runs in.
|
Process Context – Runs on behalf of a task/PID – Can sleep / block – Can use GFP_KERNEL – Can copy_to_user() / copy_from_user() – Can be preempted |
Interrupt (Atomic) Context – Runs on behalf of hardware – Cannot sleep / block – Must use GFP_ATOMIC – Cannot touch user-space memory – Preemption effectively disabled |
Rule 1: Never Block or Sleep
Because the scheduler cannot run while a hard-IRQ handler is executing, any function that could put the current context to sleep is forbidden. This includes:
- Waiting on a mutex (
mutex_lock()) - Calling
copy_to_user()/copy_from_user(), since a page fault might need to sleep to fetch a swapped-out page - Allocating memory with
GFP_KERNEL(useGFP_ATOMICinstead — it fails fast rather than blocking) - Calling any blocking I/O function such as an I2C/SPI transfer on many controllers
Rule 2: Interrupts Are Masked While You Run
By default, while your handler executes, the specific interrupt line is masked on all CPUs, and on the CPU actually running your handler, local interrupts are typically disabled too. This has two effects: your handler is naturally protected from being re-entered by the same interrupt, but it also means the longer you take, the longer other interrupts on that core may be delayed.
Rule 3: Keep It Fast — Measure, Don’t Guess
Two metrics matter for interrupt handler quality: how long your handler runs (interrupt latency it introduces for others) and how long interrupts stay disabled system-wide. On modern kernels you can inspect this with tracer tools such as ftrace‘s irqsoff tracer, which records the worst-case interrupt-disabled duration observed on the system.
# Enable the irqsoff latency tracer (requires CONFIG_IRQSOFF_TRACER)
echo irqsoff > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
sleep 5
cat /sys/kernel/tracing/trace > /tmp/irq_latency.txt
A Simple, Original Example: Wrong vs Right
The following original example shows a handler that violates the atomic-context rules, followed by a corrected version using a threaded IRQ and a workqueue-friendly pattern.
/* WRONG: allocates with GFP_KERNEL and sleeps inside a hard IRQ handler */
static irqreturn_t bad_isr(int irq, void *dev_id)
{
void *buf = kmalloc(256, GFP_KERNEL); /* may sleep -- BUG */
mutex_lock(&some_driver_mutex); /* may sleep -- BUG */
/* ... */
mutex_unlock(&some_driver_mutex);
kfree(buf);
return IRQ_HANDLED;
}
/* RIGHT: hard IRQ just wakes a thread; heavy work happens safely later */
static irqreturn_t good_hard_isr(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
static irqreturn_t good_thread_fn(int irq, void *dev_id)
{
struct mydev *priv = dev_id;
void *buf = kmalloc(256, GFP_KERNEL); /* safe here: process context */
mutex_lock(&priv->lock); /* safe here: process context */
/* ... do the real work ... */
mutex_unlock(&priv->lock);
kfree(buf);
return IRQ_HANDLED;
}
Top Half vs Bottom Half: Where Does the Real Work Go?
Because a hard-IRQ handler (“top half”) must be minimal, the kernel offers several mechanisms to defer the rest of the work (“bottom half”) to a safer context.
| Mechanism | Runs In | Can Sleep? | Typical Use |
|---|---|---|---|
Threaded IRQ (request_threaded_irq) | Kernel thread | Yes | I2C/SPI sensors, modern driver default |
| Tasklet (legacy) | Softirq context | No | Older drivers; being phased out in favor of threaded IRQs |
| Workqueue | Kernel worker thread | Yes | Work that needs to run later, possibly with delay |
| Softirq | Atomic context | No | Very high-frequency subsystems (e.g. networking) |
Note: On current kernels, the recommended default for most device drivers is a threaded IRQ rather than a tasklet, because tasklets are being deprecated in favor of the threaded-IRQ and workqueue model, which is simpler to reason about and avoids atomic-context restrictions entirely.
Common Mistakes
- Calling
printk()excessively inside a hard-IRQ handler — logging is slower than it looks and adds latency. - Allocating large buffers with
GFP_ATOMICrepeatedly, which can fail under memory pressure since atomic allocations have no room to reclaim memory. - Forgetting that shared-line handlers must quickly check whether the interrupt actually belongs to their device before doing any work.
- Using a spinlock in the top half and then trying to acquire the same spinlock in process context without disabling interrupts, leading to a self-deadlock.
Best Practices
- Treat the hard-IRQ handler as a “notify and exit” function — do the minimum, then hand off.
- Prefer threaded IRQs for anything involving I2C/SPI, memory allocation, or locking.
- Use
spin_lock_irqsave()/spin_lock_irqrestore()when sharing data between a hard-IRQ handler and process context. - Profile with
ftrace‘sirqsofftracer during development on latency-sensitive boards.
Performance Considerations
On multi-core embedded SoCs, IRQ affinity (which CPU core handles a given interrupt) can be tuned via /proc/irq/<n>/smp_affinity to keep latency-sensitive interrupts off a core that is busy with other real-time work.
Security Considerations
An interrupt handler that takes unpredictably long — for example due to an unbounded loop reading from a misbehaving or malicious peripheral — can be used as a denial-of-service vector against the whole system. Always bound loops and timeouts inside a handler.
Summary / Key Takeaways
- Hard-IRQ handlers run in atomic context: no sleeping, no
GFP_KERNEL, no user-space copies. - Interrupts are masked while your handler runs, so speed matters.
- Use threaded IRQs or workqueues to move real work out of atomic context.
- Tasklets still exist but are considered legacy compared to threaded IRQs on modern kernels.
Frequently Asked Questions
Q1. Why can’t an interrupt handler sleep?
Sleeping requires the scheduler to pick another task, but the kernel cannot safely reschedule while it is servicing a hardware interrupt in atomic context.
Q2. What is the difference between GFP_KERNEL and GFP_ATOMIC?
GFP_KERNEL may block while the kernel reclaims memory; GFP_ATOMIC never blocks and instead fails immediately if memory isn’t readily available.
Q3. Are tasklets still used in new drivers?
They still exist, but new driver code is generally steered toward threaded IRQs or workqueues, which don’t carry the same atomic-context restrictions.
Q4. What happens if my interrupt handler takes too long?
It increases system-wide interrupt latency and can starve other interrupts and even trigger watchdog or soft-lockup detectors on some kernel configurations.
Q5. Can I use printk() inside an interrupt handler?
Yes, but sparingly — heavy logging inside a hard-IRQ handler adds latency and should be reserved for rare error paths, not routine events.
Q6. How do I safely share data between the hard-IRQ handler and process context?
Use spin_lock_irqsave()/spin_lock_irqrestore() around the shared data on both sides so the process-context path also disables local interrupts while holding the lock.
This lesson is part of EmbeddedPathashala’s free Linux kernel programming and device drivers course.
Next Lecture →
2 Comments