Skip to content

PIC & APIC

Hardware devices signal the CPU asynchronously through interrupt lines, and something has to decide which of those lines actually reaches the CPU, in what priority, and as which interrupt vector. On the earliest PC-compatible hardware that role belonged to the 8259 Programmable Interrupt Controller (PIC); on any machine built for multiprocessing, it belongs instead to the Advanced Programmable Interrupt Controller (APIC), split into a per-CPU Local APIC and a shared I/O APIC.

A single 8259 handles eight interrupt lines, numbered IRQ0 through IRQ7. IBM PC-compatible machines wire two of them together in a master/slave cascade (the slave’s output feeds into one of the master’s own input lines, IRQ2 by convention), extending the total to fifteen usable lines, IRQ0 through IRQ15, with IRQ2 itself no longer available for a device since it is consumed by the cascade.

Both chips are programmed through a pair of I/O ports each: 0x20/0x21 for the master, 0xA0/0xA1 for the slave. Initialization sends a sequence of Initialization Command Words (ICW1 through ICW4) establishing, among other things, which interrupt vector each chip’s IRQ0 maps to: critical on x86, because the PIC’s factory-default mapping places IRQ0 through IRQ7 at vectors 8 through 15, directly colliding with the CPU’s own reserved exception vectors 0 through 31. Every PC-compatible kernel remaps the PIC during initialization, commonly to vectors 32 and 40 for the master and slave respectively, specifically to avoid a hardware interrupt and a CPU exception ever claiming the same vector number.

After initialization, Operation Command Words (OCW1 through OCW3) control day-to-day behavior: OCW1 sets the interrupt mask, one bit per line, letting software disable individual IRQs without touching the CPU’s global interrupt flag; an End-Of-Interrupt command, sent to the command port after servicing an interrupt, tells the PIC it may deliver further interrupts of equal or lower priority. Omitting it leaves the PIC believing an interrupt is still being serviced, and it will not deliver another. A spurious interrupt (IRQ7 on the master or IRQ15 on the slave, arriving with no line actually asserted) is a known artifact of the 8259’s design under specific timing conditions rather than a hardware fault, and a spurious IRQ15 in particular still requires an EOI sent to the master (though not the slave, which never actually raised anything), a detail easy to get backward.

The 8259 model has one interrupt controller delivering to one CPU, which is fundamentally incompatible with a system that has more than one CPU: there is no way to route a given interrupt to whichever CPU should handle it, or to have different CPUs handle different interrupts at all. The APIC architecture splits interrupt handling into two cooperating pieces to solve this. A Local APIC exists once per CPU core, integrated into the processor itself, and is what actually delivers an interrupt vector to that specific core along with handling that core’s own local interrupt sources (the APIC timer, and inter-processor interrupts described below). An I/O APIC (one or more per system, external to any specific CPU) receives interrupts from devices and, through a programmable redirection table, decides which vector and which Local APIC (or which subset of CPUs) each incoming line should be delivered to.

Every CPU’s Local APIC is controlled through a set of memory-mapped registers, whose physical base address is read from the IA32_APIC_BASE model-specific register, by convention 0xFEE00000 unless firmware has relocated it. Key registers include the APIC ID (identifying which CPU this Local APIC belongs to, used as a destination when routing an interrupt), the End-Of-Interrupt register (written, not read, to acknowledge the current interrupt; unlike the PIC’s dedicated command, any value at all written here suffices), the Spurious Interrupt Vector register (which also holds the APIC’s global enable bit), and the APIC Timer’s configuration and count registers, providing a general-purpose timer local to each CPU independent of any shared system timer.

Where the 8259 hardwires which line maps to which vector, an I/O APIC’s redirection table (one 64-bit entry per input line, typically 24 entries) is fully programmable: each entry specifies the target interrupt vector, delivery mode, destination CPU or CPUs, trigger mode (edge or level), and polarity, letting the operating system route a given physical interrupt line to whatever vector and whatever CPU it chooses, and change that routing at runtime. A kernel discovers how many I/O APICs exist, their base addresses, and how legacy ISA IRQ numbers correspond to their input pins by parsing the ACPI MADT (Multiple APIC Description Table) rather than by any fixed convention, since this mapping is not guaranteed identical across different chipsets.

A kernel that has switched to the APIC still needs to deal with the 8259 pair, which remains present and, if left in its default state, can still generate spurious interrupts on the same lines the APIC configuration now expects to be quiet. The straightforward approach masks every line on both PICs by writing 0xFF to both mask registers, leaving the chips physically present but permanently silenced rather than attempting to disable them entirely, which the 8259 provides no clean mechanism for.

Beyond routing device interrupts, the Local APIC provides a capability the 8259 has no equivalent for at all: one CPU can send an interrupt directly to another CPU, or to a group of CPUs, by writing to its own Interrupt Command Register. This is how a kernel implements a TLB shootdown (interrupting other CPUs specifically to have each one flush its own stale translation cache entries), and how a bootstrap CPU brings additional CPU cores online in the first place, through a specific sequence of IPIs known as INIT-SIPI-SIPI: an INIT IPI resets the target core, followed by one or two Startup IPIs (SIPI) carrying the physical address of the code the newly-started core should begin executing, since an application processor otherwise starts in a state with no way to be told where to begin on its own.

Enabling the APIC without also correctly configuring, or explicitly masking, the legacy PIC is a common source of interrupts arriving twice or not at all during bring-up: both controllers can be simultaneously active and both attempting to deliver the same physical line unless the legacy PIC is masked first. Reading the MADT is effectively mandatory rather than optional for correctness: assuming the conventional IRQ0-through-IRQ15-to-vector mapping without checking the MADT’s interrupt source override entries will work on many machines by coincidence and fail silently, in ways that are difficult to distinguish from other bugs, on others where firmware has remapped a legacy IRQ to a different I/O APIC input.

  1. ^ Intel, 8259A Programmable Interrupt Controller Datasheet: defines the ICW/OCW programming model referenced above.
  2. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Chapter 10: defines the Local APIC register set and IPI delivery model.
  3. ^ ACPI Specification, MADT (Multiple APIC Description Table): the firmware-provided source of I/O APIC and interrupt routing information.
  • The IDT: the table an interrupt vector is dispatched through once delivered.
  • System Calls: a software-triggered interrupt path unrelated to the hardware routing described here.
  • Timers: the Local APIC’s own timer, covered separately from its interrupt-routing role here.
  • Multiprocessor Bring-Up: the INIT-SIPI-SIPI sequence sent as an IPI through the Local APIC covered here.