Interrupts: How Hardware Gets the CPU's Attention
A network card cannot call a function. It raises a line, and the CPU abandons what it was doing at the next instruction boundary. The handler's instruction count is the smallest part of what that costs.
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
The only way in
A CPU core executes one instruction stream. Nothing in that stream knows a packet arrived, a disk finished writing, or a timer expired. Those events happen on other clocks entirely, and the core has no way to observe them except by being *told*.
The telling is a physical signal. The device asserts an interrupt request; an interrupt controller decides whether this core should take it now, given what else is pending and what priority the core is currently masking. If it should, the core takes the interrupt at the next instruction boundary — not immediately, and not mid-instruction. In-flight work either retires or is discarded, which is the first hidden cost: an out-of-order core may have dozens of instructions in flight, and taking an interrupt means giving up on the speculative ones.
Then the hardware does the minimum handoff it can: save enough architectural state to resume, raise the privilege level, and jump to the address the vector table holds for that interrupt number. Everything after that is software. The OS decides how much work to do now and how much to defer, and that decision is the main lever anyone actually has (see Why Kernel Mode Is Actually Privileged for the mode transition itself).
What an interrupt actually costs
Ask an engineer what an interrupt costs and you will usually get the handler's instruction count. That is the part you can see in source, and it is routinely the smallest term. The costs that dominate are the ones no code expresses.
The pipeline drains: speculative work in flight is discarded, and the front end restarts cold on the handler. The privilege transition has its own fixed cost. Then the handler executes — touching driver structures, device registers and queues that the interrupted thread had no reason to have cached. When control returns, the original thread resumes with its working set partially evicted and its TLB entries displaced. It runs slower for a while, and nothing about that shows up as "interrupt time" in any accounting.
The scale below is deliberately unitless, because the ratios are what transfer between machines. On one particular server the absolute numbers might be tens of nanoseconds; on an embedded core they will differ by an order of magnitude, and the balance between the terms shifts with pipeline depth and cache size.
Where the cost lands, and who pays it
The thread that suffers an interrupt is not the thread that wanted the I/O. It is whichever thread happened to be scheduled on that core. This is why interrupt-heavy machines show latency spikes on workloads that do no I/O at all, and why the spikes are so hard to attribute — the victim's profile shows time in its own code, just more of it, because it is now running against a colder cache.
Operating systems respond by splitting the work: do the absolute minimum in the interrupt context — acknowledge the device, note what happened — and defer the rest to a context that can be scheduled and accounted for. The taxonomy differs by OS, but the shape is universal: a small, urgent, uninterruptible piece and a larger, deferrable piece.
Which gives you the two real levers. Coalescing: tell the device to raise one interrupt for many events instead of one per event, trading latency for throughput. Affinity: steer a device's interrupts at specific cores so that latency-critical threads run somewhere else. Both are configuration, not code, which is why they are so often overlooked by the people writing the code that suffers.
| Context | What runs there | Can it be preempted? | Who pays |
|---|---|---|---|
| Interrupt (hard IRQ) | Acknowledge the device, record the event, schedule the rest | No — keep it tiny | Whatever thread was on that core |
| Deferred (soft IRQ, tasklet, DPC) | Protocol processing, buffer handling | Partly, and it is accounted separately | Still that core, but interruptibly |
| Thread (kernel or user) | Everything that can wait | Yes, fully scheduled | Charged to a real thread you can see |
Key points
- An interrupt is taken at an instruction boundary, not immediately — in-flight speculative work is discarded first.
- The handler's instruction count is the smallest component of what an interrupt costs.
- The largest component is cache and TLB displacement, and it is paid *after* the handler returns.
- The cost lands on whatever thread was running on that core, which is usually not the one doing I/O.
- The two practical levers are coalescing (latency for throughput) and interrupt affinity (isolate latency-critical cores).
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Device → interrupt controller: the device asserts a request; the controller applies priority and masking to decide whether any core should take it now.
- 2Controller → core: the core accepts at the next instruction boundary; in-flight speculative instructions are squashed rather than completed.
- 3Core → vector table: hardware saves a minimal architectural context, raises the privilege level and jumps to the handler address registered for that vector.
- 4Handler → device: the driver acknowledges the device so it stops asserting, does the minimum, and defers the remainder to a schedulable context.
- 5Core → interrupted thread: state is restored and execution resumes — into a drained pipeline, a partially evicted cache and a displaced TLB.
- • "The handler is 200 instructions, so an interrupt costs 200 instructions." It costs that plus a pipeline drain, a privilege transition and the cache the victim thread has to rebuild.
- • "Interrupt overhead shows up as interrupt time." Most of it shows up as the *interrupted* thread running slower afterwards, attributed to that thread's own code.
- • "The core is only 30% busy, so interrupts are not a problem." Utilisation counts the handler; it does not count the slowdown the handler leaves behind.
- • "Interrupts are a kernel concern." They set the tail latency of user-space threads that never issue a syscall.
Consequences, controls and cost
- • Threads doing no I/O show latency spikes on interrupt-heavy cores, with profiles that just show "more time in my own code".
- • A core saturated by interrupts converts a large fraction of its cycles into overhead, so throughput falls while utilisation looks high.
- • High packet or completion rates can make interrupt delivery itself the bottleneck, which is the situation polling exists to solve.
- • Interrupt-driven wakeups add scheduling latency that is invisible to the code doing the waiting.
- • Enable interrupt coalescing on high-rate devices — one interrupt per batch instead of per event. This is the single biggest lever and it is a device setting, not a code change.
- • Pin device interrupts away from latency-critical cores with IRQ affinity, so the threads that must not stutter never take them.
- • Keep handler work minimal and push everything deferrable into a schedulable context, so the cost is at least attributable.
- • At very high event rates, stop taking interrupts at all and poll instead — see [[polling-vs-interrupts]].
- • Per-core interrupt counts over time, split by source — an unbalanced distribution is usually accidental rather than designed.
- • Time attributed to hard and deferred interrupt contexts per core.
- • Run the same latency-sensitive thread on an interrupt-heavy core and a quiet one and compare the tail; the difference is the cost nothing else attributes.
- • Cache miss counters on the victim thread before and after moving device interrupts elsewhere.
- • Coalescing raises latency for every event in the batch in exchange for far fewer interrupts — wrong for a latency-critical control path, right for bulk throughput.
- • Interrupt affinity constrains the scheduler and can leave cores idle that could have helped, in exchange for predictability.
- • Deferring work makes accounting honest but adds a scheduling hop, which is latency the interrupt context would not have paid.
Scope
§224 — what these claims are specific to.
- GENERALThe mechanism — asynchronous signal, instruction-boundary delivery, vectored handler, deferred work — is common to essentially all general-purpose CPUs.
- PLATFORM-SPECIFICController design, vector table format, priority and masking rules differ substantially between x86-64 (APIC), AArch64 (GIC) and embedded designs. The deferred-work taxonomy is an OS choice, not a hardware one.
- SIMPLIFIEDThe cost scale omits interrupt-remapping, virtualisation-induced exits, and the extra flushes some speculative-execution mitigations impose on privilege transitions.
Misconceptions
Where the rest of this lives
An interrupt handler runs in the middle of a thread that holds no lock and expects no concurrency, which is why handler code has such severe restrictions on what it may touch.