Building a kernel from scratch is one thing. Mastering its interrupt system is another. In this article, We'll talk about how I designed, implemented, and fine-tuned a full-featured interrupt handling system in my custom kernel, complete with queues, priorities, and statistics collection, all without relying on external multitasking frameworks.
The primary goals for the new interrupt handling system were:
- Immediate execution of critical ISRs for devices that just can not wait (for example PIT)
- Flexible queue and prioritization for non-critical ISRs
- Interrupt-related statistics such as processing time, trigger frequency and drop count.
- Simple and maintainable interface for registering and handling ISRs.
The ISR Parameter Block
At the core of the system is the isrpb_t structure - a parameter block for every handler. It stores the statistics, configuration, flags, pointer to handler itself and optional additional context resolution.
struct kernel_isrpb {
ISR handler;
unsigned int avgTimeElapsed;
unsigned int avgTrigPerSecond;
unsigned int maxTimeElapsed;
unsigned int minTimeElapsed;
unsigned int trigCount;
unsigned int trigCountTmp;
unsigned int completeCount;
unsigned int trigTimestamp;
unsigned int droppedCount;
unsigned int flags;
void* (*additionalContextResolv) (struct kernel_isrpb* param);
} __attribute__((packed));
Using the unified block allows to manage interrupts consistently and extend its capabilities.
Queues and priorities
Non-critical ISRs are enqueued using a FIFO circular buffer keeping the system responsive while respecting priorities. The queue supports 3 priority levels determined based on ISR Parameter Block flags (IsrPriorityHigh and IsrPriorityCrit).
if(isr->flags & IsrPriorityCrit) return 0;
return isr->flags & IsrPriorityHigh ? 1 : 2;
I also designed a small function that based on queue takes the ISR with the greatest priority.
isrpb_t* isr_queue_autotake() {
for(unsigned int priority = 0; priority < 3; priority++) {
isrpb_t* isr = isr_queue_pop(priority);
if(isr) return isr;
}
return NULL;
}
Dispatching and execution
The dispatch function handles executing ISRs while updating their statistics, such as average execution time and trigger counts, but more on statistics later.
Critical ISRs bypass the queue and are executed immediately, this also happens when the queue is empty, so the routine is as short as possible.
The system also tracks whether an ISR is currently active by setting IsrActive flag LOW and HIGH and checking it's value to prevent reentrant execution unless explicitly allowed using the IsrReenterant flag.
isr->trigCount++; isr->trigCountTmp++;
isr->trigTimestamp = pit_get();
if((isr->flags & IsrActive) && !(isr->flags & IsrReentrant))
return;
isr->flags |= IsrActive;
u32 start = 0;
// do stats only if IsrDoStats and not IsrPriorityCrit
int doStats = (isr->flags & IsrDoStats) && !(isr->flags & IsrPriorityCrit);
if(doStats) start = isr->trigTimestamp;
ISR handler = isr->handler;
handler(reg);
if(doStats) isr_doStats(isr, start);
isr->flags &= ~IsrActive;
isr->completeCount++;
The dispatch logic executes scheduled when the system iterates to the next ISR through the queue or immediately when an interrupt is fired and the queue is empty. Since PIT (Programmable Interrupt Timer) is active, there's always an interrupt.
void isr_processQueue() {
isrpb_t* isr = isr_queue_autotake();
if(isr) isr_dispatch(isr, NULL, 1);
}
void isr_irqHandler(REGISTERS *reg) {
// ...
// run immediatelly if critical or queue empty
unsigned int priority = isr_getPriority(isr);
if(isr_canRunNow(priority)) goto skip;
isr_queue_push(isr);
return;
skip:
isr_dispatch(isr, reg, 1);
isr_processQueue();
return;
}
Statistics observation
The next important part was measuring how interrupts actually behave in real-time. For each ISR, the system tracks following statistics (stored in ISR Parameter Block):
-
Average, Minimum and Maximum Execution time - the moving average of processing time in ticks (
avgTimeElapsed) and min/max time extremes observed for the ISR (minTimeElapsed,maxTimeElapsed).
if(isr->completeCount > 0) {
isr->avgTimeElapsed = ((isr->avgTimeElapsed * isr->completeCount) + elapsed) / (isr->completeCount + 1);
if(elapsed > isr->maxTimeElapsed) isr->maxTimeElapsed = elapsed;
if(elapsed < isr->minTimeElapsed) isr->minTimeElapsed = elapsed;
return;
}
isr->avgTimeElapsed = elapsed; isr->minTimeElapsed = elapsed; isr->maxTimeElapsed = elapsed;
-
Trigger count - how many times the ISR was invoked (
trigCount). -
Completion count - how many times the ISR actually finished the execution (
completeCount). -
Drop count - how many times the ISR execution was dropped (
droppedCount). The ISR drop actually happens when the system is overloaded and the space in queue is no longer sufficient. This statistic is even logged independently onIsrDoStatsflag and is being done inisr_queue_pushfunction.
if(next == queue->head) {
// system is overloaded and not capable of any new ISR at the moment.
isr->droppedCount++;
return;
}
-
Average trigger frequency - how many times the ISR was invoked per second in average (
avgTrigPerSecond). ThetrigCountTmpandtrigTimestampfields (parameters) are used for calculation of this statistic.
if(elapsed == 0) elapsed = 1;
u32 tdiff = now - isr->trigTimestamp;
if(tdiff >= PIT_FREQUENCY) {
isr->avgTrigPerSecond = (isr->trigCountTmp * PIT_FREQUENCY) / tdiff;
isr->trigCountTmp = 0;
}
These statistics (except the droppedCount) are updated only for non-critical ISRs to avoid skewing by high-frequency timer interrupts, and only if the IsrDoStats flag is set. Critical ISRs (like the PIT), bypass statistics to prevent counter overflow and measurement noise and to allow even better latency.
This data provides deep insight into system behavior. You can see which interrupt dominate the CPU time, how often they are triggered, and how effeciently each handler executes.
Putting it all together
Once the queue, dispatch and registration mechanisms were in place, the system ran smoothly. Even althrough i've not implemented the multitasking yet, this design allows the kernel to maintain predictable and measurable behavior and allows easier debugging of device drivers.
IRQ Lookup: an utility for interrupt handling related diagnosis
To better understand and debug interrupt behavior, I implemented an IRQ Lookup utility in the kernel.
ISR listing
Executing the command with no arguments makes the utility iterate through all registered interrupt handlers and prints key information about each ISR in the following format:
IRQ_BASE+<IRQ no.> -> (Handler address (Base16)) (Handler function symbol name):
-> avgTps=X avgTE=X minTE=X maxTE=X, lastTrig=X cc=X tc=X dc=X flags=X
Detailed summary for ISR specified
Executing the command with --show-irq <IRQ no.> argument generates a detailed summary of what is contained in ISR Parameter Block, that includes:
- IRQ. no, handler address and symbol name
- Additional context resolution function address and symbol name
-
Stats
- IRQ acknowledge count, ISR complete count, ISR drop count
- Last timestamp
- Averge, minimal and maximal time elapsed
-
Total time elapsed - this is calculated in time by multiplying an average time (
avgTimeElapsed) with acknowledge count (trigCount), givingu64 time_tot - Average frequency - how many times the interrupt was invoked per second in average
- Parameter block flags - written using their names as strings.
const char* flagName(unsigned int bit) {
switch (bit) {
case IsrActive: return "IsrActive";
// ...
case IsrWakeup: return "IsrWakeup";
default: return "Unknown";
}
}
u64 time_tot = ((p->flags & IsrDoStats) && !(p->flags & IsrPriorityCrit) && p->avgTimeElapsed != 0)
? (p->completeCount * p->avgTimeElapsed) : 0;
int first = 1;
for(unsigned int bit = 1; bit != 0; bit <<= 1) {
if(p->flags & bit) {
if(!first) callback_stdout("\n ");
callback_stdout((char*) flagName(bit));
first = 0;
}
}
Parameter block flags decode
Executing the command with --decode-flags <Flags integer> argument decodes the flags and outputs them as string in human readable format.
Conclusion
Mastering interrupt handling in kernel is not just about making ISRs run, but it's also about observing, controlling and prioritizing them effectively. Implementing a queue-based priority system ensures that critical interrupts are serviced immediately while less critical ones wait for their turn, maintaining system stability and predictability.
The IRQ Lookup utility complement this design by providing real-time visibility into the interrupt subsystem. With these, I cam:
- Inspect which ISRs are registered and active.
- Monitor execution statistics.
- Verify flags and handler states to ensure correct behavior.
This combination provides both robustness and transparency.


Top comments (0)