Mac OS X Internals: A Systems Approach

10.2. The I/O Kit

The I/O Kit is a collection of several pieces of kernel-level and user-level software that together constitute a simplified driver development mechanism for many types of devices. It provides a layered runtime architecture in which various pieces of software and hardware have dynamic relationships. Besides being a foundation for device drivers, the I/O Kit also coordinates the use of device drivers. Features of the I/O Kit include the following.

  • It presents an abstract view of the system's hardware to higher layers of Mac OS X. In this sense, one of the I/O Kit's jobs is to act as a hardware abstraction layer (HAL). In particular, it provides an approximation of the hardware hierarchy by representing it in software: Each type of device or service is abstracted by an I/O Kit C++ class, and each real-life instance of that device or service is represented by an instance of the corresponding C++ class.

  • It incorporates an in-memory database called the I/O Registry for tracking live (instantiated) objects and another database called the I/O Catalog for tracking all I/O Kit classes available on a system, including uninstantiated ones.

  • It facilitates code reuse and promotes stability by encapsulating common functionality and behavior shared among various driver types (or driver families) and specific drivers. In particular, the I/O Kit exports a unified object-oriented programming interface. Certain types of devices can be driven by user-space drivers. Examples of such devices include cameras, printers, and scanners. Specifically, the connection protocols of these devicessuch as USB and FireWireare handled by kernel-resident I/O Kit families, but device-specific higher-level aspects are handled in user space.

  • In general, the I/O Kit provides a variety of services for accessing and manipulating devices from user space. These services are available to user programs through the I/O Kit framework (IOKit.framework).

  • Besides helping to avoid duplicating common functionality across drivers, the I/O Kit shields the programmerto some extentfrom having to know details of kernel internals. For example, the I/O Kit abstracts Mach-level details of virtual memory and threadingit provides simpler wrappers as part of its programming interface.

  • It supports automatic configuration, or Plug-and-Play. Device drivers can be automatically loaded and unloaded as appropriate.

  • It provides interfaces for driver stacking, wherein new services can be instantiated based on existing services.

Code reuse is not always possible as the I/O Kit may have limited or no support for some types of devices. Hardware quirks and subtleties may mean that apparently similar cases must be handled separately.

Figure 101 shows an overview of the important components and features of the I/O Kit.

Figure 101. An overview of the I/O Kit

Note that whereas a user-space program using the I/O Kit links against IOKit.framework, a kernel-space program, such as a device driver, uses the Kernel framework (Kernel.framework) during its build stage. Kernel.framework does not contain any libraries; it provides only kernel header files. In other words, a driver does not link against Kernel.frameworkit links against the kernel itself.

10.2.1. Embedded C++

Unlike its predecessor, the Driver Kit, which used Objective-C, the I/O Kit uses a restricted subset of C++ as its programming languageit is implemented in and is programmed by using embedded C++ (EC++).[2] The EC++ specification includes a minimum language specification, which is a proper subset of C++, a library specification, and a style guide. The library is more than a typical embedded C library but less than a full-fledged C++ library. Important C++ features omitted from EC++ are the following:

[2] The EC++ Technical Committee was formed in Japan in late 1995, with the goal of providing an open standard for the language and encouraging commercial products that support the standard.

  • Exceptions

  • Templates

  • Multiple inheritance and virtual base classes

  • Namespaces

  • Runtime type identification (RTTI)

Note that the I/O Kit does implement its own minimal runtime typing system.

10.2.2. I/O Kit Class Hierarchy

The various parts of the I/O Kit are implemented using building blocks from the kernel-resident libkern C++ library. Figure 102 shows the high-level class hierarchy of the I/O Kit.

Figure 102. The I/O Kit class hierarchy

The General OS Classes category includes OSObject, which is the kernel's root base class. Besides device drivers, these OS classes are available to all kernel code.

The General I/O Kit Classes category includes IORegistryEntry and its subclass, IOService. The former is the root class of the I/O Kit hierarchy. It allows I/O Kit objects to appear in the I/O Registry and manages their "personalities." In particular, an I/O Kit driver's attach() and detach() methods are used for connecting to the I/O Registry.

