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: wwdc2006-404
$eventId
ID of event: wwdc2006
$eventContentId
ID of session without event part: 404
$eventShortId
Shortened ID of event: wwdc06
$year
Year of session: 2006
$extension
Extension of original filename: mov
$filenameAlmostEvery
Filename from "(Almost) Every..." gist: ...

WWDC06 • Session 404

Writing Drivers for Mac OS X: An Introduction to the I/O Kit

OS Foundations • 58:03

The I/O Kit handles most of the heavy lifting of driver development in Mac OS X. Learn when to use (and when not to use) the I/O Kit, and gain a deeper understanding of how the I/O Kit models hardware and I/O connections. Discover how to take advantage of 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.

Speakers: Dean Reece, Nik Gervae

Unlisted on Apple Developer site

Transcript

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

Welcome to the middle of the conference. I hope everybody's found it instructional so far and had a good time. So we've actually got quite a lot to cover this morning, so I hope you've had your coffee, and let's get started. So this morning's session is sort of an introductory session to I/O Kit or possibly a refresher for some of you who've spent some time there. If this is not the session you thought you were in, this is your opportunity to bail. We'll press on now.

So, what is I/O Kit? Sort of in the simplest terms, it is the device driver model for Mac OS X. And a little more specifically, it is the driver model where you write drivers in C++, load them inside the kernel, the Darwin kernel, and they execute in that environment. Applications talk to those drivers indirectly by using mechanisms across the user-kernel boundary, a variety of plug-ins or other system abstractions, and we'll talk about those later.

So to kind of lay it out for you graphically, this is a simplified stack of sort of the Mac OS X operating system. Up above the dashed line there, we have the user space where all the applications run, and they make use of the various frameworks and libraries to use the system services.

And the purple area is the kernel, and within that it's subdivided into a variety of technologies. And the one we're going to be focusing on today is over here, is the I/O Kit space. And that includes what we call families, as well as the actual device drivers. And we'll spend most of our time this morning talking about those. We will talk a little bit about kecks in general a little later on, which is all the gray boxes in this diagram.

So when is it appropriate to use I/O Kit? A lot of devices already are supported with generic drivers or facilities in the system. So in those cases, you won't need to use I/O Kit to write a driver. Also, there are a variety of applications where you would want to have a user-land construct, an application or utility, talking to hardware. Now, we don't consider this an I/O Kit driver because it's not running inside the kernel. Some examples of this, anything that uses the image capture framework, for example, cameras, scanners, printers, these are all user-level plug-ins.

Now, they do use I/O Kit in that they use it to communicate with their hardware, but these drivers don't run in an I/O Kit environment. They actually run out in user-space. And from I/O Kit's perspective, they could just be any application. So when I use the term "applications" in this presentation, I'm not referring just to apps that the user launches.

I really refer to anything running, any task or process running out of the user-- out of the kernel address space. These uses of I/O Kit use the I/O Kit framework. The I/O Kit framework is available for applications and utilities outside of the kernel to gain access to I/O Kit drivers.

Now, device drivers that must run inside the kernel, which is what we'll be focusing on this morning, there's a few criteria there that help you kind of decide where you want to put your driver. Once you start looking at the documentation and some other examples, you'll probably be guided by what other people have done, but a lot of that comes down to these three requirements here. If you need direct access to your hardware, and I mean like register-level I/O, you pretty much have to be in the kernel for that. If you require interrupt handling, you pretty much have to be in the kernel for that.

Also, if your primary client is in the kernel, this is really what's driven most I/O Kit drivers into the kernel. For example, storage drivers, their primary client is the file system layer. So, it only makes sense for storage drivers to be below them in the architecture, so therefore they're in the kernel. Now, this is important. The kernel framework is where you will find all of the APIs that you use inside the kernel.

This includes all of the I/O Kit APIs that I/O Kit drivers use. So, if you're writing an I/O Kit driver, you do not use the I/O Kit framework. I know that's kind of counterintuitive, but look in the kernel framework. That is the one and only framework that you can use inside the kernel.

In that being said, you really should try to avoid writing a kernel-level driver if at all possible, because it's harder to debug, it's a more limited environment, you don't have all the frameworks available, but it is necessary in a lot of cases, so we'll press on and talk about those cases.

Now, I/O Kit has a number of design goals, and I'm going to spend a minute on philosophy here just so you can kind of understand maybe some of the decisions we've made. So writing a device driver in a modern operating system is a very complicated task. You have a lot of things to juggle. From a scheduling perspective, you have multiprocessor systems, you have preemption to worry about. You have multiple address spaces, and now as you've seen from sessions this week, those address spaces can be different sizes. They're not all 32-bit address spaces anymore.

Also, architecture neutrality, also very important to Apple these days. So I/O Kit needs to be able to cope with various ndns and other variations that can be even more subtle. We also really want drivers to focus on mechanism and not on policy. And this actually has driven a number of decisions that we'll kind of hit on later on.

But what I mean by this is drivers are there to service requests from the user-space applications. They're not there to generate I/O. Now, there will be a few cases where they have to generate I/O in the course of doing their business, and that's okay. But all the I/O that they do really should be either a direct or indirect result of some request that came in from above. That's what I mean by mechanism. They simply are there to serve.

