Skip to content

Power management on macOS: IOPM, idle states, and wake from sleep

How XNU tells every driver to drop power when idle and bring it back when needed — the IORegistry-walked power graph, IOPMrootDomain, and the sleep/wake choreography.

Published 5 min read

The second-biggest job IOKit does — after matching drivers to hardware — is power management. Every node in the IORegistry can declare a set of power states; the kernel orchestrates the whole tree to drop into low power when idle and bring it back when wake events arrive. This article walks how that orchestration works.

The power graph

Every IOService instance can implement joinPMtree and declare a power state table: an array describing each state the device supports, what the state costs, what capabilities are available in it, and how long the transition between adjacent states takes.

apple-oss-distributions/xnuiokit/Kernel/IOServicePM.cppThe service-side power management — every driver's interface to IOPM.View on GitHub(line ) apple-oss-distributions/xnuiokit/Kernel/IOPMrootDomain.cppThe root domain — orchestrator of system-wide sleep/wake.View on GitHub(line )

Power states are integers from 0 (off) up to some implementation-defined max. The driver promises:

  • "In state N, I can do X." Capabilities at the state.
  • "Going from state N to N+1 takes T microseconds." Transition latency.
  • "In state N, I draw P milliwatts." Power cost.

The kernel uses these to plan when to demote (drop to lower state) and when to promote (raise back up).

Power parent → child relationships

The IORegistry tree doubles as a power hierarchy. A child node's power needs trickle up: if a USB device needs full power, its USB hub must be powered, which means the USB controller chip must be powered, which means the PCIe bus must be powered.

When a driver's IOService joins the PM tree, it declares a power parent. The parent guarantees a minimum power state to its child; the child consumes power against the parent's budget.

The result is a directed graph rooted at IOPMrootDomain — the system's root power node. When a leaf node wants to wake (say, an incoming keystroke on the keyboard), the wake request propagates up the graph: "wake me, which means wake my hub, which means wake the controller, which means wake the bus."

IOPMrootDomain — the system-wide power orchestrator

The IOPMrootDomain is a singleton IOService that owns system-level power decisions:

  • Display sleep / dim.
  • Disk spindown (legacy; modern Macs are SSD).
  • Whole-system sleep.
  • Wake on lid open, on power button, on network packet, on Bluetooth peripheral, on alarm.
apple-oss-distributions/xnuiokit/Kernel/IOPMrootDomain.cppThe root power node — coordinates every sleep/wake transition.View on GitHub(line )

When the user closes the laptop lid:

  1. The lid-sensor driver notifies IOPMrootDomain.
  2. IOPMrootDomain decides: full sleep (default) or stay awake (Power Nap, AC plugged in with certain settings).
  3. If sleeping, root domain walks the PM tree in reverse, telling every device "drop to your lowest state."
  4. Each device acknowledges (or refuses, with a vetoing reason).
  5. Once all devices are at lowest state, root domain tells the SoC to enter the deep sleep state. CPU clocks stop, most of the chip powers down.

Wake reverses this. A wake event (open the lid, keypress on external keyboard, scheduled alarm) wakes the CPU, root domain walks the tree forward, every device transitions back up.

Idle vs sleep — two different mechanisms

Don't confuse:

  • Idle states (C-states on Intel terminology, idle cluster gating on Apple Silicon) — the CPU goes into low-power microsecond-scale idle when there's nothing to run. The OS doesn't notice; it's a hardware-level optimization.
  • Sleep — the OS notices. The display turns off, devices drop power, the user sees "asleep."

The two interact. While idling, the CPU might still service interrupts every few ms (timer, network, USB poll). Real sleep stops those — interrupts are reconfigured to wake the SoC instead of keeping CPU clocks running.

Power vetoes

A driver can refuse to drop power. If the system tries to sleep and a driver returns kIOReturnNotReady to its power-down request, the system stays awake and surfaces a notice (in the kernel log: "AppleHDA prevented sleep").

Common veto reasons:

  • The driver has an in-flight async operation that must complete before sleep.
  • The driver is the only handle on a resource needed for wake (the wake-up source itself).
  • A userspace pmset setting has been configured to never sleep.

pmset -g assertions from the command line lists every active veto, with the process that holds it. Useful when "my Mac won't sleep" debugging.

Sleep wake — the user-visible chain

user closes lid
  ↓
lid sensor driver → IOPMrootDomain
  ↓
root domain → notify all subscribed drivers ("about to sleep")
  ↓
each driver returns "ready" or "veto"
  ↓
if all ready: walk PM tree backward, drop every device to lowest state
  ↓
SoC enters deep sleep
  ↓
[time passes]
  ↓
wake event (lid open, alarm, etc.)
  ↓
SoC wakes, CPU resumes
  ↓
root domain walks PM tree forward, brings devices back up
  ↓
display turns on, user sees their session

Each driver's setPowerState callback is where its specific code runs — for an audio driver, that might mean re-initializing the codec, re-loading firmware, flushing pending audio buffers.

Idle-state management for the SoC itself

On Apple Silicon, the pmgr driver is responsible for cluster-level voltage/frequency scaling and idle-state gating:

  • Each cluster (P and E) has its own voltage rail.
  • When all cores in a cluster idle, the cluster's clock can be stopped, then its voltage dropped, then the entire cluster powered off.
  • Wake from idle is fast (microseconds); wake from full cluster-off is slower (milliseconds).
apple-oss-distributions/xnuiokit/DriversIn-tree Apple Silicon drivers — pmgr, AIC, AOP are here in spirit even if the macOS-specific ones are closed-source.View on GitHub(line )

What surprises newcomers

  • Power management is IOKit's job, not BSD's. POSIX has nothing to say about CPU power states or device wake; the entire mechanism is IOKit.
  • Sleep is a negotiation, not a command. Any driver can veto. This is why apps holding "prevent sleep" assertions actually keep the Mac awake.
  • Wake-from-network is configurable per-interface via pmset. The kernel arms the NIC's wake-on-LAN; certain packets fire the wake event.
  • The display going dark isn't always "system asleep." Display sleep is a separate, lighter-weight state — most of the system stays running, just the screen is off. Full sleep is the deeper state where most of the SoC powers down.

apple-oss-distributions/xnuiokit/Kernel/IOPMpowerState.cppThe power-state object — what each state declares.View on GitHub(line ) apple-oss-distributions/xnuiokit/Kernel/IOPMWorkArbiter.cppThe arbiter — coordinates concurrent power requests across drivers.View on GitHub(line )

And the IOKit overview — the same IORegistry that holds devices is what the power graph walks.

Related

From plug-in to working app — IOUSBHost enumeration, IOKit matching, the DriverKit dext load, the user-space SDK. A complete trace of one device's journey through the stack.
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.
Same IOKit object model, userland process. Why kexts are dying, what DriverKit gives you, and how a USB driver actually crosses the boundary.