The Family Superclasses category includes I/O Kit families for several device types. IOService is the direct or indirect superclass of most I/O Kit Family Superclassestypically, at least one important class in each family inherits from IOService. In turn, most drivers are instances of a subclass of a class in an I/O Kit family. A driver's lifecycle within the I/O Kit's dynamic runtime environment is captured by IOServicespecifically, by its virtual functions. Examples of interfaces defined by IOService include functions for the following purposes:

  • Initializing and terminating driver objects

  • Attaching and detaching driver objects to the I/O Registry

  • Probing hardware to match drivers to devices

  • Instantiating drivers based on the existence of their providers

  • Managing power

  • Mapping and accessing device memory

  • Notifying interested parties of changes in the states of services

  • Registering, unregistering, enabling, and triggering device interrupts

The I/O Kit's main architectural abstractions are families, drivers, and nubs.

10.2.3. I/O Kit Families

An I/O Kit family is a set of classes and associated code that implement abstractions common to devices of a particular category. From a packaging standpoint, a family may include kernel extensions, libraries, header files, documentation, example code, test modules, test harnesses, and so on. Typically, the kernel components of a family can be dynamically loaded into the kernel as needed. The purpose of a family is to allow the driver programmer to focus on device-specific issues, rather than reimplementing frequently used abstractions, which the family implements and provides as a library. In other words, given the specific needs of a particular device, its driver can be constructed by extending the appropriate family.

In some cases, the services a driver requires may be directly provided by the IOService classthat is, the driver may not have a specific family.

Families exist for storage devices, human-interface devices, network devices and services, bus protocols, and others. Examples of Apple-provided I/O Kit families include the following:

  • Apple Desktop Bus (ADB)

  • ATA and ATAPI

  • Audio

  • FireWire

  • Graphics

  • Human Interface Device (HID)

  • Network

  • PC Card

  • PCI and AGP

  • Serial Bus Protocol 2 (SBP-2)

  • SCSI Parallel and SCSI Architecture Model

  • Serial

  • Storage

  • USB

Device/service types for which no families exist include tape drives, telephony services, and digital imaging devices.

10.2.4. I/O Kit Drivers

A driver is an I/O Kit object that manages a specific piece of hardware. It is usually an abstraction around a particular device or a bus. I/O Kit drivers have dependencies on one or more families and perhaps on other types of kernel extensions. These dependencies are enumerated by the driver in an XML-formatted property list file (Info.plist), which is part of the driver's packaging as a Mac OS X bundle. A driver is dynamically loaded into the kernel, and so are the driver's non-kernel-resident dependencies,[3] which must be loaded before the driver.

[3] A driver can also depend on built-in kernel components.

The default locations for drivers are the Library/Extensions/ directories in Mac OS X file system domains. Apple-provided drivers reside in /System/Library/Extensions/.

When a driver belongs to a family, the driver's class typically inherits from some class in the family. This way, all drivers that inherit from a given family acquire the family's instance variables and common behaviors. A family may need to call methods in a driver that inherits from it, in which case the driver implements the methods.

When the system starts to boot, a logical chain of devices and services involved in I/O connections is initialized, starting with the main logic board (hardware) and the corresponding driver (software). This chain grows incrementally, as busses are scanned, devices attached to them are discovered, matching drivers are found, and stacks of providers and clients are constructed. In such a layered stack, each layer is a client of the layer below it and a provider of services to the layer above it. From an implementation standpoint, a typical driver conceptually sits between two families in a stack of C++ objects that represent family instances. The driver inherits from a class in the top family and uses the services provided by the bottom family.

10.2.5. Nubs

A nub is an I/O Kit object representing a controllable entityspecifically, a device or a logical service. It is a logical connection point and communication channel that bridges two drivers and, in turn, the drivers' families. Besides providing access to the entity it represents, a nub provides functionality such as arbitration, matching of drivers to devices, and power management. In contrast to a nub, an actual driver manages specific hardware, with which it communicates through the nub.

Examples of entities represented by nubs include disks, disk partitions, emulated SCSI peripheral devices, keyboards, and graphics adapters.

A driver may publish a nub for each individual device or service it controls or may even act as its own nubthat is, a nub can also be a driver.

A nub's most important function is driver matching: On discovering a new device, the nub attempts to find one or more drivers that match that specific hardware device. We will discuss driver matching in Section 10.2.11.

