Mastering interrupt handling in your kernel

Published: (December 23, 2025 at 08:02 PM EST)
7 min read
Source: Dev.to

Source: Dev.to

Introduction

Building a kernel from scratch is one thing. Mastering its interrupt system is another. In this article we’ll walk through the design, implementation, and fine‑tuning of a full‑featured interrupt handling subsystem for a custom kernel – complete with queues, priorities, and statistics collection, all without relying on external multitasking frameworks.

Primary Goals

#Goal
1Immediate execution of critical ISRs (e.g., the PIT)
2Flexible queuing and prioritisation for non‑critical ISRs
3Interrupt‑related statistics (processing time, trigger frequency, drop count)
4Simple, maintainable API for registering and handling ISRs

ISR Parameter Block

The core of the system is the ISR parameter block (isrpb_t).
It stores configuration flags, runtime statistics, a pointer to the handler, and an optional context‑resolution callback.

/* kernel_isrpb.h */
struct kernel_isrpb {
    ISR  handler;                                   /* ISR entry point */

    /* Runtime statistics */
    unsigned int avgTimeElapsed;    /* moving average of execution time (ticks) */
    unsigned int avgTrigPerSecond; /* average trigger frequency (Hz)          */
    unsigned int maxTimeElapsed;   /* longest observed execution time          */
    unsigned int minTimeElapsed;   /* shortest observed execution time         */

    unsigned int trigCount;        /* total number of triggers                 */
    unsigned int trigCountTmp;    /* temporary counter for frequency calc.   */
    unsigned int completeCount;   /* number of successful executions         */
    unsigned int trigTimestamp;   /* timestamp of the last trigger            */
    unsigned int droppedCount;     /* how many times the ISR was dropped      */

    unsigned int flags;            /* configuration flags (see below)         */

    /* Optional extra context resolver */
    void* (*additionalContextResolv)(struct kernel_isrpb* param);
} __attribute__((packed));

Using a unified block lets us manage every interrupt consistently and extend capabilities later.

Queue & Prioritisation

Non‑critical ISRs are placed into a FIFO circular buffer.
The queue supports three priority levels derived from the ISR flags:

/* Priority helper */
static inline unsigned int isr_getPriority(const isrpb_t *isr)
{
    if (isr->flags & IsrPriorityCrit)   return 0;          /* highest */
    return (isr->flags & IsrPriorityHigh) ? 1 : 2;      /* medium / low */
}

Auto‑take from the queue

The scheduler always picks the highest‑priority ISR that is ready:

