← Previous Lecture Next Lecture →
Every Linux device driver that responds to hardware events has to deal with interrupts. But real hardware interrupt handling is rarely a single quick function — it often needs to talk to slow buses like I2C or SPI, which means it needs to sleep. That is exactly the problem threaded interrupt handlers were built to solve.
In this free Linux kernel programming lecture, we break down threaded interrupt handlers in plain language, show you the difference between hard interrupt context and a kernel thread, and walk through a simple, original, modern kernel example you can build on for your own free Linux device drivers course project.
What You Will Learn
- Why a plain hardware interrupt handler is not always enough for a Linux device driver
- What a threaded interrupt handler actually is, in beginner-friendly terms
- The exact job of the primary handler versus the threaded handler
- What
IRQ_WAKE_THREADandIRQF_ONESHOTreally do internally - A simple, original threaded interrupt code example for a modern kernel
- Common mistakes beginners make when writing threaded interrupt handlers
Prerequisites
Before this lecture, you should be comfortable with:
- Basic Linux kernel module programming (
module_init,module_exit) - The idea of a hardware interrupt and an IRQ number
- Basic C programming inside the kernel (pointers, structures)
Why Do We Need Threaded Interrupt Handlers?
A hardware interrupt handler normally runs in what the kernel calls hard interrupt context. This context has strict rules: it must be fast, it cannot sleep, and it cannot take a mutex or perform any operation that might block, such as reading a register over I2C or SPI.
But many real devices — touchscreens, sensors, audio codecs — are connected over I2C or SPI. Reading their status registers takes time and may sleep while waiting for the bus. Doing that work directly inside a hard interrupt handler is not allowed, because it would stall the entire system.
The kernel’s answer to this problem is the threaded interrupt handler. It splits the work into two parts that run in two different contexts.
|
1. Hardware fires IRQ Device raises interrupt line |
→ |
2. Primary Handler Runs in hardirq context, checks device, returns IRQ_WAKE_THREAD
|
→ |
3. Kernel Wakes Thread Thread named irq/N-name is scheduled
|
→ |
4. Threaded Handler Runs in process context, may sleep, does real work |
The Primary Handler: What Its Job Really Is
The first function you pass to request_threaded_irq() is the primary handler. It still runs in hard interrupt context, so the same restrictions apply: keep it fast, do not sleep. Its only three jobs are:
- Confirm the interrupt actually belongs to your device (return
IRQ_NONEif not, mainly needed for shared lines) - Quiet the interrupt source at the hardware level so it doesn’t keep firing
- Return
IRQ_WAKE_THREADto hand off the real work to the threaded handler
If you don’t need this quick check at all, you can pass NULL as the primary handler. The kernel then installs a default primary handler that simply wakes your thread every time the interrupt fires.
The Threaded Handler: Where the Real Work Happens
The threaded handler runs as a dedicated kernel thread, in normal process context. This is where it is safe to:
- Read or write registers over I2C or SPI
- Take a mutex
- Do any processing that takes more than a few microseconds
You can watch these threads on a running system:
$ ps -eLo pid,comm | grep "irq/"
188 irq/27-mmc0
191 irq/45-touchscreen
Understanding IRQF_ONESHOT
This is the flag beginners get wrong most often. Normally, once the primary handler finishes, the kernel re-enables the interrupt line right away — even before your threaded handler has run. For a level-triggered interrupt, that is dangerous: if the device is still asserting the line because it hasn’t been serviced yet, the interrupt fires again immediately, and again, and again. This is called an interrupt storm.
IRQF_ONESHOT tells the kernel: keep the interrupt line masked until the threaded handler has completely finished. Only then does the kernel unmask it. This guarantees exactly one clean handling cycle per event.
|
Without IRQF_ONESHOT
Line unmasked right after primary handler → device still asserting it → interrupt fires again before thread runs → storm |
With IRQF_ONESHOT
Line stays masked → thread clears the source → kernel unmasks line only after thread finishes → no storm |
Because of this risk, the kernel enforces the flag: if you supply a threaded handler along with the default (NULL) primary handler, the kernel silently forces IRQF_ONESHOT on for you. If you supply your own primary handler and forget the flag on a level-triggered line, the request can fail outright.
A Simple Original Example: GPIO Button on a Modern Kernel
Below is a small, original threaded interrupt example written for current (6.x) kernels using the modern gpiod descriptor-based GPIO API. It is deliberately kept simple for a beginner-friendly course.
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
struct my_button_dev {
struct gpio_desc *gpiod;
int irq;
};
/* Primary handler: hardirq context, must be fast */
static irqreturn_t button_primary_handler(int irq, void *data)
{
struct my_button_dev *btn = data;
if (!btn)
return IRQ_NONE;
/* nothing to quiet on a simple GPIO line, just wake the thread */
return IRQ_WAKE_THREAD;
}
/* Threaded handler: process context, may sleep */
static irqreturn_t button_thread_fn(int irq, void *data)
{
struct my_button_dev *btn = data;
int value = gpiod_get_value_cansleep(btn->gpiod);
pr_info("my_button: press event, line value = %d\n", value);
/* debounce-friendly delay is safe here because we are in process context */
msleep(20);
return IRQ_HANDLED;
}
static int my_button_probe(struct platform_device *pdev)
{
struct my_button_dev *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 < 0)
return btn->irq;
ret = request_threaded_irq(btn->irq, button_primary_handler,
button_thread_fn,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"my_button", btn);
if (ret) {
dev_err(&pdev->dev, "failed to request threaded irq: %d\n", ret);
return ret;
}
platform_set_drvdata(pdev, btn);
return 0;
}
static void my_button_remove(struct platform_device *pdev)
{
struct my_button_dev *btn = platform_get_drvdata(pdev);
free_irq(btn->irq, btn);
}
static struct platform_driver my_button_driver = {
.probe = my_button_probe,
.remove = my_button_remove,
.driver = {
.name = "my_button",
},
};
module_platform_driver(my_button_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple threaded interrupt GPIO button example");
What’s happening: the primary handler runs instantly and just requests a wake-up. The real work — reading the GPIO value and applying a small debounce delay — happens safely in the threaded handler, in process context, where sleeping is allowed.
The threadirqs Kernel Boot Parameter
Linux also supports a kernel command-line option, threadirqs, which forces almost all interrupt handlers on the system to run threaded, except those explicitly marked with IRQF_NO_THREAD. This is mainly used for real-time (PREEMPT_RT) systems, where keeping hard interrupt context as short as possible is critical for predictable latency.
# Example: adding threadirqs via GRUB kernel command line
GRUB_CMDLINE_LINUX="threadirqs"
Common Mistakes and Troubleshooting
- Forgetting IRQF_ONESHOT on a level-triggered interrupt — causes request failure or interrupt storms
- Sleeping inside the primary handler — will trigger kernel warnings or a crash, since hardirq context cannot sleep
- Not clearing the interrupt source at the hardware level before returning
IRQ_WAKE_THREAD, leading to repeated firing - Returning IRQ_HANDLED instead of IRQ_WAKE_THREAD from the primary handler by mistake, which means the thread function never runs
Best Practices
- Keep the primary handler as short as physically possible
- Always use
IRQF_ONESHOTfor level-triggered threaded interrupts - Use a descriptive
devnameso the thread name inps aux | grep irq/is easy to identify while debugging - Prefer the NULL primary handler when there is nothing meaningful to check in hardirq context
Key Takeaways
- Threaded interrupt handlers split work into a fast primary handler and a sleep-capable threaded handler
IRQ_WAKE_THREADis the return value that hands off work from hardirq context to the kernel threadIRQF_ONESHOTprevents interrupt storms on level-triggered lines by keeping the line masked until the thread finishes- The
threadirqsboot parameter forces threading system-wide for real-time predictability
FAQ
Q1. What is the difference between request_irq() and request_threaded_irq() in the Linux kernel?
request_irq() registers a single handler that always runs in hard interrupt context. request_threaded_irq() registers two handlers: a fast primary handler in hardirq context, and a threaded handler that runs in process context and can sleep.
Q2. Can the primary handler be NULL in a threaded interrupt handler?
Yes. If you pass NULL, the kernel installs a default primary handler that simply wakes your threaded handler, and it automatically applies IRQF_ONESHOT.
Q3. Why is IRQF_ONESHOT mandatory for level-triggered threaded interrupts?
Without it, the interrupt line is re-enabled right after the primary handler finishes, before the device has actually been serviced, which can cause the interrupt to fire again immediately in a storm.
Q4. Can a threaded interrupt handler sleep?
Yes, the threaded handler runs in normal process context, so it can sleep, take mutexes, and perform I2C or SPI transfers safely.
Q5. What does the kernel name the interrupt thread?
It follows the format irq/IRQ_NUMBER-DEVICE_NAME, visible via ps aux | grep irq/.
Q6. Is threaded interrupt handling only for embedded Linux drivers?
No, it is used throughout the mainline kernel wherever a device (touchscreens, sensors, storage controllers) needs interrupt handling that may require blocking operations.
Q7. What happens if I forget to return IRQ_WAKE_THREAD?
Your threaded handler will simply never run, since that return value is what signals the kernel to wake the thread.
In the next lecture, we cover the managed API devm_request_threaded_irq() and why it is the recommended way to allocate threaded interrupts in modern drivers.

2 Comments