Although we differentiate between nubs and drivers, they are both classified as driver objects, with the IOService class being the eventual superclass of all driver classes. Moreover, a family usually makes available a class that describes a nub and another class that member drivers use in their implementations. A nub is always registered in the I/O Registrythe registration initiates driver matching. In contrast, it is possible for a driver to be attached but not registered in the I/O Registry. An attached-but-unregistered object is not directly found through I/O Kit lookup functions but must be indirectly looked up by first finding a registered parent or child, after which a parent/child traversal function is used to reach the unregistered object.

10.2.6. General I/O Kit Classes

As shown in Figure 102, General I/O Kit Classes include IORegistryEntry, IOService, and a variety of helper classes. IORegistryEntry is the base class for all I/O Registry objects, whereas IOService is the base class for most I/O Kit families and drivers. Other fundamental I/O Kit classes include IORegistryIterator and IOCatalogue. The former implements an iterator object for traversing (recursively, if desired) the I/O Registry. IOCatalogue implements the in-kernel database containing all I/O Kit driver personalities.

The helper class category primarily includes two types of classes: those that provide memory-related operations, including management of memory involved in I/O transfers, and those that are useful for synchronization and serialization of access.

10.2.6.1. Classes for Memory-Related Operations

The following classes provide memory-related operations.

  • IOMemoryDescriptor is an abstract base class used for representing a buffer or range of memory, where the memory could be physical or virtual.

  • IOBufferMemoryDescriptor is a type of memory descriptor that also allocates its memory when it is created.

  • IOMultiMemoryDescriptor is a type of memory descriptor that encapsulates an ordered list of multiple IOMemoryDescriptor instances, which together represent a single contiguous memory buffer.

  • IODeviceMemory is a subclass of IOMemoryDescriptor that describes a single range of device physical memory.

  • IOMemoryMap is an abstract base class that provides methods for memory-mapping a range of memory described by an IOMemoryDescriptor.

  • IOMemoryCursor implements the mechanism for generating a scatter/gather list of physical segments from a memory descriptor. The generation is based on the nature of the target hardware. During the initialization of an instance of IOMemoryCursor, a pointer to a segment function is provided by the caller. Each invocation of the segment function outputs a single physical segment.

  • IOBigMemoryCursor is a subclass of IOMemoryCursor that generates physical segments in the big-endian byte order.

  • IOLittleMemoryCursor is a subclass of IOMemoryCursor that generates physical segments in the little-endian byte order.

  • IONaturalMemoryCursor is a subclass of IOMemoryCursor that generates physical segments in the processor's natural byte order.

  • IODBDMAMemoryCursor is a subclass of IOMemoryCursor that generates a vector of descriptor-based DMA (DBDMA) descriptors.

  • IORangeAllocator implements a range-based memory allocator. A new instance of the class is created with either an empty free list or a free list that contains a single initial fragment.

10.2.6.2. Classes for Synchronization and Serialization of Access

The following classes assist with synchronization and serialization of access.

  • IOWorkLoop is a thread of control that helps drivers protect resources from concurrent or reentrant access. For example, a work loop can be used to serialize invocations of functions that access critical resources. A single work loop can have multiple registered event sources, each of which has an associated action.

  • IOEventSource is an abstract superclass representing a work-loop event source.

  • IOTimerEventSource is a work-loop event source that implements a simple timer.

  • IOInterruptEventSource is a work-loop event source for delivering interrupts to a driver in a single-threaded manner. In contrast to conventional primary interrupts, IOInterruptEventSource delivers secondary or deferred interrupts.

  • IOFilterInterruptEventSource is a version of IOInterruptEventSource that first calls the driverin primary interrupt contextto determine whether the interrupt should be scheduled on the driver's work loop.

  • IOCommandGate inherits from IOEventSource and provides a lightweight mechanism for executing an action in a single-threaded manner (with respect to all other work-loop event sources).

  • IOCommand is an abstract base class that represents an I/O command passed from a device driver to a controller. Controller command classes such as IOATACommand, IOFWCommand, and IOUSBCommand inherit from IOCommand.

  • IOCommandPool implements a pool of commands that inherit from IOCommand. It supports extracting commands from the pool and returning commands to the pool in a serialized manner.

10.2.6.3. Miscellaneous Classes

