If you are learning interrupt handling in Linux device drivers, one of the most confusing questions beginners face is this: should your interrupt handler run in hard IRQ context, or should it be deferred to a kernel thread? Modern Linux kernels (6.x series) give driver authors a clean, unified answer through the IRQ allocation APIs. In this lecture, part of our free Linux kernel programming course, we break this down using simple language, our own original code examples, and easy-to-follow diagrams.
Prerequisites
- Basic understanding of Linux kernel modules (insmod/rmmod)
- Familiarity with C programming
- A Linux system running kernel 6.x with headers installed (a Raspberry Pi, BeagleBone, or QEMU virtual machine works fine)
- Basic knowledge of GPIO or platform devices is helpful but not mandatory
Why Interrupt Handling in Linux Device Drivers Needs Two Contexts
When a hardware device raises an interrupt, the CPU immediately jumps to your driver’s interrupt handler. This happens in an atomic context — interrupts on that CPU core are effectively frozen, and your handler cannot sleep, cannot take a mutex, and cannot call any function that might block. This is called a hardirq context.
But many real devices sit behind slow buses such as I2C or SPI. Reading a sensor’s status register over I2C requires the kernel to sleep while waiting for the bus transaction to finish. You simply cannot do that inside a hardirq handler. This is exactly the problem interrupt handling in Linux device drivers had to solve, and the solution is the threaded interrupt handler.
Runs immediately. Cannot sleep. Must be extremely fast. Good for reading a status register from memory-mapped I/O.
Runs in a dedicated kernel thread. Can sleep. Good for I2C/SPI transactions and heavier processing.
request_threaded_irq(): The Standard Way
Most modern drivers register both a hardirq handler and a threaded handler in one call using request_threaded_irq(). The hardirq part does the bare minimum (acknowledge the interrupt), and the threaded part does the heavy lifting.
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
/* Quick hardirq handler: just wake up the thread */
static irqreturn_t ep_sensor_hardirq(int irq, void *dev_id)
{
struct ep_sensor_dev *sdev = dev_id;
if (!ep_sensor_irq_pending(sdev))
return IRQ_NONE;
return IRQ_WAKE_THREAD; /* hand off to the threaded handler */
}
/* Threaded handler: allowed to sleep, do I2C reads here */
static irqreturn_t ep_sensor_threaded(int irq, void *dev_id)
{
struct ep_sensor_dev *sdev = dev_id;
int temperature;
temperature = ep_sensor_read_i2c_reg(sdev, EP_SENSOR_TEMP_REG);
dev_info(sdev->dev, "New temperature reading: %d\n", temperature);
return IRQ_HANDLED;
}
static int ep_sensor_probe(struct i2c_client *client)
{
struct ep_sensor_dev *sdev;
int ret;
sdev = devm_kzalloc(&client->dev, sizeof(*sdev), GFP_KERNEL);
if (!sdev)
return -ENOMEM;
sdev->dev = &client->dev;
ret = devm_request_threaded_irq(&client->dev, client->irq,
ep_sensor_hardirq,
ep_sensor_threaded,
IRQF_ONESHOT,
"ep_sensor_irq", sdev);
if (ret)
dev_err(sdev->dev, "failed to request threaded irq: %d\n", ret);
return ret;
}
IRQF_ONESHOT is required whenever you pass a NULL hardirq handler or use a two-stage handler on a shared line, because it keeps the IRQ line masked until the threaded handler finishes. This avoids interrupt storms on slow-bus devices.
request_any_context_irq(): Letting the Kernel Decide
Sometimes a driver is written to be generic across many platforms — the same driver might be wired to a fast, memory-mapped GPIO controller on one board and to a slow I2C-based GPIO expander on another. In kernel 6.x, request_any_context_irq() solves exactly this portability problem. The kernel inspects the underlying interrupt controller and automatically decides whether your handler should run as a hardirq handler or as a nested (threaded) handler.
#include <linux/interrupt.h>
static irqreturn_t ep_keypad_irq(int irq, void *dev_id)
{
struct ep_keypad_dev *kdev = dev_id;
ep_keypad_scan_matrix(kdev); /* may sleep on I2C-backed GPIO expanders */
return IRQ_HANDLED;
}
static int ep_keypad_setup_irq(struct ep_keypad_dev *kdev, int irq)
{
int ret;
ret = request_any_context_irq(irq, ep_keypad_irq,
IRQF_TRIGGER_FALLING,
"ep_keypad_irq", kdev);
if (ret < 0) {
pr_err("ep_keypad: irq request failed: %d\n", ret);
return ret;
}
if (ret == IRQC_IS_NESTED)
pr_info("ep_keypad: running as a nested (threaded) handler\n");
else
pr_info("ep_keypad: running as a hardirq handler\n");
return 0;
}
Return Value Reference
| Return Value | Meaning |
|---|---|
| IRQC_IS_HARDIRQ | Handler will run in atomic hardirq context. |
| IRQC_IS_NESTED | Handler will run in a sleep-capable, threaded (nested) context. |
| Negative errno | Request failed; always check this before proceeding. |
Real-World Use Cases
- GPIO-driven matrix keypads wired through I2C GPIO expanders, where the same driver code must also work on native SoC GPIO lines.
- Touchscreen controllers connected over I2C or SPI that need to read multiple registers per interrupt.
- Generic input drivers shipped as loadable modules that must run correctly across many different boards without recompilation.
Common Mistakes
- Forgetting to check the return value. A negative errno silently breaks your driver if ignored.
- Doing blocking I/O in a plain request_irq() handler. This will trigger a kernel warning (“scheduling while atomic”) or crash.
- Not using IRQF_ONESHOT with request_threaded_irq() on shared or edge-triggered lines, causing interrupt storms.
- Assuming the context is always the same across boards — this is precisely why request_any_context_irq() exists.
Best Practices
| Practice | Why It Matters |
|---|---|
| Use devm_request_threaded_irq() | Automatically frees the IRQ when the device is removed, preventing leaks. |
| Keep the hardirq handler minimal | Reduces latency for other interrupts on the same core. |
| Log the context on probe (as shown above) | Makes debugging portability issues across boards much easier. |
Performance and Security Considerations
Threaded handlers add a small scheduling latency compared to raw hardirq handlers because the kernel has to wake a thread. For most sensor and input drivers this is irrelevant, but for high-frequency devices such as network interface cards, this latency is unacceptable — which is why NICs still prefer the classic hardirq top-half plus softirq/tasklet bottom-half design instead. From a security standpoint, always validate any data read inside your handler before acting on it; an interrupt handler is a very easy place for a compromised or faulty peripheral to feed malformed data into the kernel.
Summary / Key Takeaways
- Hardirq handlers must never sleep; threaded handlers can.
- request_threaded_irq() is the standard two-stage API most drivers use today.
- request_any_context_irq() is for portable drivers where the required context depends on the underlying interrupt controller.
- Always check the return value to know which context your handler ended up in.
Conclusion
Understanding the difference between hardirq and threaded interrupt handling is one of the foundational skills in interrupt handling in Linux device drivers. Once you’re comfortable with request_threaded_irq() and request_any_context_irq(), you’ll be able to write drivers that behave correctly on both fast, memory-mapped hardware and slow, bus-connected peripherals — without changing a single line of code. In the next lecture of this free Linux kernel programming course, we will look at how to selectively enable and disable individual IRQ lines from within your driver.
FAQ
Q1. What is the difference between request_irq() and request_threaded_irq()?
request_irq() registers only a hardirq handler that cannot sleep. request_threaded_irq() registers both a quick hardirq handler and a sleep-capable threaded handler.
Q2. When should I use request_any_context_irq()?
Use it when writing a generic driver that must work correctly regardless of whether the underlying interrupt controller requires atomic or sleep-capable handling, such as GPIO-based drivers.
Q3. Can a threaded interrupt handler access user-space memory?
No. Threaded handlers still run in kernel context; they can sleep and use blocking kernel APIs, but they cannot directly touch user-space memory.
Q4. What happens if I forget IRQF_ONESHOT with a threaded handler?
On a shared or edge-triggered line, the hardware interrupt line may remain active and re-trigger before the threaded handler runs, leading to an interrupt storm.
Q5. Is IRQC_IS_NESTED the same as a softirq?
No. A nested/threaded handler runs in its own kernel thread context, while softirqs run in a separate, non-thread software interrupt context. They serve different purposes.
Q6. Why do NICs avoid threaded handlers?
High-throughput devices like network cards need the lowest possible latency, and the scheduling overhead of waking a thread per interrupt would hurt performance, so they use hardirq + softirq/tasklet instead.
Q7. Does request_any_context_irq() work on all kernel versions?
It has been available in mainline Linux for many years and continues to be supported in the 6.x series, though it is exported GPL-only, so it can only be used by GPL-compatible modules.
This lecture is part of EmbeddedPathashala’s free Linux kernel programming and device drivers course.
Visit EmbeddedPathashala Next Lecture →
2 Comments