« Previous Lecture | Next Lecture »
If you are learning Linux device driver development, sooner or later you will hit a wall with plain hardware interrupt handlers: they run with interrupts disabled, they cannot sleep, and they cannot call anything that might block. This is exactly the problem a threaded interrupt handler in the Linux kernel solves. In this free Linux kernel programming course lecture, we break down how threaded IRQs work in modern kernels, why the genirq subsystem was designed this way, and how you can write your own threaded interrupt driver today.
What You Will Learn
- The difference between a hardirq (primary handler) and a threaded handler
- Why the genirq subsystem introduced threaded interrupt handlers
- How
request_threaded_irq()anddevm_request_threaded_irq()work on kernel 6.x - What
IRQF_ONESHOTreally does and when you must use it - How the kernel internally creates and manages the per-IRQ kernel thread
- A complete, original threaded IRQ driver example for a GPIO interrupt line
- Common mistakes, best practices, and performance/security considerations
Prerequisites
Before this lecture, you should be comfortable with basic character/platform driver structure, the concept of interrupt context versus process context, and how request_irq() is used for a simple hardirq-only handler. If any of this feels new, revisit our earlier lecture on hardware interrupt basics in this free Linux device drivers course before continuing.
Hardirq vs Threaded Handler: The Core Idea
A traditional interrupt handler (the “hardirq” or primary handler) runs immediately when the IRQ line fires. It executes atomically, with that interrupt line masked, and it cannot sleep. This makes it fast, but also dangerous to write long or blocking code inside — for example, an I2C or SPI register read that can take microseconds to milliseconds simply does not belong in a hardirq.
A threaded interrupt handler splits the work into two parts: a short primary handler that runs in interrupt context, and a “thread function” that runs later as a normal, schedulable kernel thread in process context. The primary handler decides whether the thread function needs to run at all.
interrupt context
quick check, returns IRQ_WAKE_THREAD
process context, schedulable
can sleep, do I2C/SPI I/O, returns IRQ_HANDLED
Why Use Threaded Interrupts?
There are three practical reasons driver authors reach for threaded IRQs instead of plain hardirqs:
| Reason | What it means in practice |
|---|---|
| Real-time responsiveness | Long hardirq handlers block every other interrupt and every real-time thread on the system. Moving work to a thread keeps worst-case latency predictable. |
| Fewer softirq/tasklet workarounds | Instead of deferring work to a tasklet or workqueue by hand, the kernel gives you a ready-made, priority-controlled thread per IRQ. |
| Can sleep and do blocking I/O | Threaded handlers run in process context, so they can call regmap/I2C/SPI functions, take mutexes, or sleep — none of which is legal in a hardirq. |
As a rule of thumb: if handling the interrupt is likely to take longer than roughly 100 microseconds, or it needs to call a sleeping API, use the threaded interrupt model.
Registering a Threaded IRQ: Modern Kernel 6.x API
On kernel 6.x, the recommended API for a driver that will be cleaned up automatically at device unbind time is devm_request_threaded_irq(). It takes both a primary handler and a thread function:
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long irqflags,
const char *devname, void *dev_id);
If you pass NULL as the primary handler, the genirq core substitutes its own default primary handler, which simply returns IRQ_WAKE_THREAD and immediately schedules your thread function. This is the most common pattern for simple GPIO-triggered interrupts.
Original Example: A Threaded GPIO Button Driver
The following is an original, from-scratch example (not taken from any book) showing a modern platform driver that debounces a button using a threaded IRQ:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/delay.h>
struct ep_button {
struct gpio_desc *gpiod;
int irq;
};
static irqreturn_t ep_button_thread_fn(int irq, void *dev_id)
{
struct ep_button *btn = dev_id;
/* Safe to sleep here: process context */
msleep(20); /* simple debounce delay */
dev_info(NULL, "ep_button: pressed, gpio value=%d\n",
gpiod_get_value(btn->gpiod));
return IRQ_HANDLED;
}
static int ep_button_probe(struct platform_device *pdev)
{
struct ep_button *btn;
int ret;
btn = devm_kzalloc(&pdev->dev, sizeof(*btn), GFP_KERNEL);
if (!btn)
return -ENOMEM;
btn->gpiod = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(btn->gpiod))
return PTR_ERR(btn->gpiod);
btn->irq = gpiod_to_irq(btn->gpiod);
if (btn->irq irq;
ret = devm_request_threaded_irq(&pdev->dev, btn->irq,
NULL, ep_button_thread_fn,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"ep-button", btn);
if (ret)
return ret;
platform_set_drvdata(pdev, btn);
return 0;
}
static struct platform_driver ep_button_driver = {
.probe = ep_button_probe,
.driver = {
.name = "ep-button",
},
};
module_platform_driver(ep_button_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala threaded IRQ example driver");
Notice two important details that trip up beginners:
- The primary handler is
NULL, so the kernel’s default primary handler wakes the thread on every rising edge. IRQF_ONESHOTis mandatory whenever the primary handler isNULL. It keeps the IRQ line masked until the thread function finishes, preventing the same edge from re-triggering the handler before debouncing completes.
How the Kernel Creates the IRQ Thread Internally
When you call request_threaded_irq() (or its devm variant), the genirq core in kernel/irq/manage.c allocates an irq_desc, and — the first time a thread function is registered for that IRQ — spawns a dedicated kernel thread using kthread_create(). That thread is named using the pattern irq/<irq-number>-<devname>, which is exactly what you will see if you run ps -eLf | grep irq/ on a running system.
This thread is scheduled with the SCHED_FIFO real-time policy at a default real-time priority of 50 out of the usable 1–99 range. It sleeps until the primary handler (or the default one) wakes it, runs your thread function to completion, and goes back to sleep. When the driver calls free_irq() (or the devm cleanup runs), the kernel stops and destroys this thread automatically.
What Changed by Kernel 6.x: Forced IRQ Threading and PREEMPT_RT
The mechanics described above have been stable since threaded IRQs were introduced, but two things are worth updating from older references:
- PREEMPT_RT is mainline. As of the kernel 6.12 series, the real-time preemption patches were merged into mainline Linux. On a kernel built with
CONFIG_PREEMPT_RT, most hardirq handlers are force-threaded by the genirq core automatically, even if the driver only registered a plain hardirq handler — this is controlled viaCONFIG_IRQ_FORCED_THREADINGand thethreadirqsboot parameter. - You can inspect and tune thread priority from userspace. On any modern kernel, the priority of a given IRQ thread can be changed at runtime using standard scheduler tools, for example
chrt -f -p <priority> <pid-of-irq-thread>, which is useful when tuning real-time systems without recompiling drivers.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Forgetting IRQF_ONESHOT with a NULL primary handler | request_threaded_irq() returns -EINVAL | Always pair NULL primary handler with IRQF_ONESHOT |
| Sleeping in the primary handler | Kernel warnings, possible system lockups | Only sleep inside the thread function, never the primary handler |
| Assuming the thread runs immediately | Unexpected latency under load | Remember it competes with other SCHED_FIFO tasks at the same/higher priority |
Best Practices
- Keep the primary handler down to a hardware status check and a return value — nothing else.
- Use
devm_request_threaded_irq()in new drivers so cleanup is automatic on probe failure or unbind. - Guard shared data between the primary handler and thread function with appropriate locking (spinlocks are fine in the primary handler; mutexes are fine in the thread).
Performance and Security Considerations
Performance: Because the hardware IRQ line stays enabled while a threaded handler runs (unless IRQF_ONESHOT is set), threaded IRQs generally improve system responsiveness compared to long hardirqs, at the cost of a small scheduling latency before the thread actually runs.
Security: Since the thread function runs in process context with real-time priority, a buggy or malicious thread function that spins forever can starve lower-priority userspace tasks. Always bound loops and avoid unbounded waits inside a thread function.
Real-World Use Cases
- I2C/SPI-connected sensors and touch controllers that must read registers after an interrupt
- GPIO buttons and rotary encoders needing debounce logic
- Network and storage controllers that batch work after a hardware event
Key Takeaways
- A threaded interrupt handler splits work into a fast primary handler and a schedulable thread function.
devm_request_threaded_irq()is the modern, kernel 6.x recommended API.IRQF_ONESHOTis required whenever the primary handler is NULL.- The kernel creates a real-time
SCHED_FIFOthread namedirq/N-namefor every threaded IRQ. - PREEMPT_RT, now mainline as of the 6.12 series, can force-thread ordinary hardirqs system-wide.
Frequently Asked Questions
1. What is a threaded interrupt handler in the Linux kernel?
It is an interrupt handling model where a short primary handler runs in interrupt context and hands off the real work to a dedicated kernel thread that runs in process context, so it is allowed to sleep and do blocking I/O.
2. When should I use request_threaded_irq() instead of request_irq()?
Use it whenever your interrupt handling needs to call sleeping APIs (I2C, SPI, mutexes) or is likely to take longer than about 100 microseconds.
3. Why is IRQF_ONESHOT required with a NULL primary handler?
Without a custom primary handler, the kernel’s default primary handler always wakes the thread. IRQF_ONESHOT keeps the IRQ line masked until the thread finishes, preventing re-entrant triggering.
4. What priority does the IRQ thread run at by default?
It runs under the SCHED_FIFO real-time policy at a default real-time priority of 50 (out of 1–99).
5. Can I change the IRQ thread’s priority at runtime?
Yes, using standard scheduler tools such as chrt against the thread’s PID, without modifying the driver.
6. Does PREEMPT_RT change how threaded IRQs work?
On a PREEMPT_RT kernel, most ordinary hardirq handlers are force-threaded automatically, even without explicit driver support, improving real-time determinism system-wide.
7. Is devm_request_threaded_irq() safe to use in probe()?
Yes, it is the recommended approach since the IRQ and its thread are automatically released if probe fails later or the device is unbound.
8. What happens to the IRQ thread when the driver is removed?
The kernel stops and destroys the thread automatically when free_irq() runs (directly or via the devm cleanup path).
Conclusion
Threaded interrupt handlers are one of the most practical tools in a Linux device driver author’s toolbox: they let you keep hardirq handlers tiny and fast while still doing real work — including blocking I/O — in a properly scheduled kernel thread. Once you are comfortable with devm_request_threaded_irq(), IRQF_ONESHOT, and how the underlying irq/N-name thread is scheduled, you are ready to move on to real-time priority tuning of interrupt threads, which is the subject of our next lecture in this free Linux kernel development course.
Continue your free Linux kernel programming journey with EmbeddedPathashala.
Browse the Free Course Watch on YouTube
2 Comments