The I/O Kit also contains the following miscellaneous classes.

  • IONotifier is an abstract base class used for implementing IOService notification requests. It provides methods for enabling, disabling, and removing notification requests.

  • IOPMpriv encapsulates private power management instance variables for IOService objects.

  • IOPMprot encapsulates protected power management instance variables for IOService objects.

  • IOKernelDebugger acts as a kernel debugger nub, interfacing with the Kernel Debugging Protocol (KDP) module and dispatching KDP requests to the debugger device, which is typically a subclass of IOEthernetController.

  • IOUserClient is used to implement a mechanism for communicating between in-kernel I/O Kit objects and user-space programs.

  • IODataQueue implements a queue that can be used for passing arbitrary, variable-size data from the kernel to a user task. The queue instance can also notify the user task of data availability.

10.2.7. The Work Loop

The I/O Kit's work-loop abstraction, which is implemented by the IOWorkLoop class, provides a synchronization and serialization mechanism to drivers. An IOWorkLoop instance is essentially a thread of control. A key feature of the class is that one or more event sources can be added to it. Each event represents work to be done by the loop, hence the name. Examples of events are command requests, interrupts, and timers. In general, a source can represent any event that should awaken a driver's work loop to perform some work. Each event source has an associated action, the execution of which manifests the concept of work. IOWorkLoop incorporates internal locking to ensure that only one unit of work is being processed at a time in a given instance of the classall event sources acquire the work loop's mutex (or close the work-loop gate) before executing the associated callbacks. Therefore, it is guaranteed that only one action can execute at a time in a work loop. In this sense, the IOWorkLoop class provides the semantics of a master lock for a given driver stack. In the case of interrupts, the work loop's thread acts as the context thread for secondary interrupts (a secondary interrupt is a deferred version of a primary interrupt).

A driver does not usually need to create its own IOWorkLoop instance. It can use its provider's work loop, which can be retrieved using the getWorkLoop() method of the IOService object representing the provider. If the driver does have a current work loop, getWorkLoop() will return that; otherwise, it will walk up the provider chain, calling itself recursively until it finds a valid work loop.

As Figure 103 shows, an IOWorkLoop's main functiontHReadMain()consists of three distinct loops: an outermost semaphore clear-and-wait loop, a middle loop that terminates when there is no more work, and an inner loop that traverses the chain of events looking for work. An event source indicates that there is more work to be done through the checkForWork() method implemented by a subclass of IOEventSource. checkForWork() is supposed to check the internal state of the subclass and also call out to the action. If there is more work, the middle loop repeats. Note that the openGate() and closeGate() methods are simple wrappers around IORecursiveLockUnlock() and IORecursiveLockLock(), respectively, with the recursive mutex lock being a protected member of the class.

Figure 103. The main function of the IOWorkLoop class

// iokit/Kernel/IOWorkLoop.cpp void IOWorkLoop::threadMain() { ... // OUTER LOOP for (;;) { ... closeGate(); if (ISSETP(&fFlags, kLoopTerminate)) goto exitThread; // MIDDLE LOOP do { workToDo = more = false; // INNER LOOP // look at all registered event sources for (IOEventSource *event = eventChain; event; event = event->getNext()) { ... // check if there is any work to do for this source // a subclass of IOEventSource may or may not do work here more |= event->checkForWork(); ... } } while (more); ... openGate(); ... if (workToDo) continue; else break; } exitThread: workThread = 0; free(); IOExitThread(); }

Let us look at an example of using IOWorkLoop in a hypothetical driverlet us call it SomeDummyDriverthat uses IOWorkLoop with two event sources: an interrupt and a command gate. In its start() method, the driver first creates and initializes its own work loop by calling the IOWorkLoop class' workLoop() method. In most cases, a driver higher up in a driver stack could use its provider's work loop.

The driver creates an IOInterruptEventSource object. In this example, the provider's IOService represents the source of interrupts, as specified by the last argument to interruptEventSource(). If this argument is NULL, the event source assumes that its interruptOccurred() method will be called by the client somehow. Next, the driver adds the interrupt event source to be monitored by the work loop. It then calls the work loop's enableAllInterrupts() method, which calls the enable() method in all interrupt event sources.

The driver also creates an IOCommandGate object, which inherits from IOEventSource, for single-threaded execution of commands, with commandGateHandler()a static functionbeing the designated action for the command gate. commandGateHandler() ensures that the object type passed to it is an instance of SomeDummyDriver and dispatches commands based on its first argument. Actions that are performed through the runCommand() or runAction() methods of IOCommandGate are guaranteed to be executed in a single-threaded manner.

