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
INTRpin 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
NMIpin and cannot be suppressed by software. They indicate critical conditions such as hardware failures or parity errors and are always dispatched to vector number2.
Internal Interrupts
- Software interrupts (e.g.,
int 0x80orsyscall) 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.
- Faults: Recoverable errors (e.g., page fault at vector
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:
- The CPU reads the corresponding IDT entry using the vector number.
- Privilege level validation is performed; if transitioning to a lower-privilege ring, a stack switch occurs using the Task State Segment (TSS).
- The current
CS,EIP, andEFLAGSare pushed onto the new stack. - For interrupt gates, the CPU clears the
TF(trap flag) andNT(nested task) bits inEFLAGSbefore pushing it. - 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
0x20for 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.