I/O Kit is also intended to be sort of a long-lived technology. We really didn't want to encumber it with any specific technology. If you think back to the classic Mac OS days, the SCSI Manager, even though SCSI wasn't really used very much, sort of the end of the days for Mac OS Classic, the SCSI Manager was kind of entrenched in the API set.

And even though people didn't really use it for SCSI, they used it for general disk management. So we didn't want to have something like that in I/O Kit that was perpetuated forever. So what we did was we took all of the device or protocol-specific stuff and split it out into these things called families. Well, they extend I/O Kit. I/O Kit remains generic. The families provide the bus-specific protocols. And of course, third parties can extend I/O Kit either by writing families or drivers within those families.

A word on the kernel environment here. As I said, it's a little different from programming in user space. The Darwin kernel is a process, just like any other process on the system. Well, not just like, but it has a lot in common with other processes. It has threads. It has its own virtual memory address space.

So a lot of the general concepts are the same. But it doesn't have any of the sort of the BSD process attributes that you think of, like you can't open file descriptors. There's lots of things like that you can't do within that environment. So we inject code into the kernel task using something called kernel extensions, or KEXTs. And we've got another speaker coming up to talk about those at the end of the session, and we'll defer that for now.

All the code running in the kernel address space shares the same virtual address space. We call it the kernel virtual address space. Very creative name there. But you'll see it in documentation. That's what it refers to, is the address space that all the code running in the kernel uses. And it is not the same as the physical address space, and it's not the same as any particular user virtual address space. Within the kernel, interfaces are called KPIs, Kernel Programming Interfaces, and again, they're present only in the kernel framework.

I'll touch on this a little bit later, but another aspect of programming in the kernel environment is once your code is running in the kernel environment, you are trusted. You basically have no bounds placed on you as to what you can do, so you have to program responsibly if you're going to do that. The user has to install your kernel extension, they have to authenticate as an admin user to do that. There's a level of trust there, so it's very important that you consider the security implications of the code you're writing.

A few more constraints for the kernel. When you're programming in the kernel, you can think of it like a sort of a long-running app. When the app exits, the machine is shut down, either happily or sadly with a panic. So, do not leak. Make sure your drivers do not leak, because the kernel memory is wired by default.

When you allocate memory or allocate an object in I/O Kit, that is actual physical memory you're pinning down. It can't be paged out later. So pay special attention to that. Floating Point is unavailable in the kernel generally. It is possible to use it. It's not trivially done, so it's not something that we'll be discussing today, but you should assume that it's not available.

Most libraries are not available. We have a very incomplete implementation of libc, for example. Even printf, you know, not all the arguments or the different format options are supported. So it's a very minimalized environment there. This question comes up very frequently. File I/O is not supported from the kernel. This is sort of a restatement of that driver shouldn't be implementing policy. A driver shouldn't be opening and writing files.

If a driver needs some information out of a file, some higher-level entity should open that file, like a daemon or an application utility should open that file and communicate the contents down to the driver. Debugging is via remote GDB. Generally that's attached over Ethernet, but it can also be done through FireWire.

C++ in the kernel. So I/O Kit is C++. We actually use kind of a restricted set. It's very similar to the embedded C++ effort from a few years ago. We don't support exceptions in the kernel. Multiple inheritance are not used. They're not supported. Non-trivial constructors are not supported. Because we don't support exceptions, you basically, your constructors can't fail. So you can't do anything in them that could cause failure. They're generally just for trivial initialization.

So we don't have the standard template library, and we don't have the standard RTTI support from C++. We actually have our own variant of that we call OS Metaclass. So templates are partially supported, and by that I mean we don't use them in Apple, we don't test them. However, we've done nothing to prevent them from working in the compiler. So it's kind of a no-lifeguard on duty situation, but I know there are developers who successfully use them.

And in general, you're free to use any language feature. If you can figure out a way to make a Fortran compile and run inside a text, You're welcome to do that. As long as none of the runtime requirements spill over across the kex boundary, it's not going to create any problems. But basically, we're using C++ as our ABI, and that's what we're going to be talking about today.

So, let's talk about classes. The root class of everything in the kernel C++ environment is called OSObject, and it provides basic object services like introspection, casting, things like that. The reference counting is one of the major things that you'll encounter in I/O Kit objects, so it is important to understand how the reference counting works. But it's basically when you allocate an object, it has a ref count of one. When the ref count goes to zero, that object is automatically freed for you. You should never call free directly on an object. You should use ref counts to manage that.

And this is so important, you'll see it at least three times in this presentation. Make sure and use the OS declare or OS define structure macros for every class that you create. This is a set of macros that generates some additional information that our binary compatibility patcher uses.

So when we load your text on future operating systems, we can patch the V tables so that it will work correctly. If you don't put these in, your driver will probably work until the next OS comes out. So, the evil of this is if you don't put it in there, there's no immediate symptom. It doesn't happen until your driver's already out in the field.

We have a number of container and collection classes. I'm not going to go into these because there's plenty of documentation on them. They're basically object wrappers that either contain other objects or contain quantities or strings. They're used for communication with I/O Kit and for communicating status and configuring drivers.

Now, probably the most important class for anybody playing an I/O Kit is I/O Service. This is where the bulk of the I/O Kit's implementation exists. This is where the bulk of the services are implemented. So, you'll need to become very familiar with this class. Every driver class in I/O Kit inherits from it, and it handles things like the driver lifecycle, the matching in of drivers, the initializing them, tearing them down later.