Figure 104 shows the relevant portions of code from SomeDummyDriver.

Figure 104. Using IOWorkLoop in a driver

// SomeDummyDriver.h class SomeDummyDriver : public SomeSuperClass { OSDeclareDefaultStructors(SomeDummyDriver) private: ... IOWorkLoop *workLoop; IOCommandGate *commandGate; IOInterruptEventSource *intSource; ... static void handleInterrupt(OSObject *owner, IOInterruptEventSource *src, int count); static IOReturn commandGateHandler(OSObject *owner, void *arg0, void *arg1, void *arg2, void *arg3); ... typedef enum { someCommand = 1, someOtherCommand = 2, ... }; protected: ... public: ... virtual void free(void); virtual bool start(IOService *provider); virtual bool free(void); ... IOreturn somePublicMethod_Gated(/* argument list */); }; bool SomeDummyDriver::start(IOService *provider) { if (!super::start(provider)) return false; workLoop = IOWorkLoop::workLoop(); // Could also use provider->getWorkLoop() ... intSource = IOInterruptEventSource::interruptEventSource( this, // Handler to call when an interrupt occurs (IOInterruptEventAction)&handleInterrupt, // The IOService that represents the interrupt source provider); ... workLoop->addEventSource(intSource); ... workLoop->enableAllInterrupts(); ... commandGate = IOCommandGate::commandGate( this, // Owning client of the new command gate commandGateHandler); // Action ... workLoop->addEventSource(commandGate); } void SomeDummyDriver::free(void) { ... if (workLoop) { if (intSource) { workLoop->removeEventSource(intSource); intSource->release(); intSource = 0; } if (commandGate) { workLoop->removeEventSource(commandGate); commandGate->release(); commandGate = 0; } workLoop->release(); // Since we created it } ... super::free(); } /* static */ void SomeDummyDriver::handleInterrupt(OSObject *owner, IOInterruptEventSource *src, int count) { // Process the "secondary" interrupt } /* static */ IOReturn SomeDummyDriver::commandGateHandler(OSObject *owner, void *arg0, void *arg1, void *arg2, void *arg3) { IOReturn ret; SomeDummyDriver *xThis = OSDynamicCast(SomeDummyDriver, owner); if (xThis == NULL) return kIOReturnError; else { // Use arg0 through arg3 to process the command. For example, arg0 // could be a command identifier, and the rest could be arguments // to that command. switch ((int)arg0) { case someCommand: ret = xThis->somePublicMethod_Gated(/* argument list */); ... break; case someOtherCommand: ... break; ... } return ret; } IOReturn SomeDummyDriver::somePublicMethod_Gated(/* argument list */) { // Calls the current action in a single-threaded manner return commandGate->runCommand(/* argument list */); }

10.2.8. The I/O Registry

The I/O Registry can be seen as an information hub between the kernel and user spaces. It is a kernel-resident, in-memory database that is both constructed and maintained dynamically. Its contents include the set of live I/O Kit objectssuch as families, nubs, and driversin a running system. On discovering new hardware, whether at boot time or at some point in a running system, the I/O Kit attempts to find a matching driver for the hardware and load it. If the driver is loaded successfully, the I/O Registry is updated to reflect the newly added or updated provider-client relationships between driver objects. The I/O Registry also tracks various other types of information, such as that related to power management and the state of a network controller. Consequently, the I/O Registry changes in various scenariosfor example, when a system wakes up from sleep.

The I/O Registry is structured as an inverted tree, each of whose nodes is an object ultimately derived from the IORegistryEntry class. The tree's root node corresponds to the system's main logic board. A stack of I/O Kit objects can be visualized as a branch in the tree. A typical node in the tree represents a driver object, with each node having one or more properties, which can be of various types and in turn are represented by various data types such as numbers, strings, lists, and dictionaries. A node's properties may have multiple sources, with a typical source being the driver's personality, which could be seen as a set of key-value pairs describing the driver. Properties may also represent configurable information, statistics, or arbitrary driver state.

There can be nodes in the I/O Registry that are contrary to the definition of a tree. For example, in the case of a RAID disk controller, several disks appear as one logical volume, with the consequence that some nodes have multiple parents.

