Interrupt Handling in Kernel Development: Architecture and Implementation

Modern operating system kernels rely fundamentally on interrupt-driven execution. Interrupts enable asynchronous event handling—such as hardware signals, exceptions, and system calls—without requiring constant polling or blocking waits.

Interrupt Classification

External Interrupts

  • Maskable interrupts arrive via the INTR pin and can be disabled globally using the IF flag in EFLAGS. These originate from peripherals like timers, keyboards, or network interfaces.
  • Non-maskable interrupts (NMI) arrive via the dedicated NMI pin and cannot be suppressed by software. They indicate critical conditions such as hardware failures or parity errors and are always dispatched to vector number 2.

Internal Interrupts

  • Software interrupts (e.g., int 0x80 or syscall) are explicitly triggered by instructions and commonly used for system call entry points.
  • Exceptions arise from exceptional CPU conditions during instruction execution:
    • Faults: Recoverable errors (e.g., page fault at vector 14). The processor re-executes the faulting instruction after resolution.
    • Traps: Synchronous notifications (e.g., breakpoint at vector 3) that occur after instruction completion—ideal for debugging.
    • Aborts: Severe unrecoverable errors (e.g., double fault at vector 8) often indicating memory corruption or descriptor violations.

IDT and Interrupt Dispatch Flow

In protected mode, the CPU uses the Interrupt Descriptor Table (IDT)—a linear array of 8-byte gate descriptors—to locate handlers for each interrupt vector. Unlike the GDT, the IDT allows all entries—including index zero—to be valid.

When an interrupt occurs:

  1. The CPU reads the corresponding IDT entry using the vector number.
  2. Privilege level validation is performed; if transitioning to a lower-privilege ring, a stack switch occurs using the Task State Segment (TSS).
  3. The current CS, EIP, and EFLAGS are pushed onto the new stack.
  4. For interrupt gates, the CPU clears the TF (trap flag) and NT (nested task) bits in EFLAGS before pushing it.
  5. Control transfers to the handler address specified in the IDT entry.

All handlers must conclude with iret, which restores saved registers and resumes execution at the interrupted context.

Legacy PIC Management: 8259A Controller

The Intel 8259A Programmable Interrupt Controller (PIC) manages external maskable interrupts in legacy x86 systems. It supports prioritization, masking, and cascading.

  • A single 8259A handles 8 IRQ lines (IRQ0–IRQ7).
  • Up to 9 controllers can be cascaded (one master + eight slaves), yielding up to 64 distinct vectors (9 × 8 − 9 = 64, accounting for slave IRQ consumption).
  • Each IRQ maps to a configurable vector offset—typically starting at 0x20 for the master PIC.

Note: Modern x86-64 platforms use the Advanced Programmable Interrupt Controller (APIC) or x2APIC for scalability and SMP support, though BIOS/UEFI firmware may retain 8259A compatibility mode for boot-time device initialization.

Kernel Initialization Sequence

PIC Reconfiguration

The default PIC configuration maps IRQs to vectors 0x08–0x0F (master) and 0x70–0x77 (slave), overlapping with CPU exception vectors. To avoid conflicts, remapping is required early in boot:

void configure_legacy_pic() {
    // ICW1: Initialize master
    outb(0x20, 0x11);
    // ICW2: Vector base = 0x20
    outb(0x21, 0x20);
    // ICW3: Slave connected to IRQ2
    outb(0x21, 0x04);
    // ICW4: 8086 mode
    outb(0x21, 0x01);

    // ICW1: Initialize slave
    outb(0xA0, 0x11);
    // ICW2: Vector base = 0x28
    outb(0xA1, 0x28);
    // ICW3: Slave ID = 2
    outb(0xA1, 0x02);
    // ICW4: 8086 mode
    outb(0xA1, 0x01);

    // OCW1: Mask all IRQs except timer (IRQ0)
    outb(0x21, 0xFE);
    outb(0xA1, 0xFF);
}

IDT Setup and Handler Registration

Each IDT entry points to a trampoline routine written in assembly. This wrapper performs register preservation, acknowledges the PIC, pushes the vector number, and invokes the C-level handler:

%macro DECLARE_INTERRUPT_HANDLER 1
section .text
intr_handler_%1:
    push ds
    push es
    push fs
    push gs
    pushad

    ; Send EOI to PIC(s)
    mov al, 0x20
    out 0xA0, al
    out 0x20, al

    push %1
    call interrupt_dispatcher
    add esp, 4
    popad
    pop gs
    pop fs
    pop es
    pop ds
    iret
%endmacro

The C dispatcher looks up the registered handler in a function pointer table:

typedef void (*irq_handler_t)(uint8_t vector);

static irq_handler_t irq_table[256];
static const char* const irq_names[256] = {
    [0] = "#DE Divide Error",
    [1] = "#DB Debug Exception",
    [2] = "NMI Interrupt",
    [3] = "#BP Breakpoint",
    [8] = "#DF Double Fault",
    [14] = "#PF Page Fault",
    [32] = "Timer Tick"
};

void interrupt_dispatcher(uint8_t vector) {
    if (irq_table[vector]) {
        irq_table[vector](vector);
    } else {
        handle_unexpected_interrupt(vector);
    }
}

void init_irq_table() {
    for (int i = 0; i < 256; ++i) {
        irq_table[i] = &default_irq_handler;
    }
}

Loading the IDT

After populating descriptors and configuring PICs, the IDT is loaded into the CPU via lidt:

struct idt_entry {
    uint16_t offset_low;
    uint16_t selector;
    uint8_t zero;
    uint8_t attributes;
    uint16_t offset_high;
} __attribute__((packed));

struct idtr {
    uint16_t limit;
    uint32_t base;
} __attribute__((packed));

void load_idt(struct idt_entry* idt_base, size_t count) {
    struct idtr idtr_val = {
        .limit = (uint16_t)(count * sizeof(struct idt_entry) - 1),
        .base = (uint32_t)idt_base
    };
    asm volatile("lidt %0" :: "m"(idtr_val));
}

Timer Interrupt Configuration

The legacy Programmable Interval Timer (PIT), typically an 8253/8254 chip, generates periodic interrupts via channel 0. Its output is wired to IRQ0, mapped to vector 0x20 post-remapping.

To configure a 100 Hz tick rate (10 ms intervals):

void setup_pit_timer(uint16_t hz) {
    uint16_t divisor = 1193180 / hz; // Input clock: 1.19318 MHz

    // Command byte: channel 0, LSB+MSB latch, mode 2 (rate generator)
    outb(0x43, 0x36);
    outb(0x40, divisor & 0xFF);
    outb(0x40, (divisor >> 8) & 0xFF);
}

Each timer interrupt triggers scheduler decisions, timekeeping updates, and preemption logic—forming the heartbeat of preemptive multitasking.

Tags: x86 interrupts kernel 8259A PIC

Posted on Mon, 11 May 2026 01:54:52 +0000 by dwu