Mastering interrupt handling in your kernel
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 |
|---|---|
| 1 | Immediate execution of critical ISRs (e.g., the PIT) |
| 2 | Flexible queuing and prioritisation for non‑critical ISRs |
| 3 | Interrupt‑related statistics (processing time, trigger frequency, drop count) |
| 4 | Simple, 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
| Metric | Insight |
|---|---|
| avgTimeElapsed / min / max | Which ISR consumes the most CPU time, and how much variance there is. |
| avgTrigPerSecond | Frequency of each interrupt – useful for spotting runaway timers. |
| trigCount vs. completeCount | Difference indicates how often an ISR was entered but never finished (e.g., pre‑empted). |
| droppedCount | Over‑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
IsrDoStatsflag. - 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:
| Field | Description |
|---|---|
| IRQ no. | Number of the interrupt |
| Handler address & symbol | Address (hex) and function name |
| Context‑resolution function address & symbol | Address (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 timestamp | Time of the most recent trigger |
| – average / minimal / maximal time elapsed | Measured in µs |
– total time elapsed (time_tot) | avgTimeElapsed * trigCount (u64) |
| – average frequency | Triggers per second, averaged over the lifetime |
| – flags | Human‑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.