Power management is driven entirely out of this object. Access control, the ability to have one object owned possibly by multiple drivers or owned exclusively by a single driver, there's implementation in there for that. In general, service rendezvous, how you find things in I/O Kit, how you acquire them, it's all done through I/O Service.

Now there's two different roles that subclasses of I/O Service can play. The first role is that of a driver. This is the actual implementation of something. This is the code that knows how to take a generic thing like a SCSI command and get it out on the silicon, out on the bus. Okay, that's the piece you're going to write.

The other object is called a nub, and this is a little bit more conceptual. It's more of an interface. You often don't have to subclass these, but these are the sort of the units of attainable service. When you publish a nub, you're saying, "Here's a service. Somebody else in the operating system can acquire it, can open it." If you have multiple services you export, you create multiple nubs.

Memory Management. I won't spend a lot of time on these. There's quite a lot there. In fact, there's a whole other session following this one that you would probably want to attend. I/O Memory Descriptor is just that. It describes memory. It's a class that we use as sort of a universal token when we communicate an I/O request across the various drivers. It remembers what task the request came from.

It knows if it's been mapped into the kernel address space. It knows if it's been wired. It keeps track of all that. So if one driver wires it down and then the next driver goes to wire it, it remembers that it's already wired. So it's very efficient when you have multiple drivers acting on the same memory.

I/O DMA Command is sort of our next generation replacement for I/O Memory Cursor, and it is necessary on the new Intel 64-bit machines. I won't spend a lot of time talking about it because about half of the following session in this room is dedicated to this class. But it basically will take your constraints. You'll say my DMA engine is capable of this reach, this stride, and so on.

And whenever you go to say, well, here's an I/O Memory Descriptor, I need to be able to hand this off down into my hardware with these constraints. It will do the right thing on that hardware. It'll either do the mappings, it'll copy it if necessary to double buffer, and it also has API for getting 64-bit physical addresses, which will be important.

Synchronization. This is probably one of the areas where I/O Kit diverges from most other driver models, and so you'll want to pay close attention here. I/O Kit's synchronization model actually comes from the same lineage as the CF run loop. Actually, I/O Kit has a common ancestor with Cocoa, if you can believe that.

So, I/O Work Loop is basically, you can, conceptually, you can think of it as a thread. It is, in fact, a lock and a thread, and I'm not going to go into the details here. But the idea is, when you have something attached to a work loop, it won't ever be re-entered.

It guarantees that only one of the things attached to the work loop is executing at a time. It's a way of serializing access to your hardware. Now, the way you do that is by attaching these things called event sources. An event source can be an interrupt, it can be a timer, it can be an arbitrary thing that you've created. Maybe you've generated your own command queue and you attach that as an event source.

So the idea is that events come in, like an interrupt or a timeout, and you've specified a callback with that event source. When that event triggers, at some point, hopefully very shortly after the event triggers, your handler will be called, and it will be called in a guaranteed synchronized manner. This is actually a very simple mechanism, and it's actually carefully crafted to work well where you have multiple drivers interacting together that are not aware of each other's internal locking structure.

It uses recursive spin locks so that you can actually share a work loop across multiple drivers, and this is being done for you whether you know it or not. And it's done in a way to prevent additional context switches as data moves up and down driver stacks. So it's intended to be simple for you to use, but it's also intended to be very efficient for the system. So pay attention and make sure you're using it correctly. Probably one of the number one pitfalls that I've seen with people porting drivers to Mac OS X is they say, "Well, I've already got a locking structure in this driver that works.

I'm not going to mess with it. I'm just going to kind of cobble on a work loop to the side to get it to work with I/O Kit." You will deadlock frequently. It will not be a pleasant debugging experience. So I strongly recommend that you don't try that approach. If you can successfully go that way, you'll be lucky, but you need to look at I/O Work Loop carefully.

As I spoke about before, I/O Kit families provide bus or protocol-specific implementation to the system. And the families can be introduced at any time. Like the ADB family is probably on its last legs. There's not a lot of reason to keep it around for many more years. So they can come and go over time with the core of I/O Kit remaining untouched.

And the way you can spot the family API or I/O Kit API is it has the I/O prefix. Except for a few small exceptions, every time you see the I/O prefix, that's I/O Kit. By that token, you should not be writing anything with an I/O prefix in your code. That's for us.

Again, look in the kernel framework. That's where all of the I/O Kit APIs for in-kernel development go. That's where all the family APIs go. And the specific family that you use depends on What service your driver is going to provide. So you can think of it this way.

The PCI Ethernet adapter that the user plugs in, what is the user putting in? They're putting in an Ethernet card. That's what they care about. That's where you look to find your family. A lot of developers will, the immediate thought is, "Well, I'm attached via PCI, therefore I must go through the PCI family." You will use the PCI family, but that is not the service that your driver is providing.

Here's a sort of short list of families that are available. Some of these are actually sort of collapsed lists of multiple sub-families as well. There's quite a few to choose from, and in fact your driver might fall into the sort of category we have of familyless drivers, and that's entirely possible. It just means you don't actually inherit any implementation. You can inherit straight from I/O Service, implement whatever you need to, and you're not actually getting any functionality from a family. But it's not required that you actually inherit from a family.

