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.
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.
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:
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:
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.
10.2.6.2. Classes for Synchronization and Serialization of Access
The following classes assist with synchronization and serialization of access.
10.2.6.3. Miscellaneous Classes
The I/O Kit also contains the following miscellaneous classes.
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
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
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 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.
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
StartIOKit() also initializes key I/O Kit classes by calling their initialize() methods, such as the following.
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:
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:
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. |
Категории