/* Return the next ISR, or NULL if the queue is empty */
isrpb_t* isr_queue_autotake(void)
{
    for (unsigned int priority = 0; priority trigCount++;
    isr->trigCountTmp++;
    isr->trigTimestamp = pit_get();          /* current timer tick */

    /* Re‑entrancy protection */
    if ((isr->flags & IsrActive) && !(isr->flags & IsrReentrant))
        return;                              /* already running – ignore */

    isr->flags |= IsrActive;                 /* mark as active */

    u32 start = 0;
    int collectStats = (isr->flags & IsrDoStats) && !(isr->flags & IsrPriorityCrit);
    if (collectStats) start = isr->trigTimestamp;

    /* Call the actual handler */
    ISR handler = isr->handler;
    handler(reg);

    /* Update statistics if requested */
    if (collectStats) isr_doStats(isr, start);

    isr->flags &= ~IsrActive;               /* clear active flag */
    isr->completeCount++;                   /* successful execution */
}

Queue processing & IRQ entry point

/* Process the next ISR from the queue */
static void isr_processQueue(void)
{
    isrpb_t *isr = isr_queue_autotake();
    if (isr) isr_dispatch(isr, NULL, 1);
}

/* Central IRQ handler (called by the CPU on every interrupt) */
void isr_irqHandler(REGISTERS *reg)
{
    /* ... decode the interrupt, locate the matching ISR block ... */
    unsigned int priority = isr_getPriority(isr);

    /* Critical ISR or empty queue → run immediately */
    if (isr_canRunNow(priority))
        goto run_now;

    /* Otherwise enqueue */
    isr_queue_push(isr);
    return;

run_now:
    isr_dispatch(isr, reg, 1);
    isr_processQueue();    /* keep the system responsive */
}

Critical ISRs (e.g., the PIT) bypass the queue and are executed instantly, guaranteeing the shortest possible latency.

Statistics Collection

For each ISR we keep a small set of runtime metrics.
All statistics except droppedCount are updated only for non‑critical ISRs and only when the IsrDoStats flag is set.

Execution‑time statistics

/* Called from isr_doStats() after an ISR finishes */
static void isr_updateTimeStats(isrpb_t *isr, u32 elapsed)
{
    if (isr->completeCount > 0) {
        /* Moving average */
        isr->avgTimeElapsed = ((isr->avgTimeElapsed * isr->completeCount) + elapsed)
                               / (isr->completeCount + 1);

        if (elapsed > isr->maxTimeElapsed) isr->maxTimeElapsed = elapsed;
        if (elapsed minTimeElapsed) isr->minTimeElapsed = elapsed;
    } else {
        /* First measurement */
        isr->avgTimeElapsed = isr->minTimeElapsed = isr->maxTimeElapsed = elapsed;
    }
}

Drop‑count handling

When the circular buffer is full, the ISR cannot be queued and is counted as dropped:

/* Inside isr_queue_push() */
if (next == queue->head) {
    /* Queue full – overload condition */
    isr->droppedCount++;
    return;
}

Trigger‑frequency statistics

/* Update average trigger frequency (Hz) */
static void isr_updateFreqStats(isrpb_t *isr, u32 now)
{
    u32 elapsed = now - isr->trigTimestamp;
    if (elapsed == 0) elapsed = 1;               /* avoid division by zero */

    if (elapsed >= PIT_FREQUENCY) {
        isr->avgTrigPerSecond = (isr->trigCountTmp * PIT_FREQUENCY) / elapsed;
        isr->trigCountTmp = 0;                    /* reset temporary counter */
    }
}

What the Data Tells You

MetricInsight
avgTimeElapsed / min / maxWhich ISR consumes the most CPU time, and how much variance there is.
avgTrigPerSecondFrequency of each interrupt – useful for spotting runaway timers.
trigCount vs. completeCountDifference indicates how often an ISR was entered but never finished (e.g., pre‑empted).
droppedCountOver‑load conditions – helps you size the queue or optimise ISR latency.

By analysing these numbers you can identify “hot” interrupts, balance priorities, and tune the kernel for deterministic real‑time behaviour.

Summary

  • A single, packed ISR parameter block (isrpb_t) stores all configuration and runtime data.
  • Three‑level priority (critical → high → low) drives both immediate execution and queue ordering.
  • Circular FIFO queues keep the system responsive while respecting priorities.
  • Dispatch logic updates statistics, protects against re‑entrancy, and respects the IsrDoStats flag.
  • Statistics (execution time, frequency, drop count) give deep insight into interrupt behaviour, enabling data‑driven optimisation.

With this foundation you have a robust, extensible interrupt subsystem that can be built upon for more advanced features (nested interrupts, per‑CPU queues, etc.) while keeping the kernel lean and maintainable.

Overview

When the patch and registration mechanisms were in place, the system ran smoothly.
Even though I haven’t implemented multitasking yet, this design lets the kernel:

  • Maintain predictable and measurable behavior.
  • Make debugging of device drivers easier.

To better understand and debug interrupt behavior, I added an IRQ Lookup utility to the kernel.

Usage

1. List all registered interrupt handlers

irqlookup          # no arguments

The utility iterates through all registered interrupt handlers and prints key information for each ISR in the following format:

IRQ_BASE+ -> (Handler address (hex)) (Handler function symbol name):
    -> avgTps=X avgTE=X minTE=X maxTE=X, lastTrig=X cc=X tc=X dc=X flags=X
  • avgTps – average triggers per second
  • avgTE – average time elapsed (µs)
  • minTE / maxTE – minimal / maximal time elapsed (µs)
  • lastTrig – timestamp of the last trigger
  • cc – acknowledge count
  • tc – complete count
  • dc – drop count
  • flags – flag bits (shown as a raw integer)

2. Show detailed information for a specific IRQ

irqlookup --show-irq <IRQ_NUMBER>

The command prints a detailed summary of the ISR Parameter Block, including:

FieldDescription
IRQ no.Number of the interrupt
Handler address & symbolAddress (hex) and function name
Context‑resolution function address & symbolAddress (hex) and name of the helper that resolves additional context
Stats
– acknowledge count (trigCount)How many times the IRQ was raised
– complete count (completeCount)How many times the ISR finished successfully
– drop count (dropCount)How many times the ISR was aborted
– last timestampTime of the most recent trigger
– average / minimal / maximal time elapsedMeasured in µs
– total time elapsed (time_tot)avgTimeElapsed * trigCount (u64)
– average frequencyTriggers per second, averaged over the lifetime
– flagsHuman‑readable flag names (see Flag decoding below)

Total time elapsed calculation

u64 time_tot = ((p->flags & IsrDoStats) && !(p->flags & IsrPriorityCrit) && p->avgTimeElapsed != 0)
               ? (p->completeCount * p->avgTimeElapsed)
               : 0;

3. Decode a raw flags integer

irqlookup --decode-flags <FLAGS_INTEGER>

The utility translates the integer into a space‑separated list of flag names.

Flag‑name helper

const char* flagName(unsigned int bit) {
    switch (bit) {
        case IsrActive:      return "IsrActive";
        // …
        case IsrWakeup:      return "IsrWakeup";
        default:             return "Unknown";
    }
}

Printing the set flags

int first = 1;
for (unsigned int bit = 1; bit != 0; bit flags & bit) {
        if (!first) callback_stdout("\n                         ");
        callback_stdout((char*)flagName(bit));
        first = 0;
    }
}

Design Rationale

Mastering interrupt handling in a kernel isn’t just about making ISRs run; it’s also about observing, controlling, and prioritizing them effectively.

  • Queue‑based priority system – Guarantees that critical interrupts are serviced immediately, while less critical ones wait their turn, preserving system stability and predictability.
  • IRQ Lookup utility – Provides real‑time visibility into the interrupt subsystem, enabling developers to:
    • Inspect which ISRs are registered and active.
    • Monitor execution statistics (latency, frequency, counts).
    • Verify flags and handler states to ensure correct behavior.

Together, these mechanisms deliver both robustness and transparency for kernel developers.

Back to Blog

Related posts

Read more »