Registry planes, property ownership, and traversal
The I/O Registry combines a graph of entries with property dictionaries. Those two kinds of state use different locks, lifetimes, and iteration mechanisms. This chapter follows the concrete IORegistryEntry and IORegistryIterator implementation; the registry introduction gives the shorter component map, object runtime and collections explains retained container members, and service matching and termination adds IOService-specific lifecycle rules. IORegistryEntry.cpp lock definitions.
1. One entry can participate in several plane graphs
IORegistryPlane stores a plane name and symbols for parent links, child links, plane-specific path name, and plane-specific location. makePlane() creates those symbols by appending suffixes to the plane name and installs the plane in the global plane dictionary. An entry's fRegistryTable stores graph links and names under those symbols; fPropertyTable stores ordinary properties. IOREGSPLITTABLES is defined in this implementation, so these are separate dictionaries even though the source retains a non-split macro alternative. plane fields and macros, makePlane, entry initialization.
| State | Owner/synchronization | Meaning |
|---|---|---|
Plane descriptor and gIORegistryPlanes |
Global registry graph lock | Symbols that select a particular graph and its path names |
fRegistryTable |
Global gIORegistryLock, shared for reads and exclusive for writes |
Parent/child arrays and name/location metadata |
fPropertyTable |
Per-entry recursive reserved->fLock |
Property dictionary; setters retain values through the dictionary |
fRegistryEntryID |
Assigned from gIORegistryLastID under graph write lock |
Entry identity independent of a particular path |
fRegistryEntryParentGenerationCount |
Updated in parent-link make/break helpers | Parent-link change counter, not a snapshot of all properties |
fIndexedProperties |
Array publication uses acquire/release atomics; slot accesses are a separate contract | Indexed owners outside the regular property dictionary |
The first five rows follow the plane constructor, lock macros, graph helpers and attachment code; the indexed-array distinction follows its separate implementation. locks, links, attachment/ID, indexed properties.
Mermaid source
flowchart LR
E[One IORegistryEntry] --> R[fRegistryTable]
E --> P[fPropertyTable]
R --> AP[Plane A parent array]
R --> AC[Plane A child array]
R --> BP[Plane B parent array]
R --> N[Plane names and locations]
AP --> X[Other entries retained by array]
AC --> Y[Other entries retained by array]
P --> O[Retained property objects]
O --> C[Nested collections may be immutable]
This is a graph interface: parent and child relationships are arrays, and an entry may have more than one parent. getDepth() follows a single-parent chain directly and recursively takes the maximum parent depth when several parents exist. getParentEntry() selects the first parent, so a generated path is not a canonical encoding of every possible route. The attachment body rejects this == parent/child, but does not perform a general graph-cycle search. Do not infer that every traversal is cycle-safe merely because one recursive iterator maintains a visited set. getDepth, first parent, attachment, recursive iterator.
2. Reciprocal links retain entries and must be detached
makeLink() finds or creates the relation's OSArray, avoids duplicate membership, and inserts the other entry as a retained array member. attachToParent() holds the graph write lock while installing the local parent link and, if necessary, the parent's child link. After unlocking, it marks collection-valued properties immutable, then calls the reciprocal virtual attachToChild() if the parent side needed installation. The reciprocal method finds the already-installed links and avoids endlessly calling back. This permits subclass hooks while keeping the actual adjacency changes under the global lock. makeLink, attachToParent, attachToChild.
The core pair of link installations is small: attachToParent.
ret = makeLink( parent, kParentSetIndex, plane );
if ((links = parent->getChildSetReference( plane ))) {
needParent = (false == arrayMember( links, this ));
} else {
needParent = true;
}
if (needParent) {
ret &= parent->makeLink(this, kChildSetIndex, plane);
}
UNLOCK;
The Boolean result accumulates success with ret &= ...; it does not roll back the first link if the reciprocal allocation/insertion fails. The virtual callback after unlock can also contribute a false result. Callers must read a false result as an unsuccessful operation, not proof that no graph state changed. This chapter does not claim that those allocation failures were reproduced. attachToParent, attachToChild.
Detach takes an explicit temporary reference to the other endpoint, removes both adjacency references while holding the write lock, unlocks, performs the reciprocal virtual detach when needed, then releases the temporary owner. The temporary reference matters because removing an array entry drops an object reference. detachAbove() similarly retains this across repeatedly removing its parents. detachAll() first collects descendants, detaches the starting entry, then removes ancestors from those collected entries in reverse order. detach pair, detachAbove/All.
Because parent and child arrays retain one another's entries, ordinary reference counting cannot reclaim an attached component simply because its creator drops a reference. Explicit graph teardown is part of lifecycle. IORegistryEntry::free() checks for remaining IOService-plane relations and panics if still attached, then releases dictionaries, indexed objects and its property lock. The diagnostic checks the service plane; it should not be described as an exhaustive validation of every plane. free, service termination ordering.
The global generation count increments every time the WLOCK macro is used, including operations that later discover no useful change. Parent generation counters likewise increment in the relation helpers for parent operations. These are mutation indicators, not transactional sequence numbers or evidence that a specific object was replaced. lock macros, parent counters.
3. Property locks protect lookup; copy APIs add lifetime
getProperty(symbol) takes the per-entry property lock, obtains the dictionary's borrowed pointer, and unlocks before returning. It does not give the caller a reference. A caller that will use a value while another thread may replace/remove the property needs copyProperty() or another explicit lifetime protocol. The copy macro takes the recursive lock, performs the lookup, retains the object before unlocking, and returns that owner. Its nested call to getProperty() is supported by the recursive lock. get/remove, copy wrapper.
The borrowed path is deliberately brief: getProperty.
OSObject *
IORegistryEntry::getProperty( const OSSymbol * aKey) const
{
OSObject * obj;
PLOCK;
obj = getPropertyTable()->getObject( aKey );
PUNLOCK;
return obj;
}
setProperty() tests whether the new value is a collection and the entry is in a plane, then under the property lock sets its immutable option and inserts it into the dictionary. On initial attachment, existing collection-valued properties receive the same option. This protects the structure of published collections according to haveUpdated()'s checks; it is not hardware read-only memory, and it does not make every non-collection value immutable. The dictionary itself remains replaceable through the entry's property APIs. setProperty, attachment option propagation, collection mutation check.
runPropertyAction() holds that recursive property lock across a caller callback, making a multi-step property transaction possible for cooperating users of the same entry lock. It does not take the graph lock or the IOService arbitration lock. Conversely, setProperties() is a virtual high-level request and the base implementation returns kIOReturnUnsupported; it is not an alias for directly replacing all dictionary members. property action, base setProperties.
Snapshot APIs have different depth. dictionaryWithProperties() shallow-copies the property dictionary while holding the property lock; its values remain retained shared objects. serializeProperties() calls copyCollection() under the lock, then unlocks before invoking serialize() on the resulting container graph and releases that snapshot afterward. Nested collections are copied according to their implementations, but non-collection leaves are shared. The resulting serialization is not a global snapshot of all entries or an automatic byte-copy of mutable OSData leaves. dictionaryWithProperties, serializeProperties, collection-copy depth.
The snapshot-before-serialization order avoids holding the entry property lock across the entire serializer callback graph: serializeProperties.
bool
IORegistryEntry::serializeProperties( OSSerialize * s ) const
{
// setProperty( getRetainCount(), 32, "__retain" );
PLOCK;
OSCollection *snapshotProperties = getPropertyTable()->copyCollection();
PUNLOCK;
if (!snapshotProperties) {
return false;
}
bool ok = snapshotProperties->serialize( s );
snapshotProperties->release();
return ok;
}
Indexed properties deserve a separate warning about contract, not a blanket safety claim. setIndexedProperty() atomically publishes a lazily allocated array, retains the new object, assigns a slot, and returns the previous pointer without releasing it. The caller receives that previous ownership to handle. Slot reads/writes themselves are plain accesses in this body; array-publication atomics do not establish a general synchronized replacement API. Final free releases objects still present in the array. indexed setters/getters, final release.
4. Paths identify a route, names and locations are plane-sensitive
getName(plane) first looks for the plane-specific name symbol, falls back to the common name, then to the metaclass name. getLocation() has the analogous plane-specific lookup. Borrowed name strings are backed by symbols; copyName() instead retains the selected symbol under the graph lock or creates a symbol for the fallback class name. names, locations.
getPath() writes a plane prefix followed by a route from the plane's root. It may use aliases; otherwise it walks parents into a retained stack and emits name@location components in reverse. Buffer checks reserve space and update the reported length on the paths shown in the body. getPathComponent() includes @location only when a location exists. Because parent selection uses the first parent, a graph with several incoming edges can have other valid routes that this call does not emit. getPath, component.
fromPath() parses a plane prefix if no plane is supplied, resolves an alias if present, and then walks child components under the graph read lock. Components match names and optional locations. Location comparison parses hexadecimal numeric fields and separators, so it is not simply a byte-for-byte string comparison. Alias storage is read from the /aliases entry as data containing paths; that is a convention consumed by these helpers, not a general filesystem symlink implementation. fromPath, child matching, location comparison, aliases.
The residual-path arguments alter success semantics: with opath and length, the function can return the entry reached so far and report the remaining text; without them, trailing unconsumed path causes failure. A successful result is retained while still under the graph read lock, so the caller owns a reference. childFromPath() forwards to the same retained lookup with this as its starting entry; it does not drop that returned reference. residual and retain, child wrapper.
5. Snapshot adjacency iterators and live recursive iterators differ
getParentIterator() and getChildIterator() copy the relevant link array under the graph read lock and return an iterator over that retained snapshot. applyToParents/Children() likewise copies the adjacency array, unlocks, and invokes callbacks afterward. The snapshot keeps those objects alive, but it does not guarantee they remain attached when the callback runs. Code that requires current service activity must recheck the corresponding service state. parent iterator, child iterator, callback snapshots.
IORegistryIterator instead maintains a cursor stack through live link arrays. Creation retains the root. getNextObjectFlat() takes the graph read lock, creates a collection iterator for the selected parent's/child's links on first entry, releases the previous current entry when advancing, obtains the next borrowed array member, and retains it as the new current entry. If the nested collection iterator became invalid, it sets a sticky invalid flag. Returned entries are owned by the iterator's current cursor, not transferred to the caller. creation, flat iteration.
Recursive iteration keeps an ordered done set. It enters a returned node only when inserting it into done succeeds; encountering the same entry again can still return that entry, but does not recursively descend into it again. Thus the visited set bounds descent through repeated edges; it does not mean each getNextObject() result is unique. iterateAll() exhausts recursive traversal and returns a retained done set, which does contain unique members. recursive traversal, iterateAll.
The precise branch is worth reading: getNextObjectRecursive.
IORegistryEntry *
IORegistryIterator::getNextObjectRecursive( void )
{
IORegistryEntry * next;
do{
next = getNextObjectFlat();
} while ((NULL == next) && exitEntry());
if (next) {
if (NULL == done) {
done = OSOrderedSet::withCapacity( 10 );
}
if (done->setObject((OSObject *) next)) {
// done set didn't contain this one, so recurse
enterEntry();
}
}
return next;
}
There is also an implementation limit in isValid() worth preserving when reasoning about concurrent graph changes. It initializes next = where and advances next through cursor links, but the nested check uses where->iter, not next->iter. The visible body therefore must not be summarized as validating every ancestor iterator. Each flat advancement still tests its current collection iterator, and the sticky invalid flag is checked; this documentation reports the body without asserting a reproduced failure or correcting kernel source. IORegistryIterator::isValid.
next = where;
RLOCK;
ok = (0 == (kIORegistryIteratorInvalidFlag & options));
while (ok && next) {
if (where->iter) {
ok = where->iter->isValid();
}
next = next->next;
}
UNLOCK;
reset() exits every cursor, releases the visited set, points back at the retained root and clears the sticky flag. free() resets and releases the root. A restart can therefore recover a traversal after observed invalidation, but it starts a new observation of a potentially different graph rather than continuing an atomic snapshot. cursor teardown/reset/free.
Reading route and limits
Read makeLink/breakLink, the attach/detach pair, and the borrowed/copy property wrappers first. Then compare dictionaryWithProperties with serializeProperties, and compare getChildIterator with IORegistryIterator::getNextObjectFlat. Finish with fromPath and the exact validity loop; these explain why ownership, snapshot depth and graph stability must be considered separately.
| Inspected implementation | Coverage | Remaining boundary |
|---|---|---|
| Plane construction, split dictionaries, attach/detach/free | Full selected link and property ownership paths | Every IORegistryEntry subclass hook is not audited |
| Property set/get/copy/action/snapshot/indexed APIs | Locks, retained versus borrowed values, snapshot depth | Per-driver property schemas and user-facing authorization remain driver-specific |
| Names, locations, paths, aliases | Lookup/formatting, residual path, returned reference | All malformed alias graphs and numeric parser corner cases are not tested |
| Both adjacency snapshots and recursive cursor iteration | Reference movement, invalidation, repeated nodes, reset | No concurrent mutation stress test; exact where/next limitation is source-only |
The graph relationships here are distinct from device-resource allocation or service-start success. Continue with matching and termination for those lifecycle rules, and notification/arbitration for callback snapshots and service locks.