Another concept that's very important to I/O Kit is something we call the I/O Registry. It is not the Windows Registry. I'll say that again. It is not the Windows registry. Thanks. It is not persistent. It is actually the network of live objects. Right? These are all the I/O Kit objects, all the I/O Service objects, sort of in this cloud. And all the registry is is just a collection of pointers linking them together.

It's very simple. We often speak of it as this tree structure. It is a tree structure. It's actually a directed acyclic graph because it is possible to have one driver with two providers below it, like in the case of RAID or a few other rare cases. But think of it as a tree.

We have one node at the bottom that represents the platform itself, the platform nub. And then everything above that represents more specific and more specific hardware until you actually sort of get to the boundaries where it steps out of I/O Kit into some other family like the file systems or the networking stacks. So focusing in on just two of the objects here, we refer to the relationship here as a client-provider relationship. And the idea here is the NUB is providing some service that the driver, in this case, is a client of. Simple as that.

In addition to the actual objects that are running in the registry, we have these dictionaries attached to the side. These are basically collections of strings, property names, and values that can be used to describe your driver. You can use it for status. You can use it for stats, command and control. It is, you can think of it as sort of like an I/Octal replacement in I/O Kit. It's not high bandwidth. You will not be doing disk I/O through the registry, but you will be discovering maybe the capabilities of the drive through the registry.

So every object in the registry has this. There's a command line utility called I/O Reg that will dump the contents of the registry. There's also an I/O Registry Explorer app that will allow you to graphically do this. If you're new to I/O Kit, try I/O Reg. You're going to learn a tremendous amount just by poking around in there. And of course, apps use the registry as sort of their main rendezvous mechanism. They discover things by doing searches on those dictionaries that are hanging off the side. We'll draw that in a bit.

There. So, in this case, we have an application which is using the I/O Kit framework to search for a driver. It's very common that you'll search for a driver by class because you know what you're looking for. You can also search by any variety of properties. We've got a staggering array of search capabilities in the registry.

And once you've found the object that you're interested in, you can communicate with it simply by getting and setting properties in the registry. Or you can open up a high bandwidth channel we call an I/O User Client, and I'll spend time talking about that later. But basically it is a dedicated channel that uses Mach IPC or Mach shared memory, depending on the context, to communicate across the user-kernel boundary.

Okay, we've just spent a little while talking about sort of the high-level concepts in I/O Kit. We've kind of taught you basic vocabulary. To drive that home, I think we're going to go through an example here of kind of how you would go about piecing a driver together using I/O Kit.

So if you're brand new to Mac OS X, you need to discover developer.apple.com. It has a fantastic search engine that searches all the available documentation, and I use it daily. It's a very powerful tool. So go there, browse it, become comfortable with it. We've created a few tutorials that we thought would be useful for getting people's feet wet sort of on day one when they were first exposed to kernel development. So the first one is called Hello Kernel. It is literally writing a kernel extension that does nothing but log Hello Kernels and then exiting.

So what you need to do is spend the half a day or so it takes to download this and experiment with it. There's also a Hello I/O Kit tutorial which takes that and puts the C++ wrappers around it that you'll need to have a driver object interact with I/O Kit. There's also a debugger tutorial that talks you through setting up the two-machine environment.

So we're writing a driver. Where do we start? Well, as I said before, you need to think about the service that you're vending out to the operating system. So a PCI Ethernet driver presents an Ethernet interface. A mouse driver provides a pointing device. In neither of those cases is the method of attachment relevant, at least not yet.

So in our exercise, we're going to write a block storage device that attaches via PCI. This is a hypothetical device. You can think of it as a battery-backed-up RAM disk or it could be some remote thing. It doesn't really matter, but it's a way of persistently storing data through a PCI card.

And at this point in the exercise, the fact that it's PCI isn't really relevant. So we have to select a family first. Again, go to developer.apple.com. If you go to the I/O Kit Fundamentals document, which I highly recommend, by the way, there's Appendix A in there, which is a family reference guide. It has like a page for each family that kind of gives you the scope of what it covers. That's your sort of first stop to figure this out.

So you select an appropriate driver, and then maybe you go search Darwin for examples of drivers written for that family. So in our case, we're going to discover the Mass Storage Device Driver Programming Guide, and it's going to become clear that our driver fits into the I/O storage family.

Now, the approach we're going to take from a class perspective is a little bit backwards than you might think. And we wouldn't code in this order, but this is the way we're going to design. So we're going to start at the top. Again, we're service-oriented. What service are we exporting to the world, to the OS in this case? So we've got to determine what our NUB class is.

Well, from looking at the headers in our selected family, we'll see this little quote here: "The I/O Block Storage Device class exports the generic block storage protocol." That's what we're looking for. So in our driver here, the nubs are going to be of class "I/O Block Storage Device." A word on I/O Kit naming. You'll very commonly see the suffix "device." For something that is representing a device, sort of the proxy for a device in the system. So this class, when you instantiate one of them, represents the actual device. It is the software embodiment of the device.

A lot of nubs have a device as their suffix, and you'll find that "driver" is very commonly the suffix for the other class we talked about. So the base class. This is the class that you're going to use to write all your code in. This is going to be the class that then goes and publishes that nub that we just discovered.

Now, our first way of determining what class to use here is going to be, again, to look at the nub class. Very few nubs are completely agnostic. They have some particular class they want to be published by, and so they'll usually point you in the right direction. And sure enough, the transport driver can be of any type as long as it inherits from I/O Service.

