Interrupt handling in IOKit: from device IRQ to driver callback
The full path of an interrupt from the device asserting a line, through the AIC, through XNU's exception handler, to the driver's IOInterruptDispatchSource callback running on a workloop.
When a device wants the CPU's attention — a network packet arrived, a USB endpoint has data, a timer expired — it raises an interrupt. The path from that hardware event to the driver callback that handles it crosses several layers and a couple of synchronization barriers. This article walks the full path.
Step 1: device asserts an interrupt line
The device is wired to an interrupt input on the AIC — the Apple Interrupt Controller. The AIC is the SoC's interrupt aggregator; every interrupt source on the chip (the GPU, every PCIe device, every USB controller, every sensor) routes through it.
apple-oss-distributions/xnuiokit/DriversApple drivers — the AIC driver lives in spirit here even if specific bits are closed.View on GitHub(line —)The AIC has its own priority and routing logic:
- Per-source mask/enable bits.
- Per-source target — which CPU cluster (P or E) receives the interrupt.
- Priority-based dispatch — high-priority interrupts preempt low-priority ones.
When a device's line asserts, the AIC selects a target CPU (typically a P-core for interactive interrupts, can be routed to E-cores for less-time-critical ones), records the source ID, and raises an interrupt on the chosen core.
Step 2: CPU exception → XNU handler
The target CPU drops out of whatever it was doing and traps into its exception vector at EL1. XNU's exception handler (sleh.c on ARM64) decodes the exception:
- Synchronous exception — instruction fault, page fault, syscall.
- IRQ — external interrupt (our case).
- FIQ — fast interrupt, used for very-low-latency things like the kernel debugger.
For an IRQ, the handler:
- Saves the interrupted thread's user-mode register state.
- Reads the AIC's "what fired" register to identify the source.
- Dispatches to the registered handler for that source.
Step 3: registered handler — bottom half vs top half
XNU follows the classic top-half / bottom-half split:
- Top half (the "primary" handler) runs in interrupt context — interrupts disabled on the local CPU, can't sleep, can't take most locks, can't allocate memory. Job: acknowledge the device, mask the interrupt source, schedule the bottom half.
- Bottom half (the "secondary" or "deferred" handler) runs in a normal thread context — can sleep, take locks, allocate. Job: do the actual work — copy the packet, update queues, wake threads waiting on data.
The top half must be fast. Microseconds. Anything that could take longer goes to the bottom half.
Step 4: IOInterruptDispatchSource — IOKit's abstraction
A driver doesn't write the top half directly. IOKit provides IOInterruptDispatchSource — an object that:
- Registers a top-half handler with the kernel.
- Receives the kernel's call when the interrupt fires.
- Schedules a bottom-half callback on the driver's workloop (or, for DriverKit dexts, on a dispatch queue in the userspace process).
The driver author writes:
- An interrupt filter — runs in top-half context, decides "is this for me?", acknowledges the device.
- An interrupt action — runs in bottom-half context, does the real work.
The split lets you separate "must run NOW" from "do the work safely."
Step 5: the workloop — single-threaded execution
Most IOKit drivers use a workloop — a thread that processes events sequentially from a queue. All bottom halves for a single driver run on the same workloop, so the driver's mutable state is implicitly protected: only one event handler runs at a time.
When the top half schedules the bottom half, it's actually enqueueing an event on the workloop. The workloop wakes, takes the event off its queue, calls the registered action.
This single-threaded model is one reason IOKit drivers are usually easier to write than equivalent Linux drivers — you don't need to worry about per-driver locks, because the workloop guarantees serial execution.
DriverKit: same shape, different process
A DriverKit dext (see the DriverKit article) gets the same IOInterruptDispatchSource abstraction, but the dispatch queue is a regular GCD queue in the userspace dext process — not a kernel workloop.
The path is:
- Hardware interrupt → AIC → CPU trap → kernel handler (same as in-kernel).
- Kernel top-half acknowledges, sends a Mach message to the dext's user-mode server.
- DriverKit framework in the dext process receives the message, dispatches to the registered handler on the queue.
- Driver code runs in userspace.
The extra Mach hop adds latency (a few microseconds). For USB peripherals this is fine; for high-end audio it can be unacceptable, which is why Core Audio's lowest-latency paths still run in-kernel.
Per-CPU interrupt routing
The AIC can route specific interrupts to specific CPU clusters. XNU uses this to:
- Keep latency-sensitive interrupts (touch, audio) on P-cores.
- Route low-priority device polling to E-cores.
- Balance load across cores when many devices are active.
irqstat and kstat (when available) show per-CPU interrupt counts; you can see this routing in action by watching which cores rack up which sources.
What surprises newcomers
- Most interrupts are spurious or shared. A driver must check "is this really for me?" before doing real work, even after AIC says it fired.
- The kernel can lose interrupts if the same source fires twice while the first is being handled with the source masked. Drivers must check device-side status after unmasking.
- The workloop is a thread, not a goroutine. It has a stack, a name (
com.apple.AppleBCM43xx-workloopor similar), and shows up inps. Tracking down "what's running on this Mac" includes the workloops. - DriverKit's userspace interrupt handling is a tradeoff. A 5 µs Mach hop vs millisecond panic-on-crash recovery — Apple's bet is the latter is worth more for almost every device class.
What to read next
apple-oss-distributions/xnuiokit/Kernel/IOWorkLoop.cppThe workloop implementation — single-threaded event dispatch for IOKit drivers.View on GitHub(line —) apple-oss-distributions/xnuiokit/Kernel/IOService.cppIOService — every driver's base class; ties power management + interrupts + matching together.View on GitHub(line —)
And the IOKit overview — interrupts are how driver code actually runs.