The two-dimensional tree structure of the I/O Kit is projected onto multiple conceptual I/O Kit planes, such as the ones listed here.

  • The Service plane (IOService), the most general plane, captures the relationships of all I/O Kit objects to their ancestors.

  • The Device Tree plane (IODeviceTree) captures the hierarchy of the Open Firmware device tree.

  • The Power plane (IOPower) captures the dependencies between I/O Kit objects with respect to power. It is possible to determine, by traversing the connections in this plane, how power flows from one node to another (say, from a provider to a client). In particular, the effects of turning a given device's power on or off can also be visualized.

  • The FireWire plane (IOFireWire) captures the hierarchy of FireWire devices.

  • The USB plane (IOUSB) captures the hierarchy of USB devices.

The sets of branches and nodes in different I/O Kit planes are not identical because each plane is a representation of different provider-client relationships between I/O Kit objects. Even though all I/O Registry objects exist on all planes, only connections that exist in a particular plane's definition are expressed in that plane.

The I/O Registry can be examined through the command-line program ioreg or by using graphical tools such as IORegistryExplorer.app (part of Apple Developer Tools) and Mr. Registry.app (part of the FireWire SDK).

10.2.9. The I/O Catalog

Whereas the I/O Registry maintains the collection of objects active in the running system, the I/O Catalog maintains the collection of available driversit is an in-kernel dynamic database containing all I/O Kit driver personalities. The IOService class uses this resource when matching devices to their associated drivers. In particular, on discovering a device, a nub consults the I/O Catalog to retrieve the list of all drivers belonging to the device's family. The IOCatalogue class provides methods for initializing the catalog, adding drivers, removing drivers, finding drivers based on caller-provided information, and so on.

During bootstrapping, the I/O Catalog is initialized from a list of built-in catalog entries. The list is represented by the gIOKernelConfigTables string [iokit/KernelConfigTables.cpp], which holds the built-in drivers' serialized information. Table 101 shows the members of the list. Much of the I/O Catalog's functionality is implemented in libsa/catalogue.cpp.

Table 101. Initial Entries in the I/O Catalog

IOClass

IOProviderClass

IONameMatch

IOPanicPlatform

IOPlatformExpertDevice

AppleCPU

IOPlatformDevice

cpu

AppleNMI

AppleMacIODevice

programmer-switch

AppleNVRAM

AppleMacIODevice

nvram

IOPanicPlatform represents a catch-all Platform Expert that matches if no legitimate IOPlatformDevice matches. The start routine of this class causes a kernel panic with a message indicating that no driver for the unknown platform could be found.

10.2.10. I/O Kit Initialization

We discussed I/O Kit initialization in Section 5.6. As shown in Figure 514, the bulk of I/O Kit initialization is performed by StartIOKit() [iokit/Kernel/IOStartIOKit.cpp]. OSlibkernInit() [libkern/c++/OSRuntime.cpp] initializes a kmod_info structure (Figure 105). The kernel variable that corresponds to this instance of the kmod_info structure is also called kmod_info. This instance is used to represent the kernel as a fictitious library kernel-module whose name is __kernel__. The module's starting address is set to the kernel's Mach-O header. As with normal kernel extensions, OSRuntimeInitializeCPP() [libkern/c++/OSRuntime.cpp] is called to initialize the C++ runtime. The OSBoolean class is also initialized by OSlibkernInit().

Figure 105. The kmod_info structure

// osfmk/mach/kmod.h typedef struct kmod_info { struct kmod_info *next; int info_version; int id; char name[KMOD_MAX_NAME]; char version[KMOD_MAX_NAME]; int reference_count; // number of references to this kmod kmod_reference_t *reference_list; // references made by this kmod vm_address_t address; // starting address vm_size_t size; // total size vm_size_t hdr_size; // unwired header size kmod_start_func_t *start; // module start entry point kmod_stop_func_t *stop; // module termination entry point } kmod_info_t;

