Writing a Linux Kernel Interrupt Handler (Kernel 6.x Guide)-Linux Device Driver Training Online

← Previous Lecture    Next Lecture →

Writing a Linux Kernel Interrupt Handler (Kernel 6.x Guide)
Free Linux Device Drivers Course — Registering IRQs with devm_request_irq() and Threaded Handlers
Lecture 2 of Device Driver Interrupts Series
Level: Intermediate
Reading Time: 14 min

In the previous lecture we covered why hardware interrupts exist and why handlers must stay fast. In this lecture of our free Linux device drivers course, we get hands-on and write a real linux kernel interrupt handler using the modern, recommended kernel APIs. We’ll use the resource-managed devm_request_irq() function instead of the legacy request_irq(), and we’ll also look at threaded interrupt handlers, which are now the preferred approach for drivers that need to do more than a few microseconds of work.

What You Will Learn
The interrupt handler function signature Registering an IRQ with devm_request_irq() Understanding irqreturn_t return values Threaded interrupt handlers A complete working GPIO button example
Prerequisites
Previous lecture: Interrupt Handling Basics Comfortable writing loadable kernel modules Basic understanding of platform/GPIO drivers

The Interrupt Handler Function Signature

Every hardware interrupt handler in the Linux kernel follows the same function signature, regardless of the device type:

static irqreturn_t my_device_isr(int irq, void *dev_id);

The two parameters are always the same:

  • irq — the IRQ number that triggered this call, useful mainly for logging or when one handler serves several lines.
  • dev_id — the private pointer you supplied when you registered the handler. This is how your handler gets access to its own device context without relying on global variables.

Registering a Handler: The Modern Way

Older tutorials (and older kernel source) often show request_irq() paired with a manual free_irq() call in the driver’s remove/exit path. On kernel 6.x, the recommended approach for platform and GPIO-style drivers is the resource-managed variant, which automatically frees the IRQ when the owning device is detached — removing an entire class of “forgot to free the IRQ” bugs:

int ret;

ret = devm_request_irq(&pdev->dev, irq_number,
                        my_device_isr,
                        IRQF_TRIGGER_RISING,
                        "my_device_irq",
                        my_dev);
if (ret) {
    dev_err(&pdev->dev, "failed to request IRQ %d: %d\n",
            irq_number, ret);
    return ret;
}

Notice there is no matching devm_free_irq() call anywhere — the device-managed (devm_) framework releases the IRQ automatically when the device is unbound, as long as you registered it against the correct struct device *.

devm_request_irq() Parameters at a Glance
dev
owning device
irq
IRQ number
handler
your ISR function
flags
trigger type / sharing
name
shown in /proc/interrupts
dev_id
your private context

Understanding the irqreturn_t Return Value

Your handler must always report back whether it actually handled the interrupt. The kernel uses this to detect misbehaving hardware and spurious interrupts, especially on shared IRQ lines. There are two values you’ll use in almost every driver:

Return Value Meaning
IRQ_HANDLED Your device generated this interrupt and you handled it.
IRQ_NONE This interrupt was not from your device (relevant mainly on shared lines).
IRQ_WAKE_THREAD Handled at the hardware level, but a threaded handler should now run to finish the work.

Threaded Interrupt Handlers: The Preferred Pattern Today

Most real-world drivers on kernel 6.x don’t just register a single hardirq handler — they register a threaded interrupt handler using devm_request_threaded_irq(). This gives you two callback functions instead of one: a quick top-half check that runs in hardirq context, and a second function that runs in its own kernel thread, where sleeping, I2C/SPI transfers, and mutexes are all perfectly safe.

static irqreturn_t my_device_quick_check(int irq, void *dev_id)
{
    struct my_device *mydev = dev_id;

    if (!my_device_irq_is_mine(mydev))
        return IRQ_NONE;

    return IRQ_WAKE_THREAD;
}

static irqreturn_t my_device_thread_fn(int irq, void *dev_id)
{
    struct my_device *mydev = dev_id;

    /* Safe to sleep here: I2C reads, mutex_lock(), etc. */
    my_device_process_event(mydev);

    return IRQ_HANDLED;
}

ret = devm_request_threaded_irq(&pdev->dev, irq_number,
                                 my_device_quick_check,
                                 my_device_thread_fn,
                                 IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                 "my_device_irq", mydev);

The IRQF_ONESHOT flag is important here — it keeps the hardware IRQ line masked until the threaded handler has finished running, which prevents the same interrupt from re-firing before your thread has had a chance to clear it on the device.

Complete Example: A GPIO Button Interrupt Driver

Here is a self-contained, original example (not taken from any book or external source) showing a minimal GPIO button driver using a threaded interrupt handler, written for a modern kernel 6.x build:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>

struct button_dev {
    struct gpio_desc *gpiod;
    int irq;
    unsigned int press_count;
};

static irqreturn_t button_thread_fn(int irq, void *dev_id)
{
    struct button_dev *bdev = dev_id;

    bdev->press_count++;
    dev_info(NULL, "Button pressed, total presses: %u\n",
             bdev->press_count);

    return IRQ_HANDLED;
}

