Skip to content

IOKit and the driver model: how a Mac talks to its hardware

Embedded C++, an object tree, and matching dictionaries — IOKit is how every driver on macOS gets loaded, paired with hardware, and called.

Published 5 min read
IOKit driver matchingA new IOPCIDevice publishes vendor/device IDs. IOKit scores every registered personality and picks the highest-scoring driver to probe, init, attach, start.new IOPCIDevicevendor=0x14e4 device=0x4331REGISTERED PERSONALITIES (IOProviderClass = IOPCIDevice)IOPCIDeviceGenericprobe() → score = 100AppleBCM43xxprobe() → score = 5000⭢ winsIOWLANDriverprobe() → score = 1500ThirdPartyWiFiKextprobe() → score = 600LIFECYCLEinit()probe()attach()start()Vendor-specific drivers consistently outscore generic ones — that's how third-party support layers cleanly under Apple's class drivers.

Unix drivers are a list of function pointers. IOKit is an object graph. That single design decision — that drivers are C++ objects held in a runtime-traversable tree, matched against hardware by a dictionary, and instantiated lazily — shapes every weird thing about device support on macOS.

This is what IOKit is, why it looks the way it does, and where to read it.

The tree

Boot a Mac, and the kernel builds a tree of IOService instances: the registry. The root is IORegistryEntry; everything else hangs off it through parent/child relationships, with IOService as the base class for anything that can be matched and started.

apple-oss-distributions/xnuiokit/Kernel/IOService.cppIOService — the base class. Every driver inherits, transitively, from this.View on GitHub(line ) apple-oss-distributions/xnuiokit/Kernel/IORegistryEntry.cppThe registry — the kernel's database of every node IOKit knows about.View on GitHub(line )

You can look at this tree yourself. From the command line:

ioreg -l

What you're seeing is the live, in-kernel IORegistry, serialized. PCI controllers, USB hubs, sensors, the keyboard, the display, the Wi-Fi chip — every one is a node. Walk the parents of any node and you'll eventually reach the platform expert and, beyond it, the registry root.

Matching: how a driver finds its hardware

When a new node appears (a USB device is plugged in, the platform expert publishes the PCI bus), IOKit probes every personality it knows about for a match. A personality is a dictionary in a driver's Info.plist: IOClass, IOProviderClass, IONameMatch, IOPropertyMatch and friends.

apple-oss-distributions/xnuiokit/Kernel/IOService.cppIOService::matchPropertyTable — the matching engine.View on GitHub(line )

The match goes:

  1. The provider (e.g., IOPCIDevice) publishes its properties — vendor/device IDs, class codes, BARs.
  2. IOKit iterates registered drivers whose IOProviderClass is IOPCIDevice.
  3. For each candidate, it scores the match using whatever keys the dictionary names (IOPCIClassMatch, IOPCIMatch, ...).
  4. The highest-scoring driver gets probe-d. If probe returns a positive score, IOKit calls init, then attach, then start.

Two things fall out of this:

  • You don't write a driver entry point. You write a class. IOKit instantiates it.
  • Drivers can compete. If a vendor-specific driver outscores a generic one, the vendor's wins. This is how third-party USB device support layers cleanly under Apple's generic class drivers.

The object model: Embedded C++

IOKit is C++ — but not the C++ you write in userspace. Kernel C++ has no RTTI, no exceptions, no STL. Apple maintains its own object model: OSObject for reference-counted base, OSMetaClass for runtime type info, OSDictionary / OSArray / OSNumber / OSString for value types.

apple-oss-distributions/xnulibkern/c++/OSObject.cppOSObject — the kernel's reference-counted base. retain() / release() pattern.View on GitHub(line ) apple-oss-distributions/xnulibkern/c++/OSMetaClass.cppOSMetaClass — the home-grown RTTI that makes runtime introspection of drivers possible.View on GitHub(line )

Every IOKit class declares its metaclass with macros (OSDeclareDefaultStructors, OSDefineMetaClassAndStructors). These wire the class into a registry of class names → constructors, so IOKit can instantiate a driver by name from a plist string.

