Programming the Microsoft Windows Driver Model

How Drivers Work

A useful way to think of a complete driver is as a container for a collection of subroutines that the operating system calls to perform various operations that relate to your hardware. Figure 2-1 illustrates this concept. Some routines, such as the DriverEntry and AddDevice routines, as well as dispatch functions for a few types of I/O Request Packet (IRP), will be present in every such container. Drivers that need to queue requests might have a StartIo routine. Drivers that perform direct memory access (DMA) transfers will have an AdapterControl routine. Drivers for devices that generate hardware interrupts will have an interrupt service routine (ISR) and a deferred procedure call (DPC) routine. Most drivers will have dispatch functions for several types of IRP besides the three that are shown in Figure 2-1. One of your jobs as the author of a WDM driver, therefore, is to select the functions that need to be included in your particular container.

Figure 2-1. A driver considered as a package of subroutines.

I ll show you in this chapter how to write the DriverEntry and AddDevice routines for a monolithic function driver, one of the types of WDM driver this book discusses. As you ll learn in later chapters, filter drivers also have DriverEntry and AddDevice routines that are similar to what you ll see here. As you ll also learn, minidrivers have very different DriverEntry routines and may or may not have AddDevice routines, all depending on how the author of the associated class driver designed the class driver interface.

How Applications Work

It s worth a moment to reflect on the implications of the package of subroutines model for a driver by contrasting it with the main program and helpers model that applies to an application. Consider this program, which is among the first that many of us learn to write:

int main(int argc, char* argv[]) { printf("Hello, world!"); return 0; }

This program consists of a main program named main and a library of helper routines, most of which we don t explicitly call. One of the helper routines, printf, prints a message to the standard output file. After compiling the source module containing the main program and linking it with a runtime library containing printf and the other helper routines needed by the main program, you would end up with an executable module that you might name HELLO.EXE. I ll go so far as to call this module by the grandiose name application because it s identical in principle to every other application now in existence or hereafter written. You could invoke this application from a command prompt this way:

C:\>hello Hello, world! C:\>

Here are some other common facts about applications:

The interesting thing about HELLO.EXE is that once the operating system gives it control, it doesn t return until it s completely done with the task it performs. That s a characteristic of every application you ll ever use in Windows, actually. In a console-mode application such as HELLO, the operating system initially transfers control to an initialization function that s part of the compiler s runtime library. The initialization function eventually calls main to do the application s work.

Graphical applications in Windows work in much the same way except that the main program is named WinMain instead of main. WinMain operates a message pump to receive and dispatch messages to window procedures. It returns to the operating system when the user closes the main window. If the only Windows applications you ever build use Microsoft Foundation Classes (MFC), the WinMain procedure is buried in the library where you might never spot it, but rest assured it s there.

More than one application can appear to be running simultaneously on a computer, even a computer that has just one central processing unit. The operating system kernel contains a scheduler that gives short blocks of time, called time slices, to all the threads that are currently eligible to run. An application begins life with a single thread and can create more if it wants. Each thread has a priority, given to it by the system and subject to adjustment up and down for various reasons. At each decision point, the scheduler picks the highest-priority eligible thread and gives it control by loading a set of saved register images, including an instruction pointer, into the processor registers. A processor interrupt accompanies expiration of the thread s time slice. As part of handling the interrupt, the system saves the current register images, which can be restored the next time the system decides to redispatch the same thread.

Instead of just waiting for its time slice to expire, a thread can block each time it initiates a time-consuming activity in another thread until the activity finishes. This is better than spinning in a polling loop waiting for completion because it allows other threads to run sooner than they would if the system had to rely solely on expiration of a time slice to turn its attention to some other thread.

Now, I know you already knew what I just said. I just wanted to focus attention on the fact that an application is, at bottom, a selfish thread that grabs the CPU and tries to hold on until it exits and that the operating system scheduler acts like a playground monitor to make a bunch of selfish threads play well together.

Device Drivers

Like HELLO.EXE, a driver is also an executable file. It has the file extension .SYS, but structurally the disk file looks exactly like any 32-bit Windows or console-mode application. Also like HELLO.EXE, a driver uses a number of helper routines, many of which are dynamically linked from the operating system kernel or from a class driver or other supporting library. A driver file can have symbolic debugging information and resource data too.

The System Is in Charge

Unlike HELLO.EXE, however, a driver doesn t contain a main program. Instead, it contains a collection of subroutines that the system can call when the system thinks it s time to. To be sure, these subroutines can use helper subroutines in the driver, in static libraries, and in the operating system, but the driver isn t in charge of anything except its own hardware: the system is in charge of everything else, including the decisions about when to run your driver code.

