Interrupt Handling in Drivers
Master the complete lifecycle of a hardware interrupt inside a Linux driver — from the electrical signal to request_irq(), the top/bottom-half split, threaded handlers, and the strict rules of atomic context.
Overview
When a hardware device needs the CPU's attention — a key was pressed, a network packet arrived, a timer expired — it does not wait politely in line. It interrupts. The CPU drops whatever it was doing, saves its state, races to a small piece of code called an interrupt handler, and then returns exactly where it left off. This mechanism is one of the most elegant ideas in computer architecture: it lets a single CPU appear to do many things at once, and it is the heartbeat of every device driver you will ever write.
This module teaches you how interrupt handling works from the hardware signal all the way to the Linux kernel's driver API. You will learn why handlers must be blazingly fast, what "top half" and "bottom half" mean and why the split exists, how multiple devices can share a single IRQ line, and how "threaded IRQs" let you write handler code that looks ordinary but runs safely in a kernel thread. Every concept is grounded in real, compilable kernel code.
By the end of this module you will understand the complete lifecycle of an interrupt in a Linux driver: from the moment the device asserts a signal on a wire, through request_irq(), through your handler, through deferred work, to free_irq() during driver removal. You will be able to write interrupt-driven I/O for a real or emulated device, and you will understand every flag and constraint that governs what you are and are not allowed to do inside an interrupt context.
The beauty here is architectural: interrupts are the universe's way of telling software "I have news." Learning to listen correctly — fast enough, safe enough, and without missing anything — is the craft at the heart of systems programming.
Every non-trivial device driver uses interrupts. Polling — spinning in a loop waiting for a device — wastes CPU cycles and adds latency. Interrupt-driven I/O is how real drivers work: the hardware signals when it is ready, the driver responds, and the CPU is free for other work in between. If you cannot write correct interrupt handlers, you cannot write correct drivers. Full stop.
For kernel contribution specifically, interrupt handling is where many subtle bugs live: use of spinlocks vs. mutexes, sleeping in atomic context, missing disable_irq before touching shared state, forgetting free_irq on error paths. Reviewers check these paths meticulously. Mastering this module means you will recognize and fix these bugs in other people's drivers — which is exactly the kind of contribution that builds your reputation in the community.
Interrupt Lifecycle in a Linux Driver
Complete flow from hardware assertion through the top-half handler to the threaded bottom half, showing the two distinct execution contexts (hard-IRQ and kernel thread) and the point at which process-context waiters are woken.
The concepts
01What Is an Interrupt? Hardware to CPU
An interrupt is an electrical signal. A device — say, a UART that just received a byte — asserts a pin on the interrupt controller chip (historically Intel's 8259 PIC, today's x86 systems use the APIC; ARM SoCs use the GIC). The interrupt controller arbitrates, assigns a priority, and raises a dedicated line to the CPU.
The CPU finishes its current instruction (x86 does not interrupt mid-instruction for most instructions), then:
- Saves the current privilege level, instruction pointer, flags register, and (on x86-64) some segment registers onto the kernel stack.
- Looks up the IDT (Interrupt Descriptor Table on x86) or exception vector table (ARM) to find the address of the kernel's first-level handler for that IRQ number.
- Jumps there. The CPU is now in kernel mode, executing the kernel's interrupt entry stub.
Linux's architecture-level entry stub (e.g., arch/x86/entry/entry_64.S) saves the rest of the registers, then calls do_IRQ() (x86) or the GIC driver's handler (ARM), which eventually calls your driver's registered handler.
Think of it like a librarian working at a desk. The librarian is your CPU executing user or kernel code. When the phone rings (interrupt signal), the librarian puts a bookmark in the current page (saves CPU state), picks up the phone (runs the handler), hangs up, and returns to exactly the right page. The key insight: the bookmark must be perfect, and the phone call must be short — other patrons (other processes, other IRQs) are waiting.
/* On x86-64, the IDT entry stub in simplified pseudocode:
The real code is in arch/x86/entry/entry_64.S */
// Hardware pushes: SS, RSP, RFLAGS, CS, RIP (the "bookmark")
// Kernel entry stub then pushes all general-purpose registers:
pushq %rax
pushq %rbx
// ... all 15 GP registers ...
// Then calls the C handler:
callq do_IRQ // passes the pt_regs pointer in %rdi
02IRQ Numbers, IRQ Descriptors, and the IRQ Domain
In early Linux, IRQ numbers were small integers that mapped 1-to-1 to hardware pins. Modern systems are more complex: an SoC may have hundreds of interrupt sources across multiple interrupt controllers chained together. Linux solves this with the IRQ domain abstraction.
Hardware IRQ (hwirq): The number the interrupt controller hardware uses internally.
Linux IRQ (virq): A kernel-global integer that uniquely identifies one interrupt source. This is what request_irq() takes.
irq_desc: The struct irq_desc in kernel/irq/irqdesc.c is the master descriptor for one Linux IRQ. It holds the hardware chip descriptor (struct irq_chip), the list of registered actions (struct irqaction), statistics (spurious count, total count), and the current IRQ flags.
An IRQ domain (struct irq_domain) is a mapping table owned by one interrupt controller driver. When a device tree specifies interrupts = <42>;, the domain's .xlate() callback translates the hardware IRQ 42 to a Linux virq. The driver calls irq_of_parse_and_map() or platform_get_irq() to get back a virq it can pass to request_irq().
For most driver writers, this is invisible — platform_get_irq() does all the work. But knowing it exists explains why you should never hardcode IRQ numbers and why /proc/interrupts shows Linux IRQ numbers that may differ from what the hardware manual says.
/* In a platform driver's probe() function:
Get the Linux IRQ number from the platform resource */
#include <linux/platform_device.h>
#include <linux/interrupt.h>
static int mydev_probe(struct platform_device *pdev)
{
int irq = platform_get_irq(pdev, 0); /* index 0 = first IRQ */
if (irq < 0)
return irq; /* propagate the error code */
/* irq is now a valid Linux virq number */
dev_info(&pdev->dev, "Got IRQ %d\n", irq);
return 0;
}
03request_irq() and free_irq(): Claiming and Releasing an Interrupt
request_irq() is the kernel API that says "I, this driver, want to handle IRQ number N." It allocates a struct irqaction, fills it with your handler pointer, name, and flags, and inserts it into the irq_desc for that IRQ.
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev_id);
irq — the Linux virq number.
handler — your interrupt service routine, irqreturn_t (*)(int, void *).
flags — bitfield controlling sharing, trigger type, etc. (covered next).
name — appears in /proc/interrupts; use a meaningful string.
dev_id — a cookie passed back to your handler; critical for shared IRQs (it identifies which device fired). Convention: pass dev (your struct device *) or your private data pointer. Never pass NULL for a shared IRQ.
Return value: 0 on success, negative errno on failure. Common failures: -EBUSY (someone else has the IRQ non-shared), -EINVAL (bad arguments).
free_irq(irq, dev_id) is the mirror: it removes your irqaction from the chain. The dev_id must match what you passed to request_irq() — it is the key used to find your specific action in a shared chain. Always call free_irq() in your driver's remove() / error-path cleanup. Failing to do so is a classic driver bug that causes crashes when the module is unloaded.
Analogy: Think of the IRQ as a telephone extension in an office. request_irq() is plugging your phone into that extension. free_irq() is unplugging it when you leave. If you forget to unplug, the next person who calls that extension may get confused noise.
#include <linux/interrupt.h>
#include <linux/module.h>
struct mydev_priv {
int irq;
/* ... other fields ... */
};
static irqreturn_t mydev_irq_handler(int irq, void *dev_id);
static int mydev_probe(struct platform_device *pdev)
{
struct mydev_priv *priv;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0)
return priv->irq;
ret = request_irq(priv->irq,
mydev_irq_handler,
0, /* flags: exclusive, level-triggered */
"mydev",
priv); /* dev_id = our private data */
if (ret) {
dev_err(&pdev->dev, "request_irq failed: %d\n", ret);
return ret;
}
platform_set_drvdata(pdev, priv);
return 0;
}
static int mydev_remove(struct platform_device *pdev)
{
struct mydev_priv *priv = platform_get_drvdata(pdev);
free_irq(priv->irq, priv); /* MUST match dev_id from request_irq */
return 0;
}
04IRQ Flags: IRQF_SHARED, IRQF_TRIGGER_*, IRQF_ONESHOT
The flags argument to request_irq() is a bitfield. Understanding the main flags is essential.
IRQF_SHARED — Allows multiple drivers to register handlers on the same IRQ line. Without this flag, request_irq() returns -EBUSY if the IRQ is already taken. With it, all registered handlers are called in sequence when the IRQ fires. Each handler must check whether its device actually caused the interrupt and return IRQ_NONE if not (see irqreturn_t below). ISA-era PCs shared IRQs routinely; PCI also supports sharing. PCIe uses MSI/MSI-X (message-signaled interrupts) which are never shared.
IRQF_TRIGGER_RISING / IRQF_TRIGGER_FALLING / IRQF_TRIGGER_HIGH / IRQF_TRIGGER_LOW — For GPIO or platform interrupts where the kernel needs to configure the trigger polarity. Level-triggered (_HIGH / _LOW) means the interrupt fires continuously while the signal is at that level — the driver must clear the interrupt source in hardware before the handler returns, or the system will be stuck in an infinite interrupt loop. Edge-triggered (_RISING / _FALLING) fires once per transition. For PCI devices, the hardware and BIOS configure this; the driver does not need to specify it.
IRQF_ONESHOT — Critical for threaded IRQs (discussed later). It tells the kernel to keep the IRQ line masked (disabled) until the threaded handler has finished running. Without it, a level-triggered interrupt would re-fire immediately after the top-half handler returns (because the hardware line is still asserted). IRQF_ONESHOT solves this: the line stays masked until your thread reads the FIFO/clears the status register and the interrupt condition goes away.
IRQF_NO_THREAD — Forces the handler to run in hard-IRQ context even when the kernel is configured with CONFIG_IRQ_FORCED_THREADING (common in PREEMPT_RT). Use only for handlers that genuinely cannot be threaded (e.g., timer tick, IPI).
Flags can be OR'd together. Example: IRQF_SHARED | IRQF_TRIGGER_RISING.
/* Shared IRQ registration: both drivers share IRQ 11 */
/* Driver A */
request_irq(11, driverA_handler,
IRQF_SHARED, "driverA", devA);
/* Driver B */
request_irq(11, driverB_handler,
IRQF_SHARED, "driverB", devB);
/*
* When IRQ 11 fires, the kernel calls BOTH handlers.
* Each must check its own device's status register:
*/
static irqreturn_t driverA_handler(int irq, void *dev_id)
{
struct devA_priv *priv = dev_id;
u32 status = readl(priv->base + STATUS_REG);
if (!(status & MY_IRQ_BIT))
return IRQ_NONE; /* not my interrupt */
writel(MY_IRQ_BIT, priv->base + STATUS_REG); /* clear it */
/* ... handle the event ... */
return IRQ_HANDLED;
}
05The IRQ Handler: irqreturn_t and Atomic Context Constraints
Your interrupt handler has the signature:
irqreturn_t handler(int irq, void *dev_id);
It must return one of three values:
IRQ_NONE— "I looked at my hardware; this interrupt was not from me." Required for shared IRQs when your device did not fire.IRQ_HANDLED— "I handled it." The normal return for a non-shared IRQ or a shared IRQ that was yours.IRQ_WAKE_THREAD— "I handled the urgent part; please wake my threaded handler for the rest." Used with the top/bottom split viarequest_threaded_irq().
The Golden Rule of Interrupt Context: Your handler runs with the IRQ line masked (or at a high hardware priority), possibly with all interrupts disabled on that CPU. This is called atomic context or hard-IRQ context. The constraints are strict:
- No sleeping. You cannot call
schedule(),msleep(),mutex_lock(),kmalloc(GFP_KERNEL), or any function that might sleep. If you do, you will get a kernel BUG or a deadlock. The scheduler cannot preempt an interrupt handler. - No user-space access.
copy_to_user()can sleep; forbidden. - Spinlocks only. You may use
spin_lock_irqsave()to protect shared data. Nevermutex_lock(). - Must be fast. An interrupt handler that runs too long starves other interrupts. The kernel will even print a warning for handlers that run over ~1ms.
- Allocations: Only
GFP_ATOMICallocations are allowed (they do not sleep but can fail).
The kernel provides in_interrupt() and in_atomic() macros that return nonzero in these contexts, but never use them to "work around" the rules — fix the root cause.
Analogy: Imagine you are a surgeon mid-operation and your pager goes off. The "interrupt handler" is glancing at the pager message. You cannot stop, leave the OR, and go have lunch — you must handle it instantly and minimally, then return to the operation.
#include <linux/interrupt.h>
static irqreturn_t mydev_handler(int irq, void *dev_id)
{
struct mydev_priv *priv = dev_id;
u32 status;
/* Read the hardware status register */
status = readl(priv->base + IRQ_STATUS);
if (!status)
return IRQ_NONE; /* spurious: not our device */
/* Acknowledge the interrupt at hardware level */
writel(status, priv->base + IRQ_ACK);
/* Store data atomically for the bottom half */
priv->pending_status = status;
/* Schedule deferred work (does NOT sleep) */
tasklet_schedule(&priv->tasklet);
return IRQ_HANDLED;
/* DO NOT: mutex_lock, kmalloc(GFP_KERNEL), msleep, etc. */
}
06Top Half vs. Bottom Half: Why and How
The tension is real: interrupts must be acknowledged quickly (or the hardware floods the CPU with repeat interrupts), but the real work — parsing a packet, copying a buffer, waking a process — takes time and may need to sleep. The solution, refined over decades, is the top-half / bottom-half split.
Top half (the hard-IRQ handler): runs immediately in interrupt context. It must:
- Acknowledge the interrupt to the hardware (clear the interrupt flag, read the status register).
- Capture the minimum data needed (DMA descriptor index, status bits, timestamp).
- Schedule deferred work.
- Return
IRQ_HANDLED(orIRQ_WAKE_THREAD).
Bottom half: runs later, in a schedulable context, to do the heavy lifting. Linux provides several mechanisms:
Tasklets (tasklet_schedule()): Run in softirq context (still atomic, no sleeping, but with interrupts enabled). Suitable for short non-sleeping work. Being phased out in favor of threaded IRQs in modern kernels.
Workqueues (schedule_work(), queue_work()): Run in a kernel thread context. Can sleep, can allocate with GFP_KERNEL, can take mutexes. The most flexible mechanism. Latency is slightly higher because a thread must be scheduled.
Softirqs: The lowest-level mechanism, used by networking (NET_RX_SOFTIRQ) and block I/O. Runs in a per-CPU, non-preemptible context. Do not use directly in drivers unless you are writing core subsystem code.
The historical terminology can confuse: Linux documentation sometimes calls tasklets and workqueues collectively "bottom halves," and there used to be an older mechanism literally called BH (bottom half) that is long gone.
For new drivers, the recommended pattern is: use request_threaded_irq() with a minimal top-half handler and a threaded bottom half that can sleep freely.
#include <linux/interrupt.h>
#include <linux/workqueue.h>
struct mydev_priv {
void __iomem *base;
u32 pending_status;
struct work_struct work; /* bottom half */
};
/* BOTTOM HALF: runs in a kernel thread, can sleep */
static void mydev_work_handler(struct work_struct *work)
{
struct mydev_priv *priv =
container_of(work, struct mydev_priv, work);
u32 status = priv->pending_status;
/* Can now sleep, allocate, take mutexes, etc. */
if (status & DATA_READY_BIT) {
u8 *buf = kmalloc(256, GFP_KERNEL); /* OK here! */
/* ... process data ... */
kfree(buf);
}
}
/* TOP HALF: hard-IRQ context, must be fast */
static irqreturn_t mydev_handler(int irq, void *dev_id)
{
struct mydev_priv *priv = dev_id;
priv->pending_status = readl(priv->base + STATUS_REG);
writel(priv->pending_status, priv->base + IRQ_ACK); /* ack */
schedule_work(&priv->work); /* kick bottom half */
return IRQ_HANDLED;
}
/* In probe(): */
// INIT_WORK(&priv->work, mydev_work_handler);
// request_irq(irq, mydev_handler, 0, "mydev", priv);
07Threaded IRQs: request_threaded_irq()
request_threaded_irq() is the modern, preferred API for the top/bottom split. It creates a dedicated kernel thread for your IRQ, eliminating the need to manually manage workqueues or tasklets for most drivers.
int request_threaded_irq(
unsigned int irq,
irq_handler_t handler, /* top half: hard-IRQ context */
irq_handler_t thread_fn, /* bottom half: kernel thread */
unsigned long flags,
const char *name,
void *dev_id);
Flow:
- Hardware fires IRQ.
handler(top half) runs in hard-IRQ context. If it returnsIRQ_WAKE_THREAD, the kernel wakes the per-IRQ thread.thread_fn(bottom half) runs in the IRQ thread (akthread). It can sleep, allocate, take mutexes.
IRQF_ONESHOT is required when thread_fn is provided and there is no primary handler (pass NULL as handler). When handler is NULL, the kernel provides a default that just returns IRQ_WAKE_THREAD. But IRQF_ONESHOT must be set so the IRQ line stays masked until thread_fn returns. This prevents a level-triggered interrupt from continuously re-firing before the thread has had a chance to clear the hardware condition.
The IRQ thread appears in ps as [irq/N-name] where N is the IRQ number. You can observe it:
$ ps aux | grep irq/
root 123 0.0 0.0 0 0 ? S 00:00 [irq/42-mydev]
This thread has real-time scheduling priority (SCHED_FIFO by default), which means it preempts ordinary tasks but can be preempted by higher-priority RT tasks and hard-IRQ handlers.
When to use: Prefer request_threaded_irq() whenever your handler needs to sleep, take a mutex, or do substantial work. It is simpler and safer than the manual workqueue pattern, and it is what most subsystem maintainers expect to see in new drivers.
#include <linux/interrupt.h>
/* Top half: runs in hard-IRQ context */
static irqreturn_t mydev_hard_handler(int irq, void *dev_id)
{
struct mydev_priv *priv = dev_id;
/* Read and acknowledge the interrupt FAST */
priv->hw_status = readl(priv->base + STATUS_REG);
if (!priv->hw_status)
return IRQ_NONE; /* not ours */
writel(priv->hw_status, priv->base + IRQ_ACK);
return IRQ_WAKE_THREAD; /* wake the thread */
}
/* Bottom half: runs in kthread, CAN sleep */
static irqreturn_t mydev_thread_handler(int irq, void *dev_id)
{
struct mydev_priv *priv = dev_id;
/* Safe to sleep, allocate, take mutexes here */
mutex_lock(&priv->lock);
process_hw_data(priv, priv->hw_status);
mutex_unlock(&priv->lock);
return IRQ_HANDLED;
}
/* In probe(): */
static int mydev_probe(struct platform_device *pdev)
{
struct mydev_priv *priv = /* ... allocate ... */ NULL;
int irq = platform_get_irq(pdev, 0);
return request_threaded_irq(
irq,
mydev_hard_handler, /* top half */
mydev_thread_handler, /* bottom half */
IRQF_ONESHOT, /* keep masked until thread done */
"mydev",
priv);
}
08Protecting Shared Data: Spinlocks and disable_irq()
Your interrupt handler and your driver's other code (e.g., read/write syscall paths running in process context) often share data structures. This is a classic concurrency problem with an important twist: the handler can interrupt process context at any moment.
The wrong solution: mutex_lock() — a mutex can sleep, and your handler cannot sleep. If the process context holds the mutex and the handler tries to lock it, deadlock.
The right solution: spinlock_t with IRQ save/restore.
- In process context (your
read()/write()/ioctl()handlers), usespin_lock_irqsave(&lock, flags). This disables local IRQs on the current CPU and acquires the spinlock atomically. It prevents the IRQ handler from racing with you on the same CPU. - In interrupt context, use
spin_lock(&lock)(notspin_lock_irqsave; IRQs are already disabled).
spin_lock_irqsave saves the current IRQ enable/disable state in flags (an unsigned long) and restores it with spin_unlock_irqrestore(&lock, flags). This is important when your process-context code might already have IRQs disabled for another reason.
disable_irq(irq) / enable_irq(irq): Sometimes you need to completely prevent the IRQ from firing (not just mask it on one CPU). disable_irq() masks the IRQ at the interrupt controller and waits for any in-progress handler to finish before returning. Use it when you need to replace hardware register state atomically with respect to interrupts. disable_irq_nosync() does the same but does not wait for the in-flight handler — useful from within another interrupt handler.
Rule of thumb: If data is shared between interrupt context and process context, protect it with a spinlock and spin_lock_irqsave in process context. If data is only shared between two process contexts (two threads, two ioctl() calls), a mutex is fine.
#include <linux/spinlock.h>
#include <linux/interrupt.h>
struct mydev_priv {
spinlock_t lock;
u32 rx_count;
/* ... */
};
/* INTERRUPT CONTEXT: IRQs already disabled on this CPU */
static irqreturn_t mydev_handler(int irq, void *dev_id)
{
struct mydev_priv *priv = dev_id;
spin_lock(&priv->lock); /* no irqsave needed: IRQs already off */
priv->rx_count++;
spin_unlock(&priv->lock);
return IRQ_HANDLED;
}
/* PROCESS CONTEXT: running in a syscall, IRQs are enabled */
static ssize_t mydev_read(struct file *f, char __user *buf,
size_t len, loff_t *off)
{
struct mydev_priv *priv = f->private_data;
unsigned long flags;
u32 count;
/* Must disable IRQs to safely read rx_count */
spin_lock_irqsave(&priv->lock, flags);
count = priv->rx_count;
priv->rx_count = 0;
spin_unlock_irqrestore(&priv->lock, flags);
/* Now copy to user space outside the lock */
return copy_to_user(buf, &count, sizeof(count)) ? -EFAULT : sizeof(count);
}
09Shared IRQs: How They Work and When to Use Them
Shared IRQs (IRQF_SHARED) allow multiple devices — and their drivers — to share a single IRQ line. This was essential in the ISA era where there were only 16 IRQ lines for potentially dozens of devices. PCI also supports shared INTx lines (though PCIe with MSI/MSI-X has largely eliminated the need).
How the kernel handles a shared IRQ:
The irq_desc for that IRQ holds a linked list of struct irqaction entries, one per registered handler. When the IRQ fires, the kernel calls every handler in the list, in order, passing each its own dev_id. Each handler checks its own hardware status register to determine if it was the source, returns IRQ_HANDLED or IRQ_NONE accordingly. The kernel collects the return values: if all return IRQ_NONE, it logs a spurious interrupt warning.
Rules for shared IRQ drivers:
- Always pass
IRQF_SHAREDto bothrequest_irq()calls. - The
dev_idmust be unique and non-NULL — it is the only wayfree_irq()can find your specificirqactionin the list to remove it. - Your handler must check your own hardware status. Never assume your device fired just because the IRQ fired.
- The trigger type (edge/level) must match between all sharers. You cannot share an edge-triggered registration with a level-triggered one.
Performance note: Shared IRQs mean more code runs per interrupt, even for devices that did not fire. On modern systems, prefer MSI/MSI-X (message-signaled interrupts) which are never shared and have much lower latency. Each MSI vector is dedicated to one device, uses a write to a special memory address to trigger the interrupt, and does not require a shared IRQ controller line.
/* /proc/interrupts shows sharing:
cat /proc/interrupts
16: 1234 0 IO-APIC 16-fasteoi uhci_hcd:usb1, eth0
^^ ^^^^^^^^^^^^^^^^^^^^^^^^
IRQ Two drivers sharing IRQ 16
Both registered with IRQF_SHARED.
When IRQ 16 fires, kernel calls uhci_hcd handler THEN eth0 handler.
Each checks its own status register. */
/* Verify sharing is possible before requesting: */
ret = request_irq(irq, my_handler,
IRQF_SHARED | IRQF_TRIGGER_LEVEL,
"mydev", priv);
if (ret == -EBUSY) {
/* IRQ is taken by a non-sharing driver — cannot share */
dev_err(dev, "IRQ %d busy and non-shareable\n", irq);
}
10Interrupt-Driven I/O: A Complete Working Example
Let us tie everything together with a minimal but complete interrupt-driven character device. The scenario: a fictional UART-like device that generates an interrupt when it has received data. We will use request_threaded_irq() since reading the FIFO might take a variable amount of time.
Key design decisions in the example:
probe()gets the IRQ from the platform resource and callsrequest_threaded_irq()withIRQF_ONESHOT.- The top-half handler reads the interrupt cause register, masks further interrupts on the device (so the threaded handler can run without re-interruption), and returns
IRQ_WAKE_THREAD. - The thread handler reads the FIFO, copies data to a kfifo, and wakes any waiting readers. It then re-enables the device's interrupt.
remove()callsfree_irq()cleanly.- A
read()file operation useswait_event_interruptible()to block until data is available.
This pattern — mask in top half, unmask in bottom half — is the correct way to handle level-triggered interrupts with a threaded handler, and it is the pattern used by real UART drivers in drivers/tty/serial/.
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/kfifo.h>
#include <linux/miscdevice.h>
#include <linux/wait.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define RX_FIFO_SIZE 256
#define STATUS_REG 0x00
#define DATA_REG 0x04
#define CTRL_REG 0x08
#define IRQ_EN_BIT BIT(0)
#define RX_READY_BIT BIT(1)
struct uart_priv {
void __iomem *base;
int irq;
DECLARE_KFIFO(rxfifo, u8, RX_FIFO_SIZE);
wait_queue_head_t rxwait;
struct miscdevice mdev;
};
/* Top half: mask interrupt, wake thread */
static irqreturn_t uart_hard_irq(int irq, void *dev_id)
{
struct uart_priv *priv = dev_id;
u32 status = readl(priv->base + STATUS_REG);
if (!(status & RX_READY_BIT))
return IRQ_NONE;
/* Mask the device's own IRQ output so it won't re-fire */
writel(readl(priv->base + CTRL_REG) & ~IRQ_EN_BIT,
priv->base + CTRL_REG);
return IRQ_WAKE_THREAD;
}
/* Bottom half: drain FIFO, wake readers, re-enable */
static irqreturn_t uart_thread_irq(int irq, void *dev_id)
{
struct uart_priv *priv = dev_id;
u8 byte;
/* Drain the hardware FIFO (can loop, may yield) */
while (readl(priv->base + STATUS_REG) & RX_READY_BIT) {
byte = readb(priv->base + DATA_REG);
kfifo_put(&priv->rxfifo, byte);
}
wake_up_interruptible(&priv->rxwait);
/* Re-enable the device interrupt */
writel(readl(priv->base + CTRL_REG) | IRQ_EN_BIT,
priv->base + CTRL_REG);
return IRQ_HANDLED;
}
static ssize_t uart_read(struct file *f, char __user *buf,
size_t len, loff_t *off)
{
struct uart_priv *priv =
container_of(f->private_data, struct uart_priv, mdev);
unsigned int copied;
int ret;
ret = wait_event_interruptible(priv->rxwait,
!kfifo_is_empty(&priv->rxfifo));
if (ret)
return -ERESTARTSYS;
ret = kfifo_to_user(&priv->rxfifo, buf, len, &copied);
return ret ? ret : copied;
}
static const struct file_operations uart_fops = {
.owner = THIS_MODULE,
.read = uart_read,
};
static int uart_probe(struct platform_device *pdev)
{
struct uart_priv *priv;
struct resource *res;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv) return -ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base)) return PTR_ERR(priv->base);
INIT_KFIFO(priv->rxfifo);
init_waitqueue_head(&priv->rxwait);
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0) return priv->irq;
ret = request_threaded_irq(
priv->irq,
uart_hard_irq,
uart_thread_irq,
IRQF_ONESHOT,
"myuart", priv);
if (ret) return ret;
priv->mdev.minor = MISC_DYNAMIC_MINOR;
priv->mdev.name = "myuart";
priv->mdev.fops = &uart_fops;
ret = misc_register(&priv->mdev);
if (ret) { free_irq(priv->irq, priv); return ret; }
platform_set_drvdata(pdev, priv);
/* Enable hardware interrupts */
writel(IRQ_EN_BIT, priv->base + CTRL_REG);
return 0;
}
static int uart_remove(struct platform_device *pdev)
{
struct uart_priv *priv = platform_get_drvdata(pdev);
/* Disable hardware interrupt first */
writel(0, priv->base + CTRL_REG);
misc_deregister(&priv->mdev);
free_irq(priv->irq, priv);
return 0;
}
11Debugging Interrupts: /proc/interrupts, ftrace, and spurious IRQs
/proc/interrupts is your first debugging tool. It shows, for each IRQ: the per-CPU hit counts, the IRQ chip name, the trigger type, and the names of all registered handlers.
cat /proc/interrupts
CPU0 CPU1
0: 4451 0 IO-APIC 2-edge timer
42: 128 92 PCI-MSI 524288-edge xhci_hcd
NMI: 0 0 Non-maskable interrupts
LOC: 84923 83012 Local timer interrupts
SPU: 0 0 Spurious interrupts
Spurious interrupts: If SPU (or your driver's IRQ) counts climb rapidly without matching real events, suspect: (1) a hardware interrupt source that is not being acknowledged properly — the line stays asserted, causing repeated firings; (2) missing IRQF_SHARED when another driver is on the line; (3) wrong trigger polarity. Fix: ensure your handler always clears the hardware interrupt source before returning.
ftrace / irqsoff tracer: echo irqsoff > /sys/kernel/debug/tracing/current_tracer records the call stack whenever IRQs are disabled for longer than a threshold. Essential for diagnosing interrupt latency problems and for finding code that holds IRQs disabled too long.
/proc/softirqs: Shows softirq counts by type (NET_RX, BLOCK, TASKLET, etc.). Useful when your bottom-half work is taking too long.
kstat_irqs(): In-kernel function to read per-CPU IRQ counts from driver code — useful in diagnostic debugfs entries.
lockdep and PROVE_LOCKING: With CONFIG_PROVE_LOCKING=y, the kernel tracks lock ordering and will warn if you try to acquire a sleeping lock (mutex) in interrupt context. This catches a huge class of bugs at development time before they manifest in production.
# See all IRQ counts, updated in real time:
watch -n1 cat /proc/interrupts
# Enable the irqsoff latency tracer:
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... reproduce your workload ...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
# Check for spurious IRQs on IRQ 42:
grep ' 42:' /proc/interrupts
# Observe the IRQ thread for your driver:
ps -eo pid,cls,pri,comm | grep 'irq/'
Key insights
- ✦An interrupt handler runs in atomic context: no sleeping, no mutexes, no GFP_KERNEL allocations — violating this causes deadlocks or crashes that are notoriously hard to debug.
- ✦The top/bottom-half split exists because hardware must be acknowledged fast, but real processing takes time. The top half acknowledges and defers; the bottom half does the work safely.
- ✦For level-triggered interrupts with a threaded handler, IRQF_ONESHOT is not optional — without it, the IRQ will re-fire continuously because the hardware line stays asserted until the thread clears it.
- ✦The dev_id cookie in request_irq() is not optional for shared IRQs — it is the only key that free_irq() uses to find and remove your specific handler from the chain.
- ✦request_threaded_irq() is the modern preferred pattern; it is simpler than manually managing workqueues and is what maintainers expect to see in new drivers.
- ✦Spinlocks with irqsave/irqrestore protect data shared between interrupt context and process context. Never use a mutex for this purpose.
- ✦Every IRQ you request must be freed — missing free_irq() on an error path is a classic driver bug that causes a crash when the module is reloaded.
- ✦/proc/interrupts is the first place to look when debugging interrupt problems; rising SPU counts mean your handler is not acknowledging the hardware interrupt source correctly.
Common pitfalls
- ⚠Sleeping in interrupt context: calling mutex_lock(), msleep(), or kmalloc(GFP_KERNEL) inside a hard-IRQ handler. The kernel will either BUG() or deadlock. Use GFP_ATOMIC for allocations and spinlocks for synchronization.
- ⚠Forgetting free_irq() on error paths in probe(). If any initialization step after request_irq() fails, the error-unwind path must call free_irq() before returning.
- ⚠Missing IRQF_ONESHOT for level-triggered threaded IRQs. Without it, the interrupt re-fires before the thread clears the hardware condition, causing an interrupt storm.
- ⚠Passing NULL as dev_id for a shared IRQ. free_irq() uses dev_id to find the right irqaction. NULL is technically allowed for exclusive IRQs but is bad practice; it is forbidden (and will fail) for shared ones.
- ⚠Not checking the hardware status register in a shared IRQ handler. If your handler always returns IRQ_HANDLED regardless, you will mask interrupts for other devices sharing the line.
- ⚠Using spin_lock() (without irqsave) in process context for data shared with an interrupt handler. On a non-SMP kernel or on the same CPU, the interrupt can fire mid-lock and re-acquire the lock, causing a deadlock.
- ⚠Directly writing hardcoded IRQ numbers instead of using platform_get_irq() or irq_of_parse_and_map(). This makes the driver non-portable and breaks device tree and ACPI enumeration.
- ⚠Forgetting to acknowledge the interrupt in hardware before returning from the handler. For level-triggered interrupts, the kernel will see the line still asserted and call the handler again immediately, creating an infinite loop.
Practice
- 01medium
Write a minimal loadable kernel module that registers a handler for a software-triggered interrupt using `raise_softirq()` or, more practically, uses `misc_register()` and a write to a debugfs file to simulate an IRQ event via a workqueue. The handler must: (1) be in a separate top and bottom half, (2) use a spinlock correctly, and (3) expose a counter via `/proc` or debugfs that increments each time the handler fires. Test by loading the module, triggering the event, and reading the counter.
- 02easy
Deliberately introduce the 'sleep in interrupt context' bug: inside a hard-IRQ handler, call `msleep(1)`. Boot a kernel with CONFIG_DEBUG_ATOMIC_SLEEP=y and CONFIG_PREEMPT=y. Observe and interpret the kernel splat. Then fix it by moving the sleep into a workqueue handler. Document what the splat told you.
- 03hard
Write a driver for a GPIO button on a Raspberry Pi (or QEMU virtio-input) that uses request_threaded_irq() with IRQF_TRIGGER_RISING | IRQF_ONESHOT. The threaded handler should debounce the button (sleep 50ms, then re-read the GPIO level) and write a timestamp to a kfifo. A character device read() should return the last N timestamps to userspace. Verify with a userspace test program.
- 04hard
Simulate a shared IRQ scenario in QEMU: two platform devices configured to share the same IRQ number (via device tree overlay or a synthetic platform driver). Write two drivers, each registering IRQF_SHARED handlers on the same IRQ. Add a debugfs knob to each that 'fires' a software interrupt. Verify via /proc/interrupts that both handlers are listed, and that IRQ_NONE is returned by the non-owning driver.
- 05medium
Use the ftrace irqsoff tracer to find the longest IRQ-off window in a running kernel (your development VM). Identify the top offender from the trace output. Look up that function in the kernel source and explain why it disables IRQs.
Resources
Chapter 10 covers interrupt handling in depth; freely available online. Some APIs are dated but the concepts are foundational.
Chapters 7 and 8 cover interrupts and bottom halves with clarity and precision; the best concise treatment available.
The interrupt handling section (search 'interrupt' in the PDF) is accurate, current, and used in professional training.
Thomas Gleixner's original article explaining the motivation and design of threaded IRQ handlers — primary source.
The authoritative kernel documentation for the generic IRQ subsystem, IRQ domains, and the irq_chip interface.
Classic article on spinlock vs. mutex vs. rwlock selection, including the interrupt-context rules.
Beginner-friendly walkthrough with worked examples; good first read before the kernel docs.
A real production UART driver using threaded IRQs and the top/bottom split. Read serial8250_interrupt() and serial8250_handle_port().
Checkpoints — you can now…
- ✓You can call request_threaded_irq() with the correct flags for a level-triggered GPIO interrupt, including IRQF_ONESHOT, and explain why each flag is needed.
- ✓You can explain why mutex_lock() is forbidden in a hard-IRQ handler and what to use instead.
- ✓You can write a handler that correctly returns IRQ_NONE vs. IRQ_HANDLED vs. IRQ_WAKE_THREAD, and explain the consequence of returning the wrong value for a shared IRQ.
- ✓You can implement the top/bottom half split using a workqueue, and explain when to prefer request_threaded_irq() over a manual workqueue.
- ✓You can use spin_lock_irqsave() in process context and spin_lock() in interrupt context to protect a shared data structure, and explain why the two calls differ.
- ✓You can read /proc/interrupts and interpret the columns, identify a spurious interrupt count, and describe one likely root cause.
- ✓You can write a correct probe() and remove() pair that registers and frees an IRQ, with proper error-path cleanup so free_irq() is always called if request_irq() succeeded.