StartIOKit() also initializes key I/O Kit classes by calling their initialize() methods, such as the following.

  • IORegistryEntry::initialize() sets up the I/O Registry by creating its root node (called Root) and initializing relevant data structures such as locks and an OSDictionary object to hold I/O Kit planes.

  • IOService::initialize() initializes I/O Kit planes (such as the Service and Power planes) and creates various global I/O Kit data structures such as keys, locks, dictionaries, and lists.

  • As we saw earlier, the IOCatalogue class implements an in-kernel database for driver personalities. An IOCatalogue instance is published as a resource used by IOService to match devices to their associated drivers. A typical matching process involves a caller providing a matching dictionary containing key-value pairs on which to base the matching. The number and type of keys determine how specific or generic a result will be and whether there will be a match at all. IOCatalogue::initialize() uses gIOKernelConfigTables, which is a serialized OSArray of OSDictionary data types, to initialize the I/O Catalog with personalities corresponding to a few built-in drivers, such as those shown in Table 101.

  • IOMemoryDescriptor::initialize() allocates a recursive lock used by the IOMemoryDescriptor class, which is an abstract base class that defines common methods for describing both physical and virtual memory. An IOMemoryDescriptor is specified as one or more physical or virtual address ranges corresponding to a memory buffer or memory range. The initialization function also creates an I/O Registry property (IOMaximumMappedIOByteCount) representing the maximum amount of memory that can be wired using the wireVirtual() method.

StartIOKit() finally creates an instance of the IOPlatformExpertDevice class as the system's root nub. As we have seen earlier, the Platform Expert is a motherboard-specific driver object that knows the type of platform the system is running on. The root nub's initialization allocates the I/O Kit device tree, initializes the Device Tree plane, and creates an instance of the IOWorkLoop class. The model property of the root nub specifies a particular type and version of Apple computer, such as the following:

  • MacBookProM,N (the x86-based MacBook Pro line)

  • PowerBookM,N (the PowerBook and iBook lines)

  • PowerMacM,N (the PowerMac line)

  • RackMacM,N (the Xserve line)

M represents the major revision, whereas N represents the minor revision.

The root nub instance is then published for matching. The matching process begins with the IOService class method startMatching(), which invokes the doServiceMatch() method synchronously or asynchronously, as indicated by the caller.

IOPlatformExpertDevice is a provider to a system architecturespecific driver, such as MacRISC4PE (systems based on the G5 processor, the U3 memory controller, and the K2 I/O controller) or MacRISC2PE (systems based on G3 and G4 processors, the UniNorth memory controller, and the KeyLargo I/O controller).

10.2.11. Driver Matching in the I/O Kit

The process of finding a suitable driver for a device attached to the system is called driver matching. This process is performed every time a system boots but can also be performed later if a device is attached to a running system.

Each driver's property list file defines one or more of the driver's personalities, which are sets of properties specified as key-value pairs. These properties are used to determine whether the driver can drive a particular device. At the nub's behest, the I/O Kit finds and loads candidate drivers. Next, it incrementally narrows the search for the most suitable driver. A typical search has the following stages of matching:

  • Class matching, during which drivers are ruled out based on their class being inappropriate with respect to the provider service (the nub)

  • Passive matching, during which drivers are ruled out based on device-specific properties contained in driver personalities, with respect to the properties specific to the provider's family

  • Active matching, during which drivers in the pared-down list of candidates are actively probed by calling each driver's probe() method, passing it a reference to the nub it is being matched against

Before active matching begins, the list of candidate drivers is ordered by the initial probe score of each driver. The probe score signifies confidence in the drivability of the device by the driver. A driver personality can specify an initial score using the IOProbeScore key. For each candidate driver, the I/O Kit instantiates the driver's principal class and calls its init() method. The principal class is the one specified by the IOClass key in the driver's personality. Next, the I/O Kit attaches the new instance to the provider by calling the attach() method. If the driver implements the probe() method, the I/O Kit calls it. In this method, a driver can communicate with the device to verify whether it can drive the device and possibly modify the probe score. Once the probe() method returns (or if there is no probe() implementation), the I/O Kit detaches the driver by calling the detach() method and moves to the next candidate driver.

After the probing phase, the probe scores of candidate drivers that could be successfully probed are considered in decreasing order. The drivers are first grouped into categories based on the IOMatchCategory optional key in driver personalities. Drivers that do not specify this key are all considered to belong to the same category. At most one driver in each category can be started on a given provider. For each category, the driver with the highest probe score is attached (again, through the attach() method) and started (through the start() method). A copy of the driver's personality is placed in the I/O Registry. If the driver starts successfully, the remaining drivers in the category are discarded; otherwise, the failed driver's class instance is freed and the candidate with the next highest probe score is considered.

If a driver has multiple personalities, each personality is treated as a separate driver from the standpoint of the matching process. In other words, a driver containing multiple matching dictionaries can apply to multiple devices.

Категории