When I first started poking around inside a CPU with a debugger open, the thing that confused me most wasn’t the registers or the instruction set — it was interrupts. Something external (a keyboard press, a timer tick, a disk finishing a read) could just barge into whatever the processor was doing and demand attention right now. And somehow, when five things demanded attention at once, the CPU always seemed to know which one mattered more. That “knowing” is interrupt prioritization, and once you understand how it works at the assembly level, a huge chunk of how operating systems and embedded firmware operate suddenly clicks into place.
In this post I’m going to walk through interrupt prioritization from the ground up — what interrupts actually are, how priority is assigned and enforced in hardware and software, how this looks in x86/x86-64 and ARM assembly, and how you’d actually debug and optimize interrupt-heavy code in the real world.
What Is an Interrupt, Really?
An interrupt is a signal that tells the processor to pause its current instruction stream and jump to a special routine called an Interrupt Service Routine (ISR) or Interrupt Handler. Interrupts come in two broad flavors:
- Hardware interrupts — generated by external devices (timers, keyboards, network cards, disk controllers) or internal events (divide-by-zero, page faults).
- Software interrupts — triggered deliberately by an instruction, like
INT 0x80on Linux x86 for a system call, orSVCon ARM.
The reason prioritization matters is simple: interrupts are asynchronous. A disk controller and a keyboard controller don’t coordinate with each other before firing a signal at the CPU. If two or more interrupts arrive close together, or one arrives while another is already being handled, the CPU needs a deterministic rule for deciding which one wins.
Why Priority Exists at All
Imagine a system with no prioritization at all — interrupts serviced strictly in arrival order. A low-importance interrupt (say, a mouse movement) could delay a critical one (say, a power-failure signal telling the system to save state before it dies). That’s unacceptable in any real system, so hardware designers built priority schemes directly into the interrupt controller hardware, and operating system designers layered software policies on top.
The Hardware Layer: Interrupt Controllers
The Legacy PIC (8259A)
On classic x86 systems, the Programmable Interrupt Controller (8259A) managed up to 8 interrupt lines (IRQ0–IRQ7), with a second cascaded PIC extending this to 15 usable lines. Priority was fixed by line number by default: IRQ0 (the system timer) had the highest priority, IRQ7 the lowest, unless you reprogrammed the priority rotation.
The Modern APIC
Modern x86 systems replaced the PIC with the Advanced Programmable Interrupt Controller (APIC), split into a Local APIC (one per core) and an I/O APIC (routes external interrupts). Each interrupt vector carries an 8-bit number, and priority is derived from the upper 4 bits of that vector: Priority Class = Vector / 16. Vectors 0–31 are reserved for CPU exceptions (which are effectively highest priority, non-maskable in most cases), while vectors 32–255 are available for hardware and software interrupts.
The Local APIC has a register called the Task Priority Register (TPR) — software can write to this to say “don’t bother me with anything below priority class N,” effectively giving the OS fine-grained masking control.
ARM’s Nested Vectored Interrupt Controller (NVIC)
In ARM Cortex-M microcontrollers, the NVIC is the star of the show. Each interrupt source has a configurable priority level (commonly 3 to 8 bits, i.e., 8 to 256 priority levels depending on silicon). Two ARM-specific ideas matter here:
- Preemption priority — determines whether a new interrupt can interrupt an already-running ISR.
- Sub-priority — determines ordering only among pending interrupts of the same preemption priority; it does not allow preemption.
This split, controlled via the AIRCR register’s PRIGROUP field, gives embedded developers very deliberate control over exactly which ISRs can nest inside others.
Priority Mechanics: Masking, Nesting, and Vectoring
Three mechanisms combine to implement prioritization:
- Masking — the CPU (or controller) can temporarily disable interrupts at or below a certain priority using flags or registers. On x86, the
IFflag inEFLAGSglobally enables/disables maskable interrupts viaCLI/STI. On ARM, thePRIMASK,BASEPRI, andFAULTMASKspecial registers let you mask by priority level rather than all-or-nothing. - Nesting — a higher-priority interrupt can preempt a lower-priority ISR that’s currently executing. This requires the CPU to save enough context (return address, flags, sometimes registers) to resume the interrupted handler later.
- Vectoring — each interrupt maps to a fixed memory address (a vector) pointing to its handler, looked up via an Interrupt Vector Table (IVT) or Interrupt Descriptor Table (IDT). Vector number often implies priority class, especially on x86.
x86 Assembly Example: Masking Interrupts
; Disable interrupts before a critical section
cli ; Clear Interrupt Flag — mask maskable interrupts
mov eax, [shared_counter]
inc eax
mov [shared_counter], eax
sti ; Set Interrupt Flag — re-enable interrupts
Note that CLI/STI only affect maskable interrupts (delivered via INTR). The Non-Maskable Interrupt (NMI) line bypasses this entirely — by design, because NMIs are reserved for catastrophic conditions like hardware failure.
x86-64: Setting Up the IDT with Priority-Bearing Vectors
; Simplified IDT entry setup for vector 0x21 (IRQ1 - keyboard, remapped)
setup_idt_entry:
mov rax, keyboard_isr
mov [idt_entry_0x21], ax ; offset low
shr rax, 16
mov [idt_entry_0x21+6], ax ; offset high
mov word [idt_entry_0x21+2], 0x08 ; code segment selector
mov byte [idt_entry_0x21+5], 0x8E ; present, DPL=0, 32-bit interrupt gate
ret
The vector number chosen (0x21 here) directly determines the interrupt’s priority class under APIC because of that vector / 16 rule mentioned earlier.
ARM Cortex-M Assembly Example: Setting NVIC Priority
; Set priority 2 for IRQ5 using NVIC_IPR registers (memory-mapped)
LDR R0, =NVIC_IPR1 ; IPR1 covers IRQ4-7
LDR R1, =0x00000200 ; priority value shifted into IRQ5's byte field
STR R1, [R0]
; Globally mask interrupts below priority 3 using BASEPRI
MOV R0, #3
MSR BASEPRI, R0
BASEPRI is elegant because it lets you mask by priority level rather than turning all interrupts off like CPSID i (ARM’s equivalent of CLI) would.
Internal Working Process
Here’s the general flow the CPU follows when handling competing interrupt requests:
flowchart TD
A[Interrupt request arrives] --> B{Is CPU accepting interrupts?}
B -- No, masked --> C[Request held pending]
B -- Yes --> D{Higher priority than<br/>currently running ISR?}
D -- No --> C
D -- Yes --> E[Save current context:<br/>PC, flags, minimal registers]
E --> F[Look up vector in IDT/IVT]
F --> G[Jump to ISR at vector address]
G --> H[ISR executes]
H --> I{New higher-priority<br/>interrupt arrives?}
I -- Yes --> E
I -- No --> J[ISR completes: IRET/BX LR]
J --> K[Restore previous context]
K --> L{Any pending interrupts?}
L -- Yes, highest priority next --> D
L -- No --> M[Resume normal execution]
Priority Comparison Table
| Mechanism | x86/x86-64 | ARM Cortex-M |
|---|---|---|
| Controller | APIC (Local + I/O) | NVIC |
| Priority source | Vector number (vector/16 = class) | Configurable priority register per IRQ (3–8 bits) |
| Global mask | EFLAGS.IF via CLI/STI | PRIMASK (all) / FAULTMASK (faults) |
| Priority-based mask | TPR in Local APIC | BASEPRI register |
| Non-maskable path | NMI pin/vector 2 | HardFault, NMI exception (fixed priority -2/-1) |
| Nesting support | Yes, via APIC + software | Yes, natively via NVIC preemption priority |
| Sub-priority (tie-break) | Software-defined | Hardware sub-priority field |
Practical Use Cases
- Real-time operating systems (RTOS) rely heavily on interrupt priority to guarantee that time-critical tasks (motor control, safety shutoffs) always preempt less urgent ones (logging, UI updates).
- Device drivers in Linux and Windows kernels register handlers at specific IRQ lines, and the kernel’s interrupt priority scheme decides how quickly a driver responds under load.
- Power management — many chips use interrupt priority to wake from sleep states only for high-priority events, saving power by ignoring or deferring low-priority ones.
Operating System Interaction
Operating systems build top halves and bottom halves (Linux terminology) or Deferred Procedure Calls (Windows) on top of hardware interrupt priority. The idea: the ISR itself runs at high priority and does the absolute minimum (acknowledge the device, grab a timestamp, queue data), then it schedules a lower-priority “bottom half” to do the heavier processing. This keeps high-priority interrupt latency low even when the total work triggered by an interrupt is substantial.
Debugging and Performance Considerations
- Interrupt latency — the time between a request and the first instruction of its ISR executing — is the number you actually care about in real-time systems. Longer critical sections (
CLI…STIblocks) directly increase worst-case latency for everything else. - Priority inversion — a classic bug where a low-priority ISR holds a resource a high-priority one needs, effectively capping the high-priority interrupt’s responsiveness. Watch for shared locks or disabled-interrupt sections that are too broad.
- Debugging tools — logic analyzers and trace units (like ARM’s ETM) can timestamp exact interrupt entry/exit; on x86, tools like
perfcombined with APIC performance counters expose interrupt counts and latencies. - Common mistake: leaving interrupts globally disabled for too long inside an ISR “just to be safe,” which defeats the entire purpose of having a priority scheme.
Best Practices
- Keep ISRs short — defer heavy work to a lower-priority task or bottom-half mechanism.
- Assign priorities based on real deadlines, not gut feeling — profile first.
- Avoid nested
CLIwithout tracking nesting depth; mismatchedSTIcalls are a classic source of “interrupts mysteriously stayed off” bugs. - Use priority-based masking (
BASEPRI,TPR) instead of global masking whenever you only need to block a subset of interrupts. - Document your vector table and priority assignments — it’s the first thing the next engineer (or you, six months later) will need.
FAQs
Can two interrupts have the exact same priority? Yes — most controllers resolve ties by a secondary rule, often fixed IRQ number order or a configurable sub-priority field (as in ARM’s NVIC).
Does higher priority always mean the interrupt fires immediately? Not if interrupts are currently masked, or if the CPU is inside a non-interruptible instruction sequence, or if a higher-or-equal priority ISR is already executing (depending on preemption settings).
What happens if two interrupts are pending when interrupts are re-enabled? The controller delivers the highest-priority pending one first; the rest wait, still pending, until the CPU is ready again.
Is a Non-Maskable Interrupt “priority infinity”? Effectively yes for delivery purposes — it can’t be masked by the usual mechanisms — but it still can’t interrupt itself, and some architectures let you defer even NMIs briefly through specific fault-masking registers.
Summary and Key Takeaways
Interrupt prioritization is the mechanism that lets a CPU make sane decisions when multiple asynchronous events compete for its attention. At the hardware level it’s implemented through controllers like the APIC (x86) or NVIC (ARM), which assign a priority value — derived from a vector number or an explicit register — to every interrupt source. Software then layers masking (CLI/STI, BASEPRI, TPR) and nesting rules on top to control exactly when and how preemption happens.
Key takeaways:
- Priority is enforced through a combination of vector numbering, dedicated controller hardware, and CPU flags/registers.
- x86 leans on APIC vector-derived priority classes and the
IFflag; ARM Cortex-M gives explicit per-IRQ priority registers via the NVIC. - Keep ISRs short, mask narrowly, and always design with worst-case latency in mind, not just average-case.
- Priority inversion and overly broad interrupt masking are the two mistakes that bite real systems most often.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manuals — Volume 3, Chapter on Advanced Programmable Interrupt Controller (APIC).
- AMD64 Architecture Programmer’s Manual, Volume 2 — System Programming.
- ARM Architecture Reference Manual, Armv7-M and Armv8-M — Nested Vectored Interrupt Controller (NVIC) chapter.
- GNU Binutils and GNU Assembler (GAS) documentation,
asmanual, for interrupt-related directives and syntax.