So it points us in the right direction, just happens that it's a very vague direction. In this case, it doesn't matter. Most nubs do care what their providing class is going to be. In our case, for this example, it doesn't. So we're just going to inherit from I/O Service.

Now the third bubble down on our I/O Registry graph is going to be the Provider Class. This is not a class that we write. This is not a class that we instantiate. This is something that is given to us. This is handed to us as our way of attaching and getting at our hardware. So we call down into this object to communicate to the hardware that we're going to be driving.

In our particular example, we know it's PCI. This is where the attachment method becomes relevant. What's interesting here is we're not inheriting any code from the PCI family. All we're doing is calling into an object that it provides, in this case a nub called I/O PCI Device. Again, the device suffix represents the, indicates that the object you're talking to is a proxy for the hardware. In this case, it represents the PCI hardware on the bus.

So, I've talked through that. Let's look at it graphically. So, on the top there, we have our nub, which is going to be an I/O block storage device subclass. Our driver, where the meat of our implementation is going to go, is going to be a subclass of I/O Service, and then I/O PCI device is going to be our provider. This is where we're going to get access to our PCI card. These are the parts that we need to write.

Okay, we have to write code in our NUB subclass and we have to write code in our driver subclass. And depending on the family and depending on exactly what you're writing, this can vary. It's very common that you don't have to write any code in your NUB at all.

Now another interesting thing here is why split this up? Why do we have two objects in this hierarchy and why are we writing code in both of them? Seems like it would be simpler to combine the two. Well, this is a contrived example that's very simple. Most controllers can export more than one device. So it wouldn't be uncommon for your controller to export two, three, four nubs. So when you think about that, it makes more sense to have this split.

But if you do have a case where you have a one-to-one relationship between the controller and the nub, in many cases I/O Kit does support combining them into one class. If you recall, the nub documentation said that its provider, the actual driver class, could be any subclass of I/O Service.

Well, the nub itself is a subclass of I/O Service. So you can imply from that it would be okay for the nub to basically be driven by itself. In this case, you could write all your code in one blob, and it would just simply publish itself. You'll find there are not many cases where this is actually useful, but for sort of first time experimenting with I/O Kit, you might find it handy.

So, let's talk about implementation a little bit. We need to write the NUB class. Again, this is the thing that presents our interface, our service out to I/O Kit. And the details here are going to be family specific. We're not going to spend a lot of time on APIs, but we will talk about them a little bit.

So again, looking, going back to our headers, we find this comment. A typical implementation for a subclass relays all the methods down to its provider, which implements the device-specific behavior. So back in that stacked diagram, the upper blue box that we're writing is primarily going to just be pass-through. And we've referred to this in the past as sort of an impedance matching layer. This is because we have two I/O Kit objects stacked up that don't inherit from a common tree.

Therefore, they can't make assumptions about what class the other one is, and they can't cast. So what we have to do is we use the nub object as a intermediary that is aware of the classes and can do the right thing. And so it's basically simply calling through directly down to its provider.

As I said before, you're going to see this in a few places. Here we're implementing a class. Make sure and put these macros in. Every time you implement a class, make sure these are in. The OS declare structures and define structures are necessary for binary compatibility. So looking through the header, we find we have to implement about 20 methods in this subclass. So let's look at what that breaks down to, just to give you an idea of what you'll be facing.

Well, again, most of them are simple down calls. Fourteen of them are just sort of capabilities and description functions, like get the vendor name, get the block size, get the block count. These are very trivial getter-setter functions. So that leaves us with about six that are actually important, complicated functions. On the data path, we basically only have two.

We have doAsyncReadWrite, and you'll notice the first argument there is an I/O memory descriptor. As I said, we use memory descriptors as a token to pass memory around between drivers. Well, this is where the block storage layer is receiving a memory descriptor that describes the memory that we're either going to be reading from or writing into. There's a block count, excuse me, a block index and a block count, and then there's a completion callback. Pretty much all of the data APIs in I/O Kit you'll find are asynchronous. We use a callback, and so that's what that's about.

Do synchronize cache is necessary for basically guaranteeing that all the writes that have happened down the stack actually get flushed to media. So we use this, I believe, for our journaling to make sure the metadata is flushed out. Then there's a few methods here for media management. We've got the eject, we've got something for reporting the media state, whether it's been inserted or not, whether it's changed since we last checked. And there's also formatting and locking the media. Again, we're not going to go into those, but these are just basic operations for managing a device. You can see sort of where the complexity here is really is around do async read/write.

Of course, there's also the additional methods that we call driver lifecycle methods that you have to implement as being part of I/O Kit. Init, start, and stop are sort of the most basic ones. There's additional ones you can implement. There's power management methods you'll want to implement. But the one thing I'll call out here is in your start method, in your nub, you're going to want to call register service. This is the thing that kicks off a lot of activity in I/O Kit. Register service says I am now available and ready for the operating system to start sending requests to me.

And I'll also point out your driver cannot be located in the registry using searching unless you've registered it. The theory there is if you don't register yourself, you don't want to be found. It's like having an unlisted number. So somebody can still navigate the registry and find you topographically, but you can't do a generic search and find an unregistered object.

So down to the driver class. Again, this is sort of the bulk of the implementation that you're going to be thinking about. This is where those commands coming down from the nub are going to have to actually be translated into something that your PCI driver can actually, or PCI hardware can actually execute. So what you want to do here is try to inherit as much behavior as possible.

In our particular exercise, we inherit nothing because we're using I/O Service as our superclass. Different families will have different details, but basically we try to embody as much common code in the superclass as possible. And your driver really only needs to implement the differences or the details that make your hardware unique. One way of thinking about it that I've seen people, seen sort of the light bulb go off, is don't think of the family as a plug-in to your driver.

Think of your driver as a plug-in to the family, right? The family is the thing driving this whole operation. It knows how the hardware works. It knows how to tie in with the rest of I/O Kit. Your driver is a plug-in to that family to give it hardware-specific details. So your driver is responding to the family as opposed to controlling the way the family works. Don't try to drive the family. Your job is to drive the hardware.

Okay, so we're going to declare a new subclass of I/O Service, and again the macros. Every time you declare a new class, it's so important. And probe and start are our first two entry points. Probe just basically says, "Can you drive this hardware?" And start says, "Start driving this hardware." Okay, again, we're going to have to provide an init routine. This is responsible for setting up any structures, anything that we're going to need to do that's sort of trivial.

There's really not a whole lot to say there. When you call your init method, you're probably going to want to pass in some configuration data, but it's basically standard object-oriented coding here. Attach is how you get attached into the registry. This is the method that actually creates that link between provider and client. And again, start is what will be called on you when the I/O PCI device below you is ready for you to start driving it.

And again, you're going to have your nub call Register Service to publish itself out into the registry so that other drivers can then attach to it and start doing their thing. Now, your driver class can instantiate as many nubs as necessary. If you've got a piece of hardware that can run four drives or 12 drives or whatever, you can instantiate a nub for each drive. Another important thing here is they don't all have to be of the same class. You can very easily write a driver that supports two or three different kinds of functions, so the nubs can be dissimilar.

Okay, so we've got our driver class. It's published in Nub. It's gone through all that, but we still have to deal with our provider. We have to gain access to our hardware. So, to do that, we're going to wind up calling down into that driver a good bit.

As I said earlier, I/O Workloop is designed to sort of allow for sharing vertically through the driver stack. Well, you get your Workloop from your provider. There's a method called, oddly enough, GetWorkloop. And so when you get that, you will synchronize all of your operations on that work loop, so when you call down to the provider, you're sharing the same work loop he is.

In our particular case, it's a PCI, I/O PCI device. Now, what does that do? Again, it's all PCI-specific. It maps the base address registers into your address space so you can actually talk to the hardware. It allows you to access config space. It hooks you up to your interrupts. You know, all the basic stuff you'd expect from a PCI family. You generally use it mostly at setup time.

You very rarely will call down into the PCI family to do actual memory reads and writes. You can, and for generating config space access, you probably will. But once you've got your registers mapped, you pretty much are just doing reads and writes straight to those registers. That's unique to PCI. If you're nubbed, you're matching against the USB. Obviously, you don't get registers to talk to your USB device. You're going to be calling other APIs.

Also, our provider is responsible for matching our driver to the hardware. If you remember, the IOPCI device nub, its job in the system is to present a piece of hardware. It's a proxy for that piece of hardware. So its job is to also then go off and find a driver that can drive that hardware.

So let's talk about matching a little bit. Matching starts off outside of code in a driver's Info.plist file. This is a dictionary, it's actually an XML file. I've presented it here in a slightly more readable form than XML, because otherwise it wouldn't fit on the slide. The idea here is that you basically have a dictionary of what we call I/O Kit personalities. This is a property that's at the top level of your Info.plist file.

Now, you'll notice it's plural. I'm only showing one personality here, but it is entirely possible for your driver to have multiple personalities. And what does that mean? Well, you could have slightly similar "There are a lot of different hardware that you want to support, and they can maybe be supported by the same driver, but you've got to have different matching information for them. Or maybe you want to have a few different parameters, and so you might create a personality for each of them. For our example, we're just going to have one personality, though.

And Apple Marketing has requested that we call it Apple PCI Storage Thingy. So, I'm getting ahead of myself here. So, the name here actually is completely irrelevant. This is just a unique identifier. Because it's a dictionary, each entry has to have a unique name. It's not really used by anything.

The Provider Class: This tells I/O Kit what class you need as a provider. Again, you're writing your driver to make calls down into, in our case, PCI, so it wouldn't work at all if our driver somehow magically got attached to a USB device, because the map bar functions don't exist there. So this is basically saying don't even consider this driver for anything other than a PCI device.

Now the next two properties are PCI-specific, and these are things the PCI-nub is going to look at to try to figure out if your driver is a good match. Just a couple random examples here: I/O PCI match matches you against the PCI vendor and device IDs. There's a number of properties you can look up in the PCI family, so you can determine if it's the main ID or the sub-ID. You can also use bitwise and if you want to do a wild mask.

The idea here is it's a space-separated list. If any of the IDs on that space-separated list match, then it's considered a match. PCI Class Match. This is similar, except instead of looking at the vendor and device codes, it's looking at the class code, which is a 24-bit quantity. Again, it's PCI-specific.

The general rule, and again, you have to look at the family to know for sure, but the general rule is if you've listed multiple properties, like these two, Both of them must match for the driver to be considered a match. So it's an AND operation between all the matching criteria you put in there. Again, it's dependent on the family, so look in the documentation for that.

Probe score. This is a little bit under-documented. This is a quantity that's used to sort if you have multiple drivers that may have made it this far. So if you have multiple drivers that all make it past the provider class criteria and the other family-specific matching criteria, then we've got a multi-way race we have to resolve. So we sort based on probe score.

So you need to look at the family documentation to get an idea of what probe scores are reasonable. 1,000 is the default probe score. If you have a driver that's specific to your vendor, If you have a vendor-specific driver or a device-specific or device-family-specific, you're going to want to put a higher probe score so that you win out over a generic device driver.

You also have to tell I/O Kit what class to instantiate. This is the actual class name that we're going to pass off into the OS Meta Class stuff and say, "Conjure one of these up." And so after we've loaded your KEXT into the kernel, we'll go and create one of these and attach it to the IOPCI device provider class. That's how the whole stacking starts off.

So, crossing the user-kernel boundary. This is actually, in many cases, not going to be necessary for you to do, because if you're writing, in our case, a blocked storage driver, there's really no reason for us to provide a new mechanism. Mac OS X already has perfectly fine storage APIs for file systems to mount and so on.

So, it doesn't really apply so much to our example, but if you're writing a driver that does need some kind of a custom interface, there's a couple possibilities here. So, again, mass storage and networking is serial. All use BSD style, either sockets or device nodes. And then most of the other families provide other mechanisms for getting at devices, generally I/O user clients. But if you want to roll your own, you can.

And as I showed you before on one of the graphics, there's two ways to have your app communicate down to your driver. The first way is very I/O control-like. It's using the registry to get and set properties. It's...you're going to have to use the registry to find your driver in the first place. So you've already gone through... 90% of what you're going to have to do to find the device, and then you can simply start getting properties on it or setting it.

If you need something that's higher bandwidth, you need to move either large amounts of memory or you need to move memory very quickly, you can use an I/O User-Client subclass. Now, why do we call it I/O User-Client? I'll show you a graphic in a second that'll make it maybe a little more clear. But the idea here is from a kernel driver perspective, You have a client that lives in the user space. That's literally where the name came from. So this is an object that lives in the kernel that is a proxy for some out-of-kernel client.

They're fairly simple. You can write a user client in just a few dozen lines of code. There's an example on the DTS website. You can look for, I believe it's just called Simple User Client Example. They're very versatile. You can do quite a lot with them. And they are efficient. So it's sometimes hard to look at a C++ class and get an understanding of sort of what's underneath the hood and know if it's going to be a pig or if it's going to be efficient.

In this case, we make these very efficient because we use them quite a bit in our own code. So they have to work well. So the basic process is you define a sort of an indexed array of entry points. So you can think of it as simply numbered entry points. And you define them in a header that's going to be common to both your user-space code and your kernel code.

Then you have an either your application directly or if you're creating a library or a plug-in, that code creates a stub function for each of those numbered entry points. And then that's going to call into an I/O Kit library function that basically says call index number blah on this object.

On the kernel side, you create an I/O user client subclass. And that's what actually implements the entry points. All right, there's actually some subtlety here, and I'll go through it graphically again just so you understand. But step one, the application uses the framework to locate the thing of interest. In this case, we have properties on a nub.

Once it's discovered it, it can then create a direct connection to a user client. Now you notice that user client was conjured up as part of this process. So what happens is the app calls I/O Service Open, and that basically will cause your driver, if it has a user client defined, to conjure one of those user clients up.

And it's very important to understand is this user client is private to your application. If there are three apps all talking to your driver, each one will get a private copy of the user client. This is very important for asset tracking, because if you have multiple I/Os in flight, and one of the three apps crashes, we have to know what I/Os to terminate or who to send the response to. So each user client represents one user client. And that VINs our custom API.

Okay, so now if the app crashes, the user client is notified. It discovers that the client, the thing it's representing has gone away. Now it's still stacked there in the kernel. It still has this nub open and potentially has in-flight IOs going. So its responsibility is to clean that mess up. It's either got to terminate the IOs if that's possible, or it's got to wait for them to complete. It's got to release any in-kernel resources that are being held on that user's behalf.

This is very important because if you don't, it's possible now for a user application to cause kernel resources to be permanently pinned. So the user client must clean up after itself. The idea is this needs to be completely symmetrical. Once it's gone, it needs to leave no traces. So as I said before, our user client provides a custom interface to our drivers, something the I/O Kit doesn't provide by default. The problem with this is, it also provides our custom APIs to any software.

This is very, very important. When you're designing your user client, remember I said all code running in the kernel is trusted. Well, the user client is a bridge between the user and kernel space. We trust your code in the kernel, therefore it needs to be trustworthy. So the user-client subclass must defend against inappropriate access.

Now there's some very basic things. This is very much like what you would think of when writing a set UID application. So first off, make sure your API is minimal. Don't implement more than you need to. Don't write "dev kmim" in your user-client just because you can. Okay? That would be very bad and it would not serve your users.

Don't receive kernel pointers. You cannot trust a pointer coming in from user-space in an attempt to reference kernel code. So, for example, if you wanted to have an API that handed a pointer, a kernel address out to user-space, and then later let it send it back to you, you cannot dereference that pointer. You don't know if it's still valid. You don't know if it's been modified by the user. So you would want to use a hash table or some other mechanism to make sure that you can validate that address yourself.

Invalidate the client credentials. It's possible for you to determine if the client is a GUI session. It's possible to determine if it's privileged or an admin user. There are a few APIs for that. So if you can restrict the users that would be able to access your device, do that. But please, please, please, if you're writing user-client, think about this. It's very important. Okay, thank you very much. With that, I'd like to hand the stage over to Nik Gervae. He's going to come up and talk about text management now.

Is the mic on? Yes, it is now. All right. So Dean talked about what you're going to do to build a KEXT. I'm going to talk to you about how you deploy it and the tools you use. So first off, what is a KEXT? It's basically a bundle, and it is the plug-in format for a kernel. It is, at core, a relocatable object file, unlinked yet. It's packaged in a bundle directory with an info dictionary, and that's pretty much all that a KEXT is, is an info dictionary and a binary.

The info dictionary identifies the KEXT in version by CFBundle identifier and lists the matching personalities as Dean covered. Those are used to match I-O kit drivers and load them into the kernel. The other kinds of KEXTs are loaded via different mechanisms. It also lists libraries needed. We haven't covered that so far. Most applications, you'll list the libraries in Xcode. They get linked when you build it.

That's not how things work with KEXTs. Those get linked at load time. KEXTs themselves can serve as libraries. In fact, all of those families Dean listed earlier are loadable KEXTs. And even the kernel itself is represented as a set of library KEXTs. The kernel itself lives in slash as Mach kernel, but there are a bunch of pseudo KEXTs, as we call them. They don't have an object file, but they do have version information and compatibility.

The dependencies that you declare go under the OS Bundle Libraries property in your info dictionary. Each key is a bundle identifier, such as com.apple.kpi Mach, if you need to use Mach resources. And the values are version strings. This is basically whatever version you need or any compatible later version.

For kernel components, you use com.apple.kpi.mocbsd/libkern or I/O Kit. If you need compatibility with pre-Tiger systems, you can use... :com.apple.kernel and any of those. And I've got something on my screen that I didn't want. There we go. You should not mix those. KPI is a new mechanism as of Tiger. .kernel is the older mechanism.

Don't mix them. It may work, but you will run into problems. And also, you can use the new keckslibs program to find any declarations you need. As I mentioned before, kecks aren't linked when you build them. You won't know until you try to load it which families you need. But if you run keckslibs, it will actually tell you, and you can even ask it to present that as XML. You can just paste it into your project.

The tools that you actually use are these: kextload is what you'll use during development, kextunload when you're done, you can unload your KEXT. It is the standard load and unload API for non-I/O Kit KEXTs. So you guys are only going to be using this when you're doing development, basically.

Another program is kextee. This is a continuously running daemon. It loads drivers on demand from the kernel, and it is the standard load mechanism for I/O Kit KEXTs. New in Leopard is the kextlabs program that I just mentioned. Once you've built your KEXT, you can just run this program on it. It will tell you what libraries you need to declare in order to then have it load properly.

KextStat lists running KEXTs in the kernel by bundle ID and version with dependency information as well. It will actually show you what KEXTs your KEXT is linked against. Some advanced programs that we use for system optimization are kextcache and mkextunpack. You probably won't need to worry about those at first. And finally, another new feature in Leopard is kextfind. You can ask it to find KEXTs with a given bundle ID, with various properties, any KEXTs that have problems. like find command for the extensions folder.

When you use kextload, this is used as both a development and a diagnostic tool at present. While you're using development, doing it for development, you'll use kextload manually to load your KEXT. If you have any problems, you run kextload-nt. I can't tell you how many times people within Apple call me and say, I'm having a problem with my KEXT. It won't load. I say, run kextload-nt, and they go, oh, there it is. There's my problem.

As a deployment tool, you're only going to need to use this with non-IO kit KEXTs, usually via an exec call. Some running application will set up the command line arguments and then do an exec. You should avoid using dash-t or other interactive and development options when you use it this way. Basically, only use the dash-b option for bundle ID, dash-d to list an explicit dependency, and dash-i to specify an alternative directory for dependencies.

Now, as far as installing kernel extensions go, keep in mind that kecks are much more sensitive than other software. They are loaded into the kernel environment and can pretty much do anything they want. So when you install your kecks, it has to be installed as owned by a root and wheel, and it must not be writable by group another.

You should also make sure that your kecks get installed atomically. You don't want a partially written kecks even being considered for loading into the kernel. So make sure you copy the whole thing to a temporary directory on the boot volume first, then change its owner group of permissions as needed, and move it atomically into the extensions folder.

When you're done, you can run a touch command on the system library extensions folder and send a SIGHUB to kexty if it's running, and you should check for that. That will cause all of the caches that we build to be updated. Don't go poking around for caches yourself. Don't look for them. They do move around and see the technical QA 1319 for more details and specifically for compatibility information with older versions of the OS.

And now that we've shot a fire hose of information at you, you can go look for these other titles on the ADC website. Dean's already mentioned a few of these. We've got the Kernel Extensions Concepts, Hello Kernel, Hello I/O Kit, and Hello Debugger, which walks you through doing a two-machine debug session. The I/O Kit Fundamentals document, which is more conceptual, and then related sessions today, which include the following sessions, 64-bit I/O Kit Drivers, the I/O Technologies Overview, and our lab later this week. There we go.