Configure player

Close

WWDC Index does not host video files

If you have access to video files, you can configure a URL pattern to be used in a video player.

URL pattern

preview

Use any of these variables in your URL pattern, the pattern is stored in your browsers' local storage.

$id
ID of session: wwdc2008-434
$eventId
ID of event: wwdc2008
$eventContentId
ID of session without event part: 434
$eventShortId
Shortened ID of event: wwdc08
$year
Year of session: 2008
$extension
Extension of original filename: m4v
$filenameAlmostEvery
Filename from "(Almost) Every..." gist: [2008] [Session 434] Getting Sta...

WWDC08 • Session 434

Getting Started with the I/O Kit: Device Drivers on Mac OS X

Essentials • 53:08

The I/O Kit is a set of system frameworks and libraries for creating device drivers on Mac OS X. If you are new to writing Mac OS X device drivers, learn about the I/O Kit's powerful in-kernel and user-space APIs. Whether you're bringing a new device to Mac OS X or developing an application to access an existing device, this session is for you.

Speaker: Dean Reece

Unlisted on Apple Developer site

Downloads from Apple

SD Video (649.5 MB)

Transcript

This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.

Good morning. So welcome to the final day of WDC 2008. My name is Dean Reece. I manage the I/O Kit team. at Apple and this morning I'm going to be talking a little bit about I/O Kit. Just a quick show of hands, who here is brand new to I/O Kit, haven't really looked at I/O Kit drivers before? Wow, so we have a bunch of new folks in the audience, that's excellent, welcome.

Hopefully this will be a good session for you to learn some interesting details about I/O Kit and show of hands, how many people have already done some I/O Kit work before and is looking for a refresher? Alright, so hopefully there'll be something in there for you as well. Let's dive in, we've got a lot to go through, I/O Kit is not a small piece of technology.

So what is I/O Kit? Let's draw a box around it so you can kind of get an idea of what it is and what it isn't. So simply stated, it's the device driver model for Mac OS X. And I/O Kit drivers are written in a subset of C++. We use a standard C++ compiler, but we restrict the language features a bit. I'll talk about that shortly.

And it runs inside the kernel. The Darwin kernel that's used on Mac OS X is a single process, single address space. All the drivers run in that single process, and we'll talk about what that means. And it also, in addition to being the kernel runtime environment for drivers, it's also a framework that applications use to talk to those drivers. So it's really two separate things depending on your perspective. Now, the I/O Kit is not part of the iPhone SDK, so if you're interested in writing iPhone applications, I/O Kit is not available to you.

So to describe it graphically where it fits in the system, the large purple area at the bottom there represents the entire kernel address space, the entire kernel process. And we're going to be focusing in specifically on I/O Kit and the drivers that plug into it. And we'll also talk about libkern as well, because that's the basic kernel library that I/O Kit builds upon. So you can't really talk about I/O Kit without exploring libkern some.

So the next question then is when do you use I/O Kit? When is it appropriate to use this framework? Well, if you're writing a driver that needs to be in the kernel, and what does that mean? Well, the number one thing is if you need to take interrupts directly or you need to get registers directly, then you're probably going to need to live in the kernel. Those things are not available to user-space applications. The third reason up here is a performance reason.

If you're inside the kernel and your primary client is inside the kernel, then you can stack very neatly and communications very quick. If your primary client, say a file system, lives in the kernel, but your driver were to live outside the kernel, that would require multiple user-kernel boundary crossings just to move data from the physical device up to your driver, back down to the file system, and there's obvious performance issues there. And it may be appropriate in some cases, but as a rule of thumb, if your primary client lives in the kernel, your driver should probably live in the kernel as well. Amen.

The in-kernel drivers, all in kernel code for that matter, uses the kernel framework. The kernel framework is unique among the frameworks in that it is the only framework kernel extensions can use, and it is also not available to applications. Kernel framework is only for use by kernel code. It's also a framework that is made up only of headers. It has no binary component. The binary component is the actual live running kernel, and that's the way kernel extensions get loaded. So kernel framework is available for kernel extensions only.

Now, that being said, you should avoid writing an in-kernel driver if you can avoid it. If you're able to accomplish what you need by writing code outside of the kernel, that's preferable. It's easier to debug, and it's also a little bit safer if something were to crash in your code, it wouldn't bring the whole system down.

So the other half of I/O Kit, the user-space half. A lot of drivers actually work correctly outside of the kernel. Cameras, scanners, image capture, printer, plug-ins, and also applications and utilities that want to present a GUI to, say, expose some control aspect of a device. Those things all live outside of the kernel and make use of the I/O Kit framework, not the kernel framework, the I/O Kit framework.

And I/O Kit framework is available to user-space programs, and it gives you access to the device drivers inside the kernel, but it itself doesn't run in the kernel. It's kind of a proxy to get you into the kernel. And again, I reiterate on the last bullet there, avoid writing a kernel extension if you can. It's generally better for you and better for your customers.