static int button_probe(struct platform_device *pdev)
{
    struct button_dev *bdev;
    int ret;

    bdev = devm_kzalloc(&pdev->dev, sizeof(*bdev), GFP_KERNEL);
    if (!bdev)
        return -ENOMEM;

    bdev->gpiod = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
    if (IS_ERR(bdev->gpiod))
        return PTR_ERR(bdev->gpiod);

    bdev->irq = gpiod_to_irq(bdev->gpiod);
    if (bdev->irq < 0)
        return bdev->irq;

    ret = devm_request_threaded_irq(&pdev->dev, bdev->irq,
                                     NULL, button_thread_fn,
                                     IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                     "gpio_button", bdev);
    if (ret)
        return ret;

    platform_set_drvdata(pdev, bdev);
    return 0;
}

static const struct of_device_id button_of_match[] = {
    { .compatible = "ep,gpio-button" },
    { }
};
MODULE_DEVICE_TABLE(of, button_of_match);

static struct platform_driver button_driver = {
    .probe = button_probe,
    .driver = {
        .name = "ep_gpio_button",
        .of_match_table = button_of_match,
    },
};
module_platform_driver(button_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala sample GPIO button interrupt driver");

Notice the top-half callback is simply NULL here — this is a valid and common pattern when your device doesn’t need a quick hardirq-level check, and you want every press to go straight to the threaded handler.

Real-World Use Cases

  • GPIO buttons and switches — exactly like the example above, common in embedded boards and IoT devices.
  • I2C/SPI sensor interrupts — accelerometers and touchscreens that need to read data over a bus, which requires sleeping, so a threaded handler is mandatory.
  • Network interface cards — typically pair a very fast hardirq handler with NAPI polling for high packet rates.
  • UART/serial controllers — handle incoming byte data and wake up any process blocked on a read.

Common Mistakes and Troubleshooting

  • Calling sleeping functions in the hardirq (top-half) callback — you’ll see a “scheduling while atomic” kernel warning. Move that code to the threaded function instead.
  • Forgetting IRQF_ONESHOT with a NULL top half — this flag is required whenever you don’t provide a hardirq handler, otherwise registration will fail.
  • Not checking the return value of devm_request_threaded_irq() — always propagate the error back from probe().
  • Interrupt never fires — verify with cat /proc/interrupts that the count is registered at all, then double-check your device tree trigger type matches your flags.

Best Practices

  • Prefer devm_request_irq() / devm_request_threaded_irq() over the legacy request_irq() in new driver code.
  • Use threaded handlers whenever the device work involves bus transfers (I2C/SPI) or anything that might sleep.
  • Give your IRQ a descriptive name — it shows up in /proc/interrupts and saves debugging time later.
  • Always validate dev_id and confirm the interrupt genuinely belongs to your device before touching shared resources.

Performance and Security Considerations

Performance: Threaded handlers add a small scheduling latency compared to doing everything in hardirq context, but this trade-off is almost always worth it for correctness and system responsiveness, since it keeps other interrupts from being blocked.

Security: Because dev_id is a raw pointer you control, always make sure it points to memory that stays valid for the entire lifetime the IRQ is registered — using devm_kzalloc() for your device structure, as shown in the example, ties its lifetime safely to the device itself.

Summary / Key Takeaways

  • Every interrupt handler follows the signature irqreturn_t handler(int irq, void *dev_id).
  • Use devm_request_irq() or devm_request_threaded_irq() on modern kernels instead of the legacy, manually-freed APIs.
  • Return IRQ_HANDLED, IRQ_NONE, or IRQ_WAKE_THREAD correctly — the kernel relies on this for shared IRQ detection.
  • Threaded handlers are the standard pattern for any device work that needs to sleep.

Conclusion

Writing a correct linux kernel interrupt handler is really about respecting the boundaries of hardirq context and choosing the right registration API for your device’s needs. With devm_request_threaded_irq() and the GPIO button example above, you now have a working template you can adapt for buttons, sensors, or any interrupt-driven peripheral on a modern embedded Linux board. This lecture is part of EmbeddedPathashala’s free Linux device drivers course — keep going to the next lecture where we look at bottom halves in more depth: tasklets, workqueues, and softirqs.

FAQ

Q1. What’s the difference between request_irq() and devm_request_irq()?
They register interrupts the same way, but devm_request_irq() ties the IRQ’s lifetime to the device, so the kernel frees it automatically on removal — you don’t need a manual free_irq() call.

Q2. When should I use a threaded interrupt handler instead of a plain one?
Whenever your interrupt processing needs to sleep — for example, reading a sensor over I2C/SPI, taking a mutex, or allocating memory with GFP_KERNEL.

Q3. Can the top-half handler in devm_request_threaded_irq() be NULL?
Yes, and it’s common when every press/event should go straight to the threaded function. You must pass IRQF_ONESHOT in that case.

Q4. What does IRQ_WAKE_THREAD actually do?
It tells the kernel that the hardirq portion is done and the registered thread function should now be woken up to complete the work.

Q5. Why did my driver fail to load with an IRQ error?
Common causes include an invalid or unmapped IRQ number, a mismatched trigger type between your flags and the device tree, or missing IRQF_ONESHOT with a NULL top-half handler.

Q6. Is it safe to allocate memory inside my ISR?
Not in the hardirq (top-half) portion. Inside a threaded handler function, sleeping allocations like GFP_KERNEL are safe.

Continue the Free Linux Device Drivers Course

Next up: Top and Bottom Halves — tasklets, workqueues, and softirqs explained.

Go to Next Lecture

← Previous Lecture    Next Lecture →

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *