What are Interrupt Handler Context Rules in the Linux Kernel: What You Can and Cannot Do-Linux Device Driver Training

Interrupt Handler Context Rules in the Linux Kernel: What You Can and Cannot Do
Free Linux Kernel Programming Course • Free Linux Device Drivers Course • EmbeddedPathashala
Level: Beginner to Intermediate
Kernel Version: 6.x
Reading Time: 11 min

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.

What You Will Learn
What “atomic context” means Why interrupt handlers cannot sleep GFP_ATOMIC vs GFP_KERNEL Interrupt masking behaviour Top half vs bottom half design Modern deferred work mechanisms

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 vs Interrupt (Atomic) Context
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 (use GFP_ATOMIC instead — 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.

Interrupt Masking Timeline
t0: hardware raises IRQ t1: kernel masks the line, calls your handler t2: your handler runs (must be short!) t3: handler returns IRQ_HANDLED t4: kernel unmasks the line, other interrupts can proceed

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 threadYesI2C/SPI sensors, modern driver default
Tasklet (legacy)Softirq contextNoOlder drivers; being phased out in favor of threaded IRQs
WorkqueueKernel worker threadYesWork that needs to run later, possibly with delay
SoftirqAtomic contextNoVery 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_ATOMIC repeatedly, 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‘s irqsoff tracer 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.

Continue Learning Linux Device Drivers

This lesson is part of EmbeddedPathashala’s free Linux kernel programming and device drivers course.

Next Lecture →

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *