Basic: Requesting an IRQ {:.gc-basic}
Basic
A hardware interrupt signals the CPU that a device needs attention. The driver registers a handler with request_irq() that the kernel calls from interrupt context when the IRQ fires.
#include <linux/interrupt.h>
#include <linux/irq.h>
/* IRQ handler — runs in hard interrupt context (atomic, no sleep) */
static irqreturn_t mydrv_isr(int irq, void *dev_id)
{
struct mydrv_priv *priv = dev_id;
u32 status;
status = readl(priv->base + REG_IRQ_STATUS);
if (!(status & MY_IRQ_MASK))
return IRQ_NONE; /* not our interrupt (shared IRQ) */
/* Acknowledge interrupt at hardware level */
writel(MY_IRQ_MASK, priv->base + REG_IRQ_CLEAR);
/* Signal bottom half */
schedule_work(&priv->work);
return IRQ_HANDLED;
}
static int mydrv_probe(struct platform_device *pdev)
{
struct mydrv_priv *priv;
int irq, ret;
/* Get IRQ number from Device Tree */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
/* Alternative: parse DT interrupt directly */
/* irq = irq_of_parse_and_map(pdev->dev.of_node, 0); */
ret = devm_request_irq(&pdev->dev, irq, mydrv_isr,
IRQF_TRIGGER_RISING, /* edge-triggered */
dev_name(&pdev->dev),
priv);
if (ret) {
dev_err(&pdev->dev, "failed to request IRQ %d: %d\n", irq, ret);
return ret;
}
priv->irq = irq;
return 0;
}
IRQ flags
| Flag | Meaning |
|---|---|
IRQF_SHARED |
IRQ line is shared — handler must verify the interrupt source |
IRQF_TRIGGER_RISING |
Trigger on rising edge |
IRQF_TRIGGER_FALLING |
Trigger on falling edge |
IRQF_TRIGGER_HIGH |
Level-triggered, active high |
IRQF_TRIGGER_LOW |
Level-triggered, active low |
IRQF_DISABLED |
(removed in 3.x) — irqs were kept disabled in handler |
# View all registered IRQs and their handler counts
cat /proc/interrupts
# Output columns: IRQ CPU0 CPU1 ... type handler_name
# 17: 1234 0 PCI-MSI eth0
# 42: 567 234 GIC mydrv
Threaded IRQs {:.gc-basic}
Basic
A threaded IRQ splits the handler into a fast hard IRQ function (runs in interrupt context) and a thread function (runs in a real kernel thread with a process context — can sleep, take mutexes, etc.).
#include <linux/interrupt.h>
/* Hard IRQ handler — must be fast, cannot sleep */
static irqreturn_t mydrv_hard_isr(int irq, void *dev_id)
{
struct mydrv_priv *priv = dev_id;
u32 status;
status = readl(priv->base + REG_IRQ_STATUS);
if (!(status & MY_IRQ_MASK))
return IRQ_NONE;
/* Mask the interrupt at source to prevent re-entry */
writel(0, priv->base + REG_IRQ_ENABLE);
/* Store status for thread to process */
priv->irq_status = status;
return IRQ_WAKE_THREAD; /* wake the thread handler */
}
/* Thread handler — process context, can sleep and take locks */
static irqreturn_t mydrv_thread_fn(int irq, void *dev_id)
{
struct mydrv_priv *priv = dev_id;
/* Can sleep here, take mutex, call i2c_transfer, etc. */
mutex_lock(&priv->lock);
process_event(priv, priv->irq_status);
mutex_unlock(&priv->lock);
/* Re-enable the interrupt */
writel(MY_IRQ_MASK, priv->base + REG_IRQ_ENABLE);
return IRQ_HANDLED;
}
/* Register with request_threaded_irq */
ret = devm_request_threaded_irq(&pdev->dev, irq,
mydrv_hard_isr, /* hard handler */
mydrv_thread_fn, /* thread handler */
IRQF_ONESHOT, /* keep IRQ masked until thread completes */
"mydrv",
priv);
IRQF_ONESHOT is required for threaded IRQs when the hard handler does not re-enable the interrupt — it keeps the line masked until the thread function returns IRQ_HANDLED.
Top and Bottom Halves {:.gc-mid}
Intermediate
The kernel’s interrupt handling is split into a top half (hard IRQ context — fast, non-sleeping) and a bottom half (deferred — can be more expensive). Three main mechanisms exist for bottom-half work.
Comparison of deferred work mechanisms
| Mechanism | Context | Can sleep? | Per-CPU? | Typical use |
|---|---|---|---|---|
| softirq | Soft interrupt | No | Yes | Network stack, block I/O |
| Tasklet | softirq-based | No | No | Legacy bottom half |
| Workqueue | Kernel thread | Yes | Optional | General deferred work |
| Threaded IRQ | Kernel thread | Yes | No | Modern IRQ bottom half |
Tasklet example
#include <linux/interrupt.h>
struct mydrv_priv {
struct tasklet_struct tasklet;
u32 saved_status;
};
static void mydrv_tasklet_fn(unsigned long data)
{
struct mydrv_priv *priv = (struct mydrv_priv *)data;
/* Runs in softirq context — no sleep, but can run on any CPU */
pr_info("tasklet: status=0x%08x\n", priv->saved_status);
}
/* In probe */
tasklet_init(&priv->tasklet, mydrv_tasklet_fn, (unsigned long)priv);
/* In hard ISR */
priv->saved_status = readl(base + REG_STATUS);
tasklet_schedule(&priv->tasklet); /* schedule for later execution */
/* In remove */
tasklet_kill(&priv->tasklet);
Workqueue example
#include <linux/workqueue.h>
struct mydrv_priv {
struct work_struct work;
struct workqueue_struct *wq;
};
static void mydrv_work_fn(struct work_struct *work)
{
struct mydrv_priv *priv = container_of(work, struct mydrv_priv, work);
/* Full process context: can sleep, take mutex, call i2c, etc. */
mutex_lock(&priv->lock);
do_slow_processing(priv);
mutex_unlock(&priv->lock);
}
/* In probe */
INIT_WORK(&priv->work, mydrv_work_fn);
priv->wq = create_singlethread_workqueue("mydrv-wq");
/* In ISR */
queue_work(priv->wq, &priv->work);
/* In remove */
flush_workqueue(priv->wq);
destroy_workqueue(priv->wq);
For simple cases, schedule_work(&priv->work) uses the system-wide workqueue (system_wq) instead of a private one.
Kernel Timers {:.gc-mid}
Intermediate
Legacy timer_list (jiffy-resolution)
#include <linux/timer.h>
#include <linux/jiffies.h>
struct mydrv_priv {
struct timer_list timer;
};
static void mydrv_timer_cb(struct timer_list *t)
{
struct mydrv_priv *priv = from_timer(priv, t, timer);
pr_info("timer fired at jiffies=%lu\n", jiffies);
/* Reschedule for periodic operation */
mod_timer(&priv->timer, jiffies + msecs_to_jiffies(100));
}
/* Setup */
timer_setup(&priv->timer, mydrv_timer_cb, 0);
mod_timer(&priv->timer, jiffies + msecs_to_jiffies(100)); /* fire in 100ms */
/* Cleanup — waits for any running callback to complete */
del_timer_sync(&priv->timer);
jiffies is the kernel’s tick counter. HZ is the number of jiffies per second (typically 100, 250, or 1000). msecs_to_jiffies(ms) converts milliseconds to jiffies safely.
hrtimer (high-resolution timer)
hrtimer uses the hardware timer directly and has nanosecond resolution, independent of HZ.
#include <linux/hrtimer.h>
#include <linux/ktime.h>
struct mydrv_priv {
struct hrtimer hr_timer;
};
static enum hrtimer_restart mydrv_hrtimer_cb(struct hrtimer *timer)
{
struct mydrv_priv *priv = container_of(timer, struct mydrv_priv, hr_timer);
pr_info("hrtimer fired: ktime=%lld ns\n", ktime_get_ns());
/* Reschedule: advance expiry by 500 µs */
hrtimer_forward_now(timer, ktime_set(0, 500000)); /* 500000 ns = 500 µs */
return HRTIMER_RESTART;
/* Return HRTIMER_NORESTART to fire once */
}
/* Setup and start */
hrtimer_init(&priv->hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
priv->hr_timer.function = mydrv_hrtimer_cb;
hrtimer_start(&priv->hr_timer, ktime_set(0, 500000), HRTIMER_MODE_REL);
/* Cancel */
hrtimer_cancel(&priv->hr_timer);
| Clock ID | Description |
|---|---|
CLOCK_MONOTONIC |
Monotonically increasing, no wall-clock jumps |
CLOCK_REALTIME |
Wall-clock time (affected by NTP/settimeofday) |
CLOCK_BOOTTIME |
Monotonic + time spent suspended |
Advanced: IRQ Domains and GIC {:.gc-adv}
Advanced
On SoCs, a Generic Interrupt Controller (GIC) sits between hardware interrupt lines and the CPU. An IRQ domain maps hardware interrupt numbers to Linux IRQ numbers.
#include <linux/irqdomain.h>
#include <linux/irq.h>
/* Interrupt controller driver: create a domain for 64 hardware IRQs */
struct irq_domain *domain;
domain = irq_domain_add_linear(np, /* DT node */
64, /* number of HW IRQs */
&myic_ops, /* irq_domain_ops */
myic); /* host data */
/* irq_domain_ops: map and unmap HW -> Linux IRQ */
static const struct irq_domain_ops myic_ops = {
.map = myic_irq_map,
.unmap = irq_domain_free_irqs_parent,
};
static int myic_irq_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
irq_set_chip_data(irq, d->host_data);
irq_set_chip_and_handler(irq, &myic_chip, handle_level_irq);
irq_set_noprobe(irq);
return 0;
}
Using a GPIO as an interrupt
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
struct gpio_desc *gpio = devm_gpiod_get(&pdev->dev, "irq", GPIOD_IN);
if (IS_ERR(gpio))
return PTR_ERR(gpio);
int irq = gpiod_to_irq(gpio);
if (irq < 0)
return irq;
ret = devm_request_irq(&pdev->dev, irq, mydrv_isr,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"mydrv-gpio-irq", priv);
In the DTS:
mydevice {
irq-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
};
Interview Q&A {:.gc-iq}
Interview Q&A
Q1: Why can’t you sleep in a hard IRQ handler?
Hard IRQ handlers run with interrupts disabled (on the local CPU) and with the CPU in interrupt context (in_interrupt() returns true). Sleeping would mean calling the scheduler, which requires a valid process context and may need to re-enable interrupts. This would corrupt interrupt state and could lead to deadlocks if the sleeping handler holds a spinlock that the scheduler needs. Any blocking work must be deferred to a workqueue or threaded IRQ.
Q2: When should you use a tasklet vs a workqueue?
Use a tasklet when: the deferred work is very short and cannot sleep (no mutex, no I/O, no kmalloc with GFP_KERNEL). Tasklets run in softirq context on the CPU that scheduled them. Use a workqueue when: the deferred work may sleep, take a mutex, call I2C/SPI functions, or allocate memory with GFP_KERNEL. Threaded IRQs are the modern replacement for most tasklet use cases and should be preferred in new drivers.
Q3: What are the requirements for IRQF_SHARED?
When IRQF_SHARED is set, multiple drivers share a single IRQ line. Every handler on that line is called for every interrupt. Each handler must: (1) check whether the interrupt actually came from its own device (usually by reading a status register), (2) return IRQ_NONE if not its interrupt, IRQ_HANDLED if it was. All handlers sharing the line must also request the IRQ with IRQF_SHARED.
Q4: What is a softirq and how does it relate to tasklets?
A softirq is a statically registered, low-priority kernel “software interrupt” that runs after hard IRQ handlers with interrupts enabled. There are a fixed number of softirq types (HI_SOFTIRQ, NET_TX, NET_RX, TIMER, etc.) compiled into the kernel. Tasklets are built on top of the TASKLET_SOFTIRQ type — they are a dynamic, serialised layer over softirqs. The key difference is that multiple softirq instances of the same type can run simultaneously on multiple CPUs; tasklets are always serialised (the same tasklet does not run concurrently on two CPUs).
Q5: How does jiffies overflow and is it a problem?
jiffies is an unsigned long (32-bit on 32-bit systems, 64-bit on 64-bit systems). On a 32-bit kernel with HZ=250, it overflows every ~198 days. The kernel provides time_after(a, b) and time_before(a, b) macros that handle unsigned wraparound correctly. Always use these instead of a > b when comparing jiffies values, otherwise timers will appear to expire immediately after an overflow.
Q6: What is the difference between hrtimer and timer_list?
timer_list (now using timer_setup) has jiffy-level resolution — at HZ=250 that is 4ms granularity. It runs in softirq context. hrtimer uses the hardware clock source directly and can fire with nanosecond precision. hrtimer callbacks also run in softirq context (or hardirq if HRTIMER_MODE_ABS_HARD is used). Use hrtimer whenever sub-millisecond timing accuracy is required (audio, motor control, PWM generation).
References {:.gc-ref}
References
| Resource | Link |
|---|---|
| Linux kernel interrupts documentation | https://www.kernel.org/doc/html/latest/core-api/irq/index.html |
| Linux Device Drivers 3rd ed — Interrupts | https://lwn.net/Kernel/LDD3/ |
| Workqueue documentation | https://www.kernel.org/doc/html/latest/core-api/workqueue.html |
| hrtimer API documentation | https://www.kernel.org/doc/html/latest/timers/hrtimers.html |
| IRQ domain documentation | https://www.kernel.org/doc/html/latest/core-api/irq/irq-domain.html |
| Threaded IRQ explanation (LWN) | https://lwn.net/Articles/302043/ |
| Softirqs and tasklets (LWN) | https://lwn.net/Articles/520076/ |