Handling Hardware Interrupts in Linux Device Drivers (Part 2): Handlers, Threaded IRQs & Bottom Halves
This is Part 2 of our lesson on handling hardware interrupts in Linux device drivers, from the EmbeddedPathashala free Linux device drivers course. In Part 1 you learned what interrupts are, how the kernel delivers them, and how to allocate an IRQ line. Now we get practical: you will learn how to write the interrupt handler routine itself, the strict rules of interrupt context, the modern threaded interrupt model, how to enable and disable IRQs safely, how to inspect interrupt lines through /proc, and how top and bottom halves split the work. This is essential material for anyone taking a free Linux kernel development course or a free embedded systems course.
What You Will Learn
- How to write a correct interrupt handler routine and what it must never do.
- The dos and don’ts of running in hardware interrupt (atomic) context.
- The threaded interrupt model with
request_threaded_irq()and why it exists. - How to enable and disable specific IRQ lines safely.
- How to view allocated interrupt lines using
/proc/interrupts. - What top halves and bottom halves are, and the modern deferral mechanisms.
Prerequisites
You should have completed Part 1 of this lesson, understand how to allocate an IRQ with request_irq(), and be comfortable writing and loading a basic kernel module. A recent 6.x kernel on a test VM or board is recommended for hands-on practice.
Implementing the Interrupt Handler Routine
The heart of handling hardware interrupts in Linux device drivers is the handler function. This is the routine the kernel calls the moment your device’s interrupt fires. Its signature must match the type introduced in Part 1: it takes the interrupt number and your registered cookie, and returns an irqreturn_t.
static irqreturn_t my_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* Is this interrupt really from our device? */
if (!device_interrupt_pending(dev))
return IRQ_NONE; /* not ours, let others check */
/* Acknowledge the interrupt at the device so it stops asserting */
device_ack_interrupt(dev);
/* Do the minimum urgent work here, then return */
handle_device_event(dev);
return IRQ_HANDLED;
}
Notice the structure: first confirm the interrupt is yours, then acknowledge it in hardware so the line quiets down, then do only the most urgent work. The functions device_interrupt_pending(), device_ack_interrupt(), and handle_device_event() above are placeholders for whatever your specific hardware requires.
The Golden Rules of Interrupt Context
A standard (non-threaded) handler runs in hardware interrupt context, also called atomic context. This context has hard restrictions. Breaking them can freeze or crash the system, so memorize them:
DO
|
DON’T
|
The Threaded Interrupt Model
Some devices sit on slow buses. Reading a register from an I2C or SPI peripheral can take milliseconds, which is far too long to do in atomic interrupt context. To solve this, the kernel provides threaded interrupts. Instead of doing all the work in atomic context, you split it into a small primary handler and a larger threaded handler that runs in ordinary process context, where sleeping is allowed.
You register a threaded interrupt with request_threaded_irq():
#include <linux/interrupt.h>
int request_threaded_irq(unsigned int irq,
irq_handler_t handler, /* primary, hardirq context */
irq_handler_t thread_fn, /* threaded, process context */
unsigned long flags,
const char *name,
void *dev);
The two handlers cooperate. The primary handler runs first in atomic context and does a quick check. If the interrupt belongs to your device, it silences the device and returns IRQ_WAKE_THREAD. The kernel then schedules a dedicated kernel thread (visible as irq/N-name) that runs thread_fn in process context, where it is safe to sleep.
|
Primary handler Atomic context · fast quick check · ack device returns IRQ_WAKE_THREAD |
→ |
Threaded handler (thread_fn) Process context · may sleep I2C/SPI reads · mutexes heavy processing |
Here is a minimal shape showing both halves working together:
static irqreturn_t my_primary(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
if (!device_interrupt_pending(dev))
return IRQ_NONE;
/* Silence the device so the line does not keep firing */
device_mask_interrupt(dev);
return IRQ_WAKE_THREAD; /* let the thread do the slow work */
}
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* Safe to sleep here: bus reads, mutexes, etc. */
read_sensor_over_i2c(dev);
process_data(dev);
/* Re-enable the device interrupt */
device_unmask_interrupt(dev);
return IRQ_HANDLED;
}
Registration then looks like this:
ret = request_threaded_irq(dev->irq,
my_primary,
my_thread_fn,
IRQF_ONESHOT | IRQF_TRIGGER_LOW,
"my-device",
dev);
if (ret)
return ret; /* handle the error */
IRQF_ONESHOT. It keeps the line masked until the threaded handler completes. Without it, the interrupt can fire again before your thread clears the source, producing an interrupt storm that hangs the system.
| Use a plain (hardirq) handler when… | Use a threaded handler when… |
|---|---|
| Work is tiny: read a register, copy data, acknowledge. | You must read a slow bus such as I2C or SPI. |
| You have hard real-time latency needs. | You must take a mutex or otherwise sleep. |
| Network receive using NAPI deferral. | Processing takes more than a few microseconds. |
Enabling and Disabling IRQs
Sometimes a driver needs to temporarily stop a specific interrupt from firing, for example while reconfiguring the device. The kernel provides simple helpers for a single IRQ line:
void disable_irq(unsigned int irq); /* waits for any running handler to finish */
void disable_irq_nosync(unsigned int irq); /* returns immediately, does not wait */
void enable_irq(unsigned int irq); /* re-enables the line */
| Function | Behavior |
|---|---|
disable_irq() | Disables the line and waits until any in-progress handler completes. Safe but may block. |
disable_irq_nosync() | Disables the line and returns at once without waiting. Use with care. |
enable_irq() | Re-enables a previously disabled line. Calls must be balanced. |
disable_irq() must be matched by an enable_irq(). Never call disable_irq() (the syncing variant) from within your own handler for the same line, because it would wait for itself and deadlock.
Viewing Allocated Interrupt Lines via /proc
To see which interrupts are registered and how often they have fired, read the special file /proc/interrupts. This is one of the most useful debugging tools when handling hardware interrupts in Linux device drivers.
cat /proc/interrupts
Each row represents an interrupt line. The first column is the IRQ number, the following columns show the count of interrupts handled per CPU, and the final columns identify the interrupt controller and the name you passed to request_irq(). Watching a count rise as you exercise your device confirms your handler is actually running. A count that never moves usually means the wrong number, the wrong trigger flag, or an interrupt that is not enabled in hardware.
| IRQ # | Count on CPU0 | Count on CPU1 | Controller | Device Name |
|---|---|---|---|---|
| the assigned number | rises as it fires | rises as it fires | GIC / APIC / MSI | your name string |
Understanding Top Halves and Bottom Halves
Because an interrupt handler must be fast and must not sleep, the kernel splits interrupt work into two conceptual pieces. The top half is the part that runs immediately in interrupt context: it acknowledges the hardware and does the minimum urgent work. The bottom half is the part that runs later, at a more convenient time, to finish the heavier processing.
|
Top Half Runs now, in interrupt context fast · cannot sleep ack hardware, schedule bottom half |
→ |
Bottom Half Runs later, at a convenient time heavier processing runs with interrupts enabled |
Linux offers several mechanisms to implement a bottom half. Choosing the right one depends on whether the deferred work needs to sleep and how urgent it is.
| Mechanism | Context | Can Sleep? |
|---|---|---|
Threaded IRQ (thread_fn) | Process (kernel thread) | Yes |
| Workqueue | Process (kernel thread) | Yes |
| Softirq | Atomic | No |
| Tasklet | Atomic | No |
Performance Considerations
- Keep the top half minimal; every microsecond spent there adds latency to the whole system.
- Prefer threaded handlers or workqueues for slow work so you do not hold off other interrupts.
- Batch processing where possible; for network receive, NAPI polling reduces per-packet interrupt overhead.
- Watch
/proc/interruptsto spot lines firing far more often than expected.
Best Practices
- Confirm the interrupt is yours before touching shared state; return
IRQ_NONEwhen it is not. - Acknowledge the device early so the line stops asserting.
- Use
IRQF_ONESHOTfor level-triggered threaded interrupts to prevent storms. - Balance every
disable_irq()with anenable_irq(). - Protect data shared between the handler and the rest of the driver with the correct locks (spinlocks in atomic context).
- Always release the line with
free_irq()during cleanup.
Common Mistakes and Troubleshooting
| Symptom | Likely Cause |
|---|---|
| System hangs when the interrupt fires | Sleeping inside a hardirq handler, or missing IRQF_ONESHOT on a level-triggered threaded IRQ. |
| Deadlock while disabling an interrupt | Calling the synchronous disable_irq() from inside the same line’s own handler. |
| Kernel warns “irq handler enabled interrupts” | Re-enabling interrupts improperly inside the handler. |
| Data corruption between handler and driver | Missing or wrong locking for shared state. |
Key Takeaways
- A standard handler runs in atomic context and must be fast and must never sleep.
- Use
request_threaded_irq()to move slow, sleeping work into a kernel thread. - Always use
IRQF_ONESHOTfor level-triggered threaded interrupts. - Enable and disable individual lines with balanced
enable_irq()/disable_irq()calls. - Inspect live interrupt activity through
/proc/interrupts. - Split work into a top half (now, atomic) and a bottom half (later); threaded IRQs and workqueues are the modern go-to.
Conclusion
You have now completed the practical side of handling hardware interrupts in Linux device drivers. You can write a correct handler, respect the strict rules of interrupt context, split slow work into a threaded handler with request_threaded_irq(), manage individual IRQ lines, read /proc/interrupts for debugging, and choose the right bottom-half mechanism. Together with Part 1, this gives you a complete, modern foundation for interrupt-driven driver development. Keep practicing on real hardware or a virtual machine, and continue with the EmbeddedPathashala free Linux kernel development course and free embedded systems course to build on these skills.
Frequently Asked Questions (FAQ)
Q1. Can an interrupt handler sleep in Linux?
A standard hardirq handler must never sleep. If your work needs to sleep, use a threaded handler registered with request_threaded_irq(), whose thread_fn runs in process context where sleeping is allowed.
Q2. What is the difference between a top half and a bottom half?
The top half runs immediately in interrupt context and does only urgent work. The bottom half runs later, at a convenient time with interrupts enabled, to finish heavier processing.
Q3. When should I use IRQF_ONESHOT?
Use it with level-triggered threaded interrupts. It keeps the line masked until the threaded handler finishes, preventing the interrupt from re-firing and causing a storm.
Q4. How do I see if my interrupt is actually firing?
Read /proc/interrupts. Find the row with your device name and watch the per-CPU counters rise as you exercise the hardware.
Q5. What is the difference between disable_irq and disable_irq_nosync?
disable_irq() disables the line and waits for any running handler to finish, which is safer but can block. disable_irq_nosync() disables and returns immediately without waiting.
Q6. Which bottom-half mechanism should a beginner choose?
For most new drivers, a threaded interrupt handler or a workqueue is the cleanest option when the deferred work may sleep. Softirqs are for core subsystems, and tasklets are legacy.
Q7. Why does my handler need to return IRQ_NONE sometimes?
On a shared interrupt line, several devices use the same number. Returning IRQ_NONE when the interrupt is not yours lets the kernel offer it to the other registered handlers.

2 Comments