Here s a brief snapshot of how the operating system might call subroutines in your driver:

  1. The user plugs in your device, so the system loads your driver executable into virtual memory and calls your DriverEntry routine. DriverEntry does a few things and returns.

  2. The Plug and Play Manager (PnP Manager) calls your AddDevice routine, which does a few things and returns.

  3. The PnP Manager sends you a few IRPs. Your dispatch function processes each IRP in turn and returns.

  4. An application opens a handle to your device, whereupon the system sends you another IRP. Your dispatch routine does a little work and returns.

  5. The application tries to read some data, whereupon the system sends you an IRP. Your dispatch routine puts the IRP in a queue and returns.

  6. A previous I/O operation finishes by signaling a hardware interrupt to which your driver is connected. Your interrupt routine does a little bit of work, schedules a DPC, and returns.

  7. Your DPC routine runs. Among other things, it removes the IRP you queued at step 5 and programs your hardware to read the data. Then the DPC routine returns to the system.

  8. Time passes, during which the system makes many other brief calls into your subroutines.

  9. Eventually, the end user unplugs your device. The PnP Manager sends you some IRPs, which you process and return. The operating system calls your DriverUnload routine, which usually just does a tiny amount of work and returns. Then the system removes your driver code from virtual memory.

At each step of this process, the system decided that your driver needed to do something, be it initializing, processing an IRP, handling an interrupt, or whatever. So the system selected the appropriate subroutine within your driver. Your routine did what it was supposed to do and returned to the system.

Threads and Driver Code

Another way in which drivers are dissimilar to applications is that the system doesn t create a special thread in which to run the driver code. Instead, a driver subroutine executes in the context of whatever thread happens to be currently active at the time the system decides to call that subroutine.

It s not possible to predict which thread will be current at the time a hardware interrupt occurs. As an analogy, imagine that you re watching a carousel at an amusement park. The horses on the carousel are like threads in a running system. Call the horse that s nearest to you the current horse. Now suppose you decide to take a picture with your camera the next time you overhear someone say the phrase, That s awesome, dude. (In my experience at amusement parks, this does not entail a long wait.) You wouldn t expect to be able to predict which horse would be current in your snapshot. Which of all the eligible threads that happens to be executing at the time of hardware interrupt is likewise not predictable. We call this an arbitrary thread, and we speak of running in an arbitrary thread context.

The system is often running in an arbitrary thread context when it decides to call a subroutine in your driver. The thread context would be arbitrary for example, when your interrupt service routine gets control. If you schedule a DPC, the thread in which your DPC routine runs will be arbitrary. If you queue IRPs, your StartIo routine will be called in an arbitrary thread. In fact, if some driver outside your own stack sends you an IRP, you have to assume that the thread context is arbitrary. Such would normally be the case for a storage driver since a file system driver will be the agent ultimately responsible for doing reads and writes.

The system doesn t always execute driver code in an arbitrary thread context. A driver can create its own system threads by calling PsCreateSystemThread. A driver can also ask the system to call it back in the context of a system thread by scheduling a work item. In these situations, we consider the thread context to be nonarbitrary. I ll discuss the mechanics of system threads and work items in Chapter 14.

Another situation in which the thread context is not arbitrary occurs when an application issues an API call that causes the I/O Manager to send an IRP directly to a driver. You can know when you write a driver whether or not this will be the case with respect to each type of IRP you handle.

You care whether the thread context is arbitrary for two reasons. First, a driver shouldn t block an arbitrary thread: it would be unfair to halt one thread while you carry out activities that benefit some other thread.

The second reason applies when a driver creates an IRP to send to some other driver. As I ll discuss more fully in Chapter 5, you need to create one kind of IRP (an asynchronous IRP) in an arbitrary thread, but you might create a different kind of IRP (a synchronous IRP) in a nonarbitrary thread. The I/O Manager ties the synchronous kind of IRP to the thread within which you create the IRP. It will cancel the IRP automatically if that thread terminates. The I/O Manager doesn t tie an asynchronous IRP to any particular thread, though. The thread in which you create an asynchronous IRP may have nothing to do with the I/O operation you re trying to perform, and it would be incorrect for the system to cancel the IRP just because that thread happens to terminate. So it doesn t.

Symmetric Multiprocessing

Windows XP uses a so-called symmetric model for managing computers with multiple central processors. In this model, each CPU is treated exactly like every other CPU with respect to thread scheduling. Each CPU has its own current thread. It s perfectly possible for the I/O Manager, executing in the context of the threads running on two or more CPUs, to call subroutines in your driver simultaneously. I m not talking about the sort of fake simultaneity with which threads execute on a single CPU on the time scale of the computer, the threads are really taking turns. Rather, on a multiprocessor machine, different threads really do execute at the same time. As you can imagine, simultaneous execution leads to exacting requirements for drivers to synchronize access to shared data. In Chapter 4, I ll discuss the various synchronization methods that you can use for this purpose.

Категории