There's a strict reason for the home-grown system: in 2000, when IOKit was designed, no C++ ABI could be safely linked across binary kexts compiled with different toolchains. The metaclass system papers over that — kexts only see a binary-stable C API plus pointer arithmetic that the macros generate, never the compiler's vtable layout.

kexts: how a driver actually gets into the kernel

A kernel extension is a Mach-O bundle that the kernel can load on demand. Each kext has:

  • A binary with one or more IOService subclasses.
  • An Info.plist with one or more personalities (the matching dictionaries above).
  • A list of link dependencies on other kexts.

kextcache (or, modern macOS, kmutil) merges all installed kext plists into the prelinked kernel at install time, so the boot loader has a single artifact to load. At runtime, when a personality matches incoming hardware, the kernel walks the dependency list, links the kext into the kernel address space, and instantiates the class.

The big direction of travel: third-party kexts are dying. Since macOS 10.15, Apple has pushed driver authors to DriverKit — a userspace driver framework where the IOKit object model is preserved but the driver process runs in userspace, isolated, with its own entitlements. The kernel still talks to it over IOKit-style messages, but the driver's crashes don't take the kernel with them.

apple-oss-distributions/xnuiokit/Kernel/IOUserServer.cppIOUserServer — the kernel side of DriverKit. How userland drivers attach to the IORegistry.View on GitHub(line )

Power management is an IOKit responsibility

Every node in the registry can announce its power needs. When the system idles, IOKit walks the tree and tells each driver to drop to a lower power state if it can; on wake, it walks the tree the other way. This is why a graphics driver and a fan controller use the same API — both are IOPMrootDomain children.

apple-oss-distributions/xnuiokit/Kernel/IOPMrootDomain.cppPower management root — the orchestrator for sleep/wake/idle decisions.View on GitHub(line )

User-kernel boundary: IOUserClient

A userspace process that wants to talk to a driver does it through IOUserClient. The driver subclasses IOUserClient and exposes a small set of method numbers; the userspace side opens the driver via IOServiceOpen and calls those methods through IOConnectCallMethod. Under the hood: Mach messages on a special port.

apple-oss-distributions/xnuiokit/Kernel/IOUserClient.cppIOUserClient — the only way userspace gets to call into a driver.View on GitHub(line )

This is why third-party userland tools that need to read sensor data, switch GPUs, or control fans all end up shipping a tiny kext (or now, a DriverKit extension): there's no /dev/whatever for most hardware. There's just an IOUserClient you have to open.

Why this is the way it is

The case for IOKit, made succinctly:

  • Hot-plug is the default, not a bolt-on. The same code path handles USB, Thunderbolt, and a Bluetooth pairing.
  • Drivers can introspect each other. A power manager doesn't need a kernel API for "list every display" — it walks IODisplayWrangler's children.
  • The object model is debuggable. ioreg, IORegistryExplorer, and lldb's IOKit awareness all read the same registry, live, on a running system.

The case against:

  • C++ in the kernel is hard to get right. ABI breakage in any IOKit base class can ripple through every third-party kext. Apple has burned a release window on that more than once.
  • Mach-message round-trips are slower than function calls. DriverKit makes the trade explicit; you accept more latency for less risk to the kernel.

Pick one piece of hardware you care about — a webcam, the trackpad, the SoC's PMU — and walk the IORegistry to its driver. Then open the driver's source on GitHub:

apple-oss-distributions/xnuiokit/DriversThe in-tree IOKit drivers — small, but instructive.View on GitHub(line )

And read IOService::start once. It's the function every driver writes, and once you've read the base class's version of it, half the IOKit literature in books and WWDC talks suddenly makes sense.

Related

Same IOKit object model, userland process. Why kexts are dying, what DriverKit gives you, and how a USB driver actually crosses the boundary.
clonefile, fclonefileat, fs_snapshot — three syscalls that let you copy 50 GB in 50 milliseconds. Here's what happens under each one, and what doesn't get copied.
What changed in XNU when Apple shipped its own ARM silicon — P/E cores, APRR page-permission switching, the AMX matrix coprocessor, and Rosetta 2.