So what were the design goals of I/O Kit? What were we thinking when we put all this together? What was in our heads? Well, writing a device driver for a modern operating system is a very complicated process. There's a lot to it. So we tried to simplify it as much as we could without oversimplifying it.

And some of the things are multiprocessor systems, preemptive kernel environment, multiple address spaces and sizes and Indian-nesses, the byte ordering. All that has to be coped with by drivers because drivers talk to hardware, drivers also talk to applications, and drivers run in the kernel. So there's three separate address spaces that drivers have to deal with.

And we also really want drivers to focus on mechanism and not policy. This is kind of a central concept that you might want to really think about when you're putting your software together. If you need to have policy that is some kind of decision being made based on what the machine is doing, based on what a user is doing, you probably want that policy to be put into a piece of software that runs outside the kernel.

The kernel is really just a service library intended to do whatever the user-space apps want. The user-space apps call into the kernel to get things done, the kernel gets them done. That's the mechanism piece. The thing that lives in the kernel is really supposed to get things done at the request of user-space pieces.

At the core, I/O Kit has a set of APIs that are device independent. We really wanted to make sure that we didn't wire in any dependencies about USB or SCSI. You know, we didn't want to have anything built in that would 10 years down the road make us not be able to move forward. So the core of I/O Kit APIs are completely architecture and protocol independent.

Now, obviously, we have to have protocol-specific bits. We have to have PCI, we have to have USB, and we even have SCSI. So we do that by these things called families. They add chunks of functionality into the kernel that device drivers can build on. And, um... Developers can also extend the kernel.

You can write your own drivers. You can also write your own families if you need to. If you have some functionality that you need to share between several drivers and you don't want to cut and paste it into all the drivers, you can write your own family as a common service library.

So what is it like to develop for the kernel? What developing in the kernel is different than developing for user-space? Well, the Darwin kernel is a process, just like any other process on the system. You can run top or PS or other utilities, and it'll show you process zero is the kernel. It's not exactly like other processes in that it has a few things that are different.

A lot of the things that you would normally associate with a BSD process, like file descriptors and things like that, are not there. But it has most of the other trappings of a process. It has an address space, it has threads, and, you know, IPC works between the kernel and other processes.

But one thing that's very different about the kernel is you don't launch it. Booting the system launches the kernel. It's actually the booter that starts it up. And the kernel doesn't exit-- well, hopefully it doesn't exit-- until you shut down cleanly. So to get code into the kernel, you're actually injecting code into a live running piece of software, which is a little different than most applications on the system. So we have a fairly extensive collection of technology we call text management or kernel extension management.

And there's actually a session on that after lunch today. These kernel extensions are packaged as CFBundles. If you've played around on the Mac platform at all, you've certainly seen this notion of a CFBundle. It's basically a directory that contains all the bits and pieces that are necessary for your product. Kernel extensions are CFBundles.

Another important thing to think about when you're putting code in the kernel and another reason not to put code in the kernel, all the code running in the kernel is inherently trusted. Once that first instruction is executed in your code, you're running basically completely unrestricted. You can do anything that any kernel code can do. You can see any memory.

That's why it's very important when you're writing code for the kernel to always have that in mind. Make sure that you vet any parameters you receive from user space. Never trust a pointer. We'll talk more about that later, but you need to keep that in mind when you're writing a kernel extension.

So also all kernel extensions and the kernel itself share a single address space. It's like any other process. All the code running in that process zero shares the same kernel address space. And on PowerPC and the i386 architecture, that's a 32-bit address space. Now, if you've heard, hopefully you have, we have a 64-bit version of the kernel in the Snow Leopard seed. And that architecture is x86-64. That's the actual identifier we use in code. And this is just a third architecture, PPC, i386, x86-64. But that obviously means you have to update code. You have to make it aware that pointers are bigger and so forth.

Now, all of the APIs in the kernel we refer to as KPIs, Kernel Programmatic Interface, and I might call them APIs up here. I apologize if I'm inconsistent, but when I'm talking about an in-kernel interface, it's technically a KPI. The KPIs are in the kernel framework, as I said before, and they're only available there.

So the kernel is kind of like a long-running app. As I said before, hopefully it won't exit. Well, if it does exit, that's called a crash or panic. Now, a lot of apps, particularly small helper utilities, don't really bother to free memory. They do something and then they exit, and that's how they free memory.

Well, you can't count on that in the kernel. You have to make sure that all of your resource management is balanced. If you allocate something, you have to free it. If you acquire a lock, you have to release the lock. Everything has to be balanced. Otherwise, the kernel will not be long running.

All kernel memory, by default, is wired. When you allocate memory in the kernel, it's wired memory. That means that it will not page out. So if an application allocates a megabyte and doesn't touch it or doesn't use it, it's kind of an accounting trick. But if the kernel allocates a megabyte, you've just acquired a megabyte of physical RAM that will not be available to any other processes. So in that way, you can think of kernel memory as more expensive than user memory.

If you need to use the memory, allocate it and use it. But just be mindful that kernel memory is wired. Floating point is not available in the kernel. That's a bit of an exaggeration. It's not available without some extensive tricks. Best not to use floating point. If you're trying to use floating point in the kernel, you need to step back and look at what you're trying to accomplish. And maybe you need to factor your code differently and do the floating point pieces outside the kernel.

Most of your familiar libraries are not available in the kernel either. For example, we have an incomplete port of libc. The kernel environment is intended to be minimal. We don't want to have more in there than we need, kernel memory being wired and all. So you're going to find that your options are a little bit limited when you're developing the kernel, and that's intentional. It's actually a way of keeping the kernel environment under control.

Also, file I/O is not supported from the kernel. The kernel does file I/O on behalf of user processes, but it doesn't initiate any file I/O. So if you've got a KEXT and you say, "Oh, I need to read a file," the KEXT needs to have a user component, a daemon or something, handling the file I/O on its behalf.

Debugging in the kernel is also different. Again, it's a running process that you can't really stop without stopping the whole system. So the way we do this is we use a two-machine debug environment. You have one machine that is running your test software, running your kernel extension. Your other machine has got GDB.

You remotely connect over either Ethernet or FireWire, and you can halt the machine, you can single-step it, you can get symbolic backtraces. There's a tutorial called Hello Debugger. It's available on the ADC site, along with the other simple Kecks tutorials. And it kind of walks you through the process of simply quickly setting up a two-machine environment and debugging a little bit.

I said before that we use a restricted subset of C++ in the kernel, and let's talk briefly about what that means. Back when we first conceived of I/O Kit, we consciously decided to restrict ourselves to the set of features in a standard called embedded C++. I believe that's a defunct standard these days, but we've stayed with the core concepts.

So exceptions are not available, and that was done because the rest of the kernel doesn't support exceptions, and if an exception went uncaught through I/O Kit, it could propagate out of the kernel and cause problems. We don't support multiple inheritance that pose too much of a binary compatibility problem for us.

We don't support non-trivial constructors. That actually goes back to the exceptions. The constructors pretty much just boil down to allocators and initializers at this point. And we also don't support STL, the Standard Template Library, and we don't use the standard C++ RTTI, the Runtime Type Information System. We actually have our own simplified RTTI. It's different, but it meets our needs, and it's mostly there for binary compatibility and some limited introspection.

So templates are partially supported in that they don't cross kernel extension boundaries. You can actually use language features available in the compiler that don't cause any runtime requirements or don't cause any linkage changes. So you can put inline ASMs, you know, you can put things in your code if you can get the compiler to emit a kernel extension that's, you know, that we'll be able to link to, but we don't support that. It's kind of a no-lifeguard-on-duty situation.

So we recommend not using templates. We have heard anecdotally there are developers that have code that they're reporting that use templates and they were able to get it to work. So if you have templates and you need to get them to work, it's possible, but it's not something we do internally. It's not something we test.

So let's talk objects. Specifically, I'm going to give you a brief tour of our class hierarchy, at least some of the highlights of it. The root class within the kernel C++ hierarchy is called OSObject. This is part of that libkern library I mentioned. And it's really the root class for all the kernel classes.

It gives you some basic type introspection. You can find out what class you are. You can find out if you're compatible with a particular class. You can find out if another object is compatible. You can do safe typing, typecasting. It also handles reference counting for you. Some pretty basic object-oriented type stuff.

It also embodies the macros that we use for binary compatibility, these OS declare structures and OS define structures macros. If you've looked at any of our sample code or any of the Darwin source code, you'll see these in there. They're very important. They provide information that our linker uses, the KEXT linker uses for binary compatibility.

I've seen cases where developers didn't put those in or didn't use them correctly, and their KEXT worked until we changed the kernel. As soon as we changed something, the binary compatibility patcher was not able to do the right thing, and the KEXT wouldn't load anymore. So it's very important to make sure you've got those right for any class, any subclass that you declare of OS object.

So very briefly, we have some standard container objects. These are very similar to the CF objects that Core Foundation has. OS number, OS Boolean contain ordinal values. And we've got OS data, OS string, and OS symbol to contain string and data values. These are not used for runtime data. You wouldn't use these to encapsulate data you send down to your driver. These are basically used for command and control. They're used to communicate configuration details, things that are typically lower bandwidth.

We also have collection classes, again, very similar to the Core Foundation collection classes. And these collection classes, for the most part, contain other collection classes or strings. If you look at how they're used in I/O Kit and look at how they're used in our drivers, we mostly use them just to build up a data structure of dictionaries and arrays and strings that describe a device, that help with our matching and so on. So I'm not going to get too far into these. They're pretty basic and very similar to other object-oriented systems out there.

Now, we're going to get down into I/O Kit, where notice the prefix has changed from OS to I/O. And there's a bunch of core concepts of I/O Kit I want to talk about. So the Work Loop is probably one of the fundamental core concepts of I/O Kit and the event sources that go with it. Work Loop is our synchronization model.

It's conceptually similar to a CF Run Loop, but its goal, its entire purpose, is to provide you a way to know that your routines are not being re-entered. You can have routines that are run on the Work Loop, as we say, and those routines will be effectively single-threaded. So you don't have to worry about protecting critical data structures. You don't have to worry about serializing access to your device. I/O Work Loop handles that for you.

And more importantly, and this is something that it's easy to lose in the details, but Work Loop is crafted specifically to work well when you've got a bunch of drivers stacked up. If you look at the way a system actually works, you've got a device plugged into PCI, and that's got some device plugged into it, and then that's got a partition scheme on top of it. And when you look at how these drivers stack up, and we will in a few minutes, there's a lot of layers there.

And once you start adding up a lot of locking schemes through those layers, you can wind up with some real performance bottlenecks. Work Loop is designed specifically to be efficient in that case, the way it shares context and shares a lock between the various layers, and that's largely transparent to any given layer. So one layer will do the right thing, but then when the system stacks them up, the Work Loop is shared in such a way that's efficient. And that's a very intentional and careful design, though it's not obvious just to look at the APIs or KPIs.

A very important point here, and I want to stress this, this is probably going to be the one area that you have to do some rewriting if you're reporting an existing driver to Mac OS X. Most of the families in I/O Kit assume that you're using a work loop for your synchronization.

Some of them actually require it. And if you're bringing a driver over that already has critical data locking and device serialization, and you try to marry a work loop up with the locking that you've already got in there, you are going to run into deadlocks that are hard to debug.

So I strongly recommend that instead of trying to do a minimal port there, you really tear your code apart, figure out what the critical areas are, and port it cleanly over to work loop. It's going to take a little more work up front, but I guarantee it will save you time in the end.

So the event sources go with the work loop. The work loop, think of it really as a lock, and it's a context on which to run, a single-threaded context. The event sources are the ways that you get into that context, the way that you acquire that lock. And so we have some built into the system. We have an interrupt event source, timer event source. We have an event source that allows you to receive commands coming in from outside of your context. And you can also create custom event sources.

You can, if you needed to have some kind of a special queue, or you wanted to have some special IPC event source, or maybe you have drivers that are cooperating, you can invent your own event sources. It's a class that's available to you. And they're relatively straightforward. You basically have a service routine that you wire up to each one of these. And whenever the timer fires or the interrupt comes in, your service routine is called in the work loop context, and that's guaranteed to be serialized.

Okay, so next on the agenda today, we'll talk about the memory classes a little bit. There's a lot to learn here, and depending on what your driver is going to be doing with memory, I can't really go into all the details because there's simply too many for an overview session like this, but there's some good documentation out there.

This is an area that has changed with Snow Leopard. It actually changed more with the introduction of Intel, but we're continuing as machines get bigger and have more complex memory models, to have to evolve these. So memory descriptor is sort of the basic way that we describe memory.

Now, what do I mean by that? Think about having a user process that wants to do I/O down into the kernel. It has some memory buffer that it's going to write. So it calls into the kernel with this pointer to this memory. Well, that's some memory that's mapped in a user's process.

We need to be able to describe that memory so that kernel drivers can work on it. So we create a memory descriptor and we say, the memory at this address in this process. And that gets kind of wrapped up in a memory descriptor. So the memory descriptor doesn't actually allocate memory, it just describes it.

Now, what's interesting is as this memory descriptor gets passed around through the kernel, different mappings and other things can happen to it. It can be wired. It can be mapped into other address spaces. The memory descriptor keeps track of that for you, so you don't have to worry about repeating operations that have already been done. Memory descriptor kind of caches that information.

Now we do have, I said before, a memory descriptor doesn't allocate memory. We do have a helper for this. We have this buffer memory descriptor. And sometimes you do need to allocate a buffer to send down to a device. Let's say you actually need to create a command buffer.

You need to issue a command where the data is not coming from user-space. You create a buffer memory descriptor. It allocates wired kernel memory with it. You fill in that memory however you need to. And then you can pass it along to lower layers of the system like any other memory descriptor. It can do DMA on it and so forth.

So DMA. If Memory Descriptor describes memory, then I/O DMA Command is the class that allows you to access that memory. In particular, it's there to help you get physical addresses that you can pass down to your DMA controllers on your hardware. So basically it will generate a physical address list, and it will do as much or as little work as necessary to that memory to get it ready for you.

So on systems that require it, it might actually even copy the memory into a low memory buffer for you. It will wire the memory if it's not already wired. It will do whatever is necessary to get it prepared for you. If it was already prepared by somebody else, then that can be very quick.

Any driver that has to actually get to the physical memory or if you just want to read bytes out of the memory, maybe you need to sniff a few bytes to see what the buffer looks like, you'd use DMA command for that as well. Now this class supersedes an older class called I/O Memory Cursor.

Memory Cursor is still available in Snow Leopard. However, it has some architectural limits that kind of bind it to the 32-bit world. So we recommend very strongly that you port all of your code off of Memory Cursor and we recommend that you not use Memory Cursor for any new code at all. Just go straight to DMA command.

So as I said, some changes were necessary to support 64-bit memory. It was really more for user processes than kernel, but these classes, Memory Descriptor, Buffer Memory Descriptor, and DMA Commander are affected by this. And drivers may need to make changes when you're porting to x86/64 with respect to these classes. Obviously, you'll have to change all your pointer math and make sure it's 64-bit clean.

Now drivers that actually subclass I/O Memory Descriptor. This is a practice we strongly discourage. We know that some developers have done it, and we don't prevent it, but we will absolutely, with Snow Leopard, have to update your subclasses of I/O Memory Descriptor if you have any. So the Snow Leopard Developer Seed actually has some release notes on there that give you the correlation between what API you were using and what you need to use. So have a look at those if you're using any of these classes.

We also have some helper classes here: I/O Command, I/O Command Pool. These are really used for putting together buffers of commands that you will want to send down to your driver, and rather than having to allocate them and free them a lot, which fragments kernel memory, you can store them on a command pool and reuse them.

So IPC classes, I call it IPC. It's really Kernel to Other Process Communication. And we have a large variety of ways to do this. And the details really determine which method you're going to want to use. So I'm going to go through them very quickly and just kind of give you a heads up on which each one is strong and what the strengths are of each one. So the simplest but also the lowest bandwidth one here is I/O Registry Entry.

We'll see a lot more about the registry later on, but the registry entry class contains properties that each driver can publish, but it also allows a user process to locate the registry entry object and set properties which the driver can then trap. It's actually very similar in concept to the way I/O controls work, except instead of finding the thing by a device name in /dev, you actually use the I/O Kit framework to look up the registry object you're interested in, and then you can do some I/O to it.

It's, as I said, it's fairly low bandwidth. It uses strings. Everything gets serialized into XML across the user-kernel boundary, so don't do this for video capture, for instance. But it's great if you just want to set a property. You need to, you know, set a buffer size or you need to set a baud rate or something like that. It's perfect for that.

We also have a couple simple queue mechanisms, I/O Data Queue and I/O Shared Data Queue. This is just a simple way of setting up a bucket brigade of bytes between user and kernel. I/O Stream is a relatively new class. We added it with Leopard. It hasn't been widely adopted yet, but I expect to see more uses of it soon.

And basically, it's a few classes that all work together to create a high bandwidth shared memory interface between the kernel, a particular driver, and a user-space process. And it's really intended for doing video frames. That was its reason to come into existence. But anything that uses sort of fixed-size, large-ish buffers that you want to have use a shared memory model to communicate up to user-space or down from user-space, I/O Stream is a good place to look. It's a lockless, single producer, single consumer, and each I/O Stream is one way. Now, if you need to do bidirectional, you just create two I/O Streams. There's nothing wrong with that. And as I said, it was introduced in Leopard, so.

Now, the primary mechanism for doing user-to-kernel communication in I/O Kit is this thing called I/O User Client. I/O User-Client objects are kernel-side objects that represent user-side clients. That's where the name "user-client" comes in. It's kind of a confusing naming at first, but when you think about it, Kernel objects expect to have kernel clients.

If you've got something in the kernel, if ending a service, it generally will expect the client of that service to be in the kernel. A user client lives in the kernel. I/O user client object lives in the kernel, but it handles the bridging of that data, those KPIs, out into user-spaces APIs.

They're called into existence at the request of a user process. A user process locates the driver it's interested in talking to, and if a user client object is available for that class, it can say, "I would like to create one of these." One will be created for it, and that will be its conduit then for getting information into and out of the kernel.

So the neat thing about it is the driver doesn't really care that it's a user client on top of it. The driver just knows that there's some in-kernel client talking to it. It could be another in-kernel driver, it could be a user client. It's a nice, clean abstraction.

So this brings us to I/O Service. I/O Service is the central class of I/O Kit. It is where the bulk of our implementation is for our core APIs, core KPIs, and it is also the root class that all device drivers inherit from. So things like driver lifecycle, driver matching, creating the driver, initializing it, tearing it down, all those things are managed through code and KPIs and I/O service. Power management, access control, and discovery, having somebody discover your driver or you discover some other driver, all happens through I/O service.

So I'm going to talk a little bit about power management. A device driver in its most basic form does no power management. We recommend that you do a little bit. Devices nowadays are certainly more and more constrained by battery size and how much heat they can dissipate. So keeping your devices in their lowest power state is good for your customers.

And so the most basic driver would have two power states. Obviously, you can have an intermediate power state. So you can have a power state that's actually a disk drive where the green on state is active and spun up. The yellow state might actually be in standby but spun down, and then off would be completely powered down. And you can have as many states as you want as long as you can actually have some useful distinction between those states.

Our power manager will allow you to handle as many states as you want, though that may be limited by the family that you're using. So go and look at your family documentation. If it's going to constrain you to a specific number of states, then you'll have to follow those rules. But a generic device driver can create any number of states.

So how do you go about becoming a power-managed driver? So the first thing is you have to define that array of power states, and you describe them in order as to what they're going to be. It's a simple array. It's handed off, I believe, to PMInit or JoinPMtree. I have to look at the documentation to know for sure. But you have to implement one method. You implement setPowerState. And then in your inner start routines, you're going to call these three methods in sequence.

And after you've done that, your set power state routine is going to be called any time the system wants you to change power states. It's very simple. The power manager automatically looks at your children and manages their power state. If they depend on you for power, they'll be powered-- the ordering is guaranteed so that everybody's powered in the correct sequence, so that anybody who's dependent on another node for power will be powered up after the dependent node.

So the I/O Registry. What is the I/O Registry? It is nothing at all like the Windows Registry. Windows Registry is a persistent collection of data that lives on disk. I/O Registry is not. It is a dynamic network of objects that live in the kernel. It's not ever stored. It's not written out. You can do a static dump of it to look at it, but it is a live running structure and it's recreated every time the system boots. There's no store.

So we have these objects. The term "nub" may be new to you. You'll hear it a lot in I/O Kit. So we have driver objects and we have nubs.

[Transcript missing]

So if you just look at a pair of objects and their relationship, the relationships are directional. So in this case, we have a driver object that is a client of a nub. The nub is a provider to that driver. So it's the client-provider relationship.

So additionally, beyond just these being live objects, they also have connected to them a collection of strings and other properties. And these are handy for discovering the objects. They're handy for controlling. If you recall earlier, I said you could set properties on the driver. It was an I/O control-like operation. Well, this is what I'm talking about.

So to further define nubs, what is a nub? Well, it is an I/O service subclass. Everything in the registry pretty much is. They're considered interface objects. Probably the closest thing to think of it is like a /dev node that lives inside the kernel for other kernel drivers. It is the unit of access. You find services by looking for nubs. You acquire the nub. You then talk to the nub. When you're done, you close the nub.

They generally tend to be fairly thin pieces of code that mostly just handle the vetting of clients and pass the request straight on through to the driver. One driver can publish as many nubs as it wants to. Many drivers just publish a single nub. Some publish dozens. But they tend to not have device-specific implementation details in them. They tend to be pretty straightforward conduits down to the driver.

The driver, on the other hand, is kind of the opposite. It's more downward-focused. It's not thinking about clients. It's thinking about its hardware. So it's also an I/O service subclass. They're loaded via matching. They're the thing that gets matched into the system, and this is the piece of code you're going to be writing. And that's where all the device-specific implementation is. So you'll see this kind of hierarchy of nubs and drivers throughout I/O Kit. It's a very common pattern that you'll see.

So how does the registry grow? As I told you earlier, we start with a single nub that represents the entire platform. And nubs have one responsibility when they're first created. When they're registered, their responsibility is to go out and actively seek a client. It's kind of an interesting model. They actually ask the system, well, tell me what drivers are available.

They then go through and look at all of the properties of those drivers and figure out which driver is the best driver to drive them. and they match that in. So in our case, if we're talking about the root nub, it's going to need a platform expert, a platform driver. That, you can kind of think of it as a main system board driver.

Well, that's going to publish nubs for each piece of silicon on there. Obviously more than two, but slides only so big. And those nubs do what they're supposed to do. They go off and look for drivers to drive them. And they're going to match in the suitable driver. And this process continues until the whole registry is built up.

There's a very important point, though. We never consider matching done. We don't have a steady state. We don't say, "Okay, we're done with matching," and we stop. Nubs--just creating a nub and registering it causes the matching process for that nub to occur. So this is how hot swap works on Mac OS X. The system's been up and running for days. You plug in a USB device. The USB driver notices it and publishes a new nub.

So how does it-- what drives the matching? How does it pick the best driver? Well, the details are very family-specific, but they all follow a fairly basic pattern. In the property list, which is a file in the CFBundle that is your text-- There's some properties in there. We have something called I/O Kit Personalities.

A personality is a unit of matching. It's basically a dictionary that says, "I can match on these kinds of things." So here's one matching dictionary. One driver can have multiple matching dictionaries, multiple personalities. Maybe the same driver could load against several different USB devices or several different PCI devices, and they can be matched in as separate personalities. But let's just look at one for now.

So this is the chunk of properties that are necessary to match, and this particular example is a PCI KEXT. Doesn't really matter. The first thing, and this is common to all families, is we have I/O Class. This specifies the name of the class to create. This is sort of the, "Okay, you've loaded my driver. Now what do you do?" It has to instantiate one of these objects, and this is the actual driver object that gets attached to a nub.

The Provider Class, this is you, your driver, declaring what nubs it can successfully attach to. Obviously, you don't want a PCI driver trying to talk to a SCSI nub. That would not be recipe for success. So the Provider Class here tells the system what nub class it can successfully talk to.

So in this particular case, we've got something called I/O PCI Class Match that's relevant to this example. But PCI offers two or three different matching. You can match on class, you can match on vendor ID, device ID, and some other stuff as well. But every family is going to have its own matching details. USB, for example, follows the USB specification for some of its matching details. So you're going to have to look to your family documentation to get details there.

Probescore is a generic I/O Kit concept. Most families support this, not all do. But if there's a tie, if all of the other properties come down and at the end you've got a-- More than one driver identified, we sort them in probe score order to decide which one to load first, give it a first crack at it, and if it fails, then we'll go down to the next one and so on.

So now I want to shift gears a little bit and give you some graphics to see how that user-client connection looks like. I described it earlier in text, and now we're going to see it in pixels. So this is sort of the setup. You've got a nub that represents a service that's of interest to a user-space application, the gray bar at the top.

So it's going to use the I/O Kit framework to locate that nub by its properties. It's going to match it, and it may just say, "Give me all the instances of this class." It may give it something more specific. We have a fairly rich matching language. So once it's found it, it says, "Oh, please conjure me a user-client." And the user-client object gets created in the kernel.

And you notice it even has properties. It's just another in-kernel driver. There's also a user-space plug-in piece, a library that your application can load. And those two pieces, the user-client in the kernel and the plug-in in user-space talk to each other and then that service out to user-space.

So let's talk a little bit about families. A device driver or device is really an intersection of two technologies. Devices are bridges. You've got a USB serial adapter. You've got a PCI Ethernet adapter. You've got, you know, FireWire hard disk drive. They're all bridging from one technology to another. And so, let's talk about the naming and how that fits into the I/O Kit scheme of families.

So devices can be described either by their method of attachment or by the service they provide. And in I/O Kit, we generally name them based on the service they provide. So for a PCI Express, for example, the attachment method is PCI. For a USB HID device, the attachment is USB.

So the orange highlights there show how the attachment point specifies the family that you use as your provider nub. So the provider nub for a PCI device is going to come out of the PCI family. The provider nub for a USB device is going to come out of the USB family.

This is the family that controls matching you in. So if you're writing a PCI Ethernet driver, it's the PCI family that manages the matching process of bringing you in. However, your driver is not a PCI driver. It's an Ethernet driver. It is named based on the service that it provides to the system, and the family that you're going to actually wind up subclassing out of is the family that is described by your service. So PCI Ethernet driver, the provider class is PCI. You're matched in by PCI, but your service class, the actual thing you're going to subclass from, is in the networking family. So keep that in mind. You're always subclassing from a family that describes the service you provide.

So the families, as I said earlier, they provide the protocol specifics for USB or FireWire, whatever. And they provide all the basic classes. So they provide the nub glasses. This is very important, because that's sort of the unit of abstraction. So all USB devices look the same at some basic level.

So you can discover them and talk to them at a basic level. They also provide the driver superclasses, the thing that you're going to extend when you go to write your driver. And where appropriate, they provide I/O user client classes. And they may also provide other helper classes that are useful for managing data in the realm of that technology.

Now the families, of course, they extend I/O Kit behaviors, but they can also replace them or override them. So as I mentioned earlier, USB has some matching rules that are a little bit different than the generic I/O Kit implementation. So you're going to have to look at the family that you're inheriting from and the family that you're matching on to get the details of all that. Power management is another example where families actually extend the I/O Kit model and sometimes force specific implementations. And also for threading and locking. Out of necessity, the families have to know somewhat what your locking model is going to be.

Now that being said, new families can be created, and they have been. Obviously, families can also be obsoleted over time as they become unimportant. So it's a nice way of allowing the technology conveyor belt to keep cranking. New stuff can come in on one side, old stuff can fall off the other side. But I/O Kit as a core technology doesn't really get dated because it's extended by new families.

So this is just a brief list, an incomplete list of I/O Kit families to kind of give you an idea of the granularity and how we break them down. The headers for all these families are in the kernel framework. That's where their KPIs live. And the binaries are actually in the kernel extensions. If you go to system library extensions, all the I/O whatever family texts, that's the actual binary, the runtime code that's going to be running your-- that provides the implementation of those families.

So you want to write a driver, where do you start? The first thing to do is figure out how you're going to attach your driver into the software stack. So run I/O Registry Explorer app or I/O Reg. Attach your device and look for changes. Run I/O Reg again, or if you're running Registry Explorer, you'll see a little green item. So if it's a hot-swappable device, like a USB or FireWare device, the system will notice it appear, and you'll be able to see where in the I/O Registry the nub is going to appear.

Look at that nub. Click on it, look at those properties, and you're going to use your family-specific documentation. You're going to look at the nub that was created for your device, and you're going to be able to figure out what the matching language is going to need to be.

Whether you're going to be a vendor ID match or whatever, that's where you're going to find out. If you've got more than one device that your driver is going to need to support, repeat this process for each one and get a list of personalities that you're going to need to build into your driver.

So that's what it looks like. This is Registry Explorer. It's the projector that makes it a little bit hard to read, but the selected blue highlight here is actually over the new device that I had plugged in. This is actually a USB Wi-Fi dongle. And that's why the text in the highlight bar is green. It's because it's a new device that was plugged in. And then over here on my left, on your right, you'll see the properties for that nub, and there's information in there that would help you know how to match on it.

All right, so you've figured out what your provider family is going to be. You've figured out what your matching dictionary looks like. Let's talk about selecting your family. So in the I/O Kit fundamentals, we have an Appendix A that actually walks you through all the families and tells you what they do. And that's, you know, going to be your quick guide to figure out what family you need to inherit from, what class you need to inherit from.

And the family that you choose is going to help you figure out what superclass you need to inherit from. And one of the best resources there is the Darwin source repository. If you're not sure, if it's not obvious just from looking at the headers, go off and find a driver that does the same thing that the driver you want to write will do. And that's a great place to look. Obviously, you're going to want to do that anyway for other pieces of code. So once you've done that, you select your base class.

And... You're going to go into Xcode and you're going to create a new I/O Kit driver project. You're going to define a new class that extends the base class that you picked. And you're going to add personalities into your Info.plist in your project. And you get the thing to build.

Well, at that point, you now have a shell of a driver. If you actually have it to build, get it to build, and then you put it in the extensions folder, when your device is attached, your driver should load. Now, it's not going to do anything because it's a hollow shell of a driver. But you've kind of got a playground now. You've got a place where you can put your code.

That's really where you're going to start. At that point, you've got to go look at the family-specific details, look at your silicon or whatever it is you're talking to, what are the things that make it unique, and that's where you're going to be coding up. At this point, go ahead and set up the two-machine debugger because you're going to need it. The next step is going to be writing kernel code. If you haven't done that before, this is your first exercise. You will be seeing the panic screen. Learn to love two-machine debugging. It'll make your life a lot easier.

All right, so to put this in pixels, what I kind of just described to you, in the stack that we're immediately concerned with, we've got three objects here. We've got--at the bottom, we have a nub that represents the device that the system detected. In the middle, we have your driver class, the class that you're creating, and then that is going to publish a nub for the rest of the system to match on, depending, again, on your family. So in this particular example, we have an Ethernet controller that's publishing an Ethernet interface nub.

So out of this, this is the code that you're going to need to write. The superclass of Ethernet controller is provided for you. You plug in the functions you need to. And for some families, you will also need to modify the nub. It's going to vary by family, so you have to look at the docs on that.

So, in summary, I/O Kit is the device driver model for Mac OS X. I/O Kit drivers run inside the kernel, and families provide support for those drivers in protocol-specific areas. For applications, I/O Kit is a framework that they use to locate and talk to I/O Kit drivers. And I/O Kit provides many mechanisms for bridging the user-kernel boundary.

You might not even need to do that. I should have mentioned that earlier. A lot of the families take care of that for you, such as Ethernet. Most Ethernet drivers don't care about bridging the user-kernel boundary 'cause we have a whole BSD sockets layer that takes care of that for you. And again, to reiterate, I/O Kit is not part of the iPhone SDK.

So if you'd like some more information, Craig Keithley is the technology manager for I/O Kit. We have, of course, on the seed, we have the release notes that I mentioned that have some good information that you need to know if you're porting a driver. And in the I/O Kit Fundamentals book, there's the section called Hardware Drivers. The URL is there. It actually has--gives you sort of a menu of all our available documentation for I/O Kit and the families. And then there's also the open-source project with the Darwin kernel and all of the related drivers, it will be a tremendous resource for you.

So if you have the opportunity to watch the reruns, on Tuesday there was the 64-bit kernel session. And then later today, we have two more sessions in this room. Right after this, we have the Maximizing Driver Compatibility for I/O Kit Drivers. And this will be talking about how to create one Xcode project that creates a driver to run across multiple generations of Mac OS X. Dean Reece So this is a very simple project.

It works on multiple architectures, multiple memory sizes, basically handles all the permutations that make sense. Then right after lunch, we have the Kernel Extension Management Session, where we'll be learning about the details of how kernel extensions are managed, and we'll learn about how that's changing for Snow Leopard.