Many kinds of drivers form a complete Windows 2000 system. Figure 1-4 diagrams several of them.
Figure 1-4. Types of device drivers in Windows 2000.
Not all the distinctions implied by this classification scheme are important all of the time. As I remarked in my previous book, Systems Programming for Windows 95 (Microsoft Press, 1996), you have not stumbled into a nest of pedants by buying my book. In particular, I'm not always going to carefully distinguish between WDM drivers and PnP drivers in the rigorous way implied by the preceding taxonomy. The distinction is a phenomenological one based on whether a given driver runs both in Windows 2000 and Windows 98. Without necessarily using the technically exact term, I'll be very careful to discuss system dependencies when they come up hereafter.
Kernel-mode drivers share a number of general attributes, as suggested by the list of attributes (drawn from the introductory chapters of the Windows 2000 Device Driver Kit) that I describe in the following sections. (Note that throughout this book, I'll often refer to just the "DDK," meaning the Windows 2000 DDK. If I need to discuss another DDK, I'll give its specific name.)
Kernel-mode drivers should be source-portable across all Windows NT platforms. WDM drivers are, by definition, source-portable between Windows 98 and Windows 2000 as well. To achieve portability, you should write your driver entirely in C, using language elements specified by the ANSI C standard. You should avoid using implementation-defined or vendor-specific features of the language, and you should avoid using run-time library functions that aren't already exported by the operating system kernel (concerning which, see Chapter 3). If you can't avoid platform dependencies in your code, you should isolate them with conditional compilation directives. If you follow all of these guidelines, you'll be able to recompile and relink your source code to produce a driver that will "just work" on any new Windows NT platform.
In many cases, it will be possible to achieve binary compatibility for a WDM driver between Windows 98 and the 32-bit Intel x86 Windows 2000 operating system. You achieve source compatibility merely by restricting yourself to using the subset of kernel-mode support functions declared in WDM.H. There are some areas in which the two operating systems behave differently in a way that matters to a device driver, however, and I'll discuss these areas in various parts of the book.
A kernel-mode driver should avoid hard-coded assumptions about device characteristics or system settings that can differ from one platform to another. It's easiest to illustrate this abstract and lofty goal with a couple of examples. On an x86-based PC, a standard serial port uses a particular interrupt request line and set of eight I/O ports whose numeric values haven't changed in over 20 years. Hard-coding these values into a driver makes it not configurable. In Chapter 8, I'll discuss two power management features—idle detection and system wake-up—that an end user should be able to control; a driver that always uses particular idle timeout constants or that always arms its device's wake-up feature would not allow for that kind of control. The driver would therefore not be configurable in the sense we're discussing.
Achieving configurability requires, first of all, that you avoid coding direct references to hardware, even within platform-specific conditional compilation blocks. Call on the facilities of the HAL or of a lower-level bus driver instead. You can also implement a standard or custom control interface to allow control-panel applications to communicate end user wishes. Better yet, you can support Web-Based Enterprise Management (WBEM) controls that allow users and administrators to configure hardware features in a distributed enterprise environment. (See Chapter 10.) Finally, you can use the registry database as a repository for configuration information that ought to persist from one session to the next.
Windows 2000 and Windows 98 are multitasking operating systems that apportion use of a CPU among an arbitrary number of threads. Much of the time, driver subroutines execute in an environment in which they can be preempted to allow another thread to execute on the same CPU. Thread preemption depends on a thread priority scheme and on using the system clock to allocate CPU time in slices to threads having the same priority.
Windows 2000 also incorporates an interrupt prioritization concept known as interrupt request level (IRQL). I'll discuss IRQL in detail in Chapter 4, but the following summary will be useful for now. You can think of a CPU as having an IRQL register that records the level at which the CPU is currently executing. Three IRQL values have major significance for device drivers: PASSIVE_LEVEL (numerically equal to 0), DISPATCH_LEVEL (numerically equal to 2), and the so-called device IRQL (or DIRQL, numerically equal to a value higher than 2) at which a particular device's interrupt service routine executes. Most of the time, a CPU executes at PASSIVE_LEVEL. All user-mode code runs at PASSIVE_LEVEL, and many of the activities a driver performs also occur at PASSIVE_LEVEL. While a CPU is at PASSIVE_LEVEL, the current thread can be preempted by any other thread that has a higher thread priority or by expiration of its own time slice. Once a CPU's IRQL is above PASSIVE_LEVEL, however, thread preemption no longer occurs. The CPU executes in the context of whatever thread was current when the IRQL was most recently raised above PASSIVE_LEVEL.
You can think of the IRQ levels above PASSIVE_LEVEL as a priority scheme for interrupts. This is a different sort of priority than that which governs thread preemption because, as I just remarked, no thread preemption occurs above PASSIVE_LEVEL. But an activity running at any IRQL can be interrupted to perform an activity at a higher IRQL. Consequently, a driver must anticipate that it might lose control at any moment while the system performs some more essential task.
Windows 2000 can run on computers with one or more than one CPU. Windows 2000 uses a symmetric multiprocessor model, in which all CPUs are considered equal. System tasks and user-mode programs can execute on any CPU, and all CPUs have equal access to memory. The existence of multiple CPUs poses a difficult synchronization problem for device drivers because code executing on two or more CPUs might simultaneously need to access shared data or shared hardware resources. The Windows 2000 kernel provides a synchronization object called a spin lock that drivers can use to avoid destructive interference in such situations. (See Chapter 4.)
The Windows 2000 kernel is object-based in the sense that many of the data structures used by device drivers and kernel routines have common features that a centralized Object Manager component controls. These features include names, reference counts, security attributes, and so on. Internally, the kernel contains method routines for performing common object management tasks such as opening and closing objects or parsing object names.
Kernel components export service routines that drivers use to manipulate certain kinds of object or certain fields within objects. Some kernel objects—the kernel interrupt object, for example—are completely opaque in that the DDK headers don't declare the members of the data structure. Other kernel objects—such as the device object or the driver object—are partially opaque: the DDK headers declare all the members of the structure, but documentation describes only certain accessible members and cautions driver writers not to access or modify other members directly. Support routines exist to access and modify those opaque fields that must be indirectly available to drivers. Partially opaque objects are analogous to C++ classes, which can have public members accessible to anyone and private or protected members accessible only via method functions.
The I/O Manager and device drivers use the I/O request packet to manage the details of I/O operations. Some kernel-mode component creates an IRP to perform an operation on a device or to send an instruction or query to a driver. The I/O Manager sends the IRP to one or more of the subroutines that a driver exports. Generally, each driver subroutine performs a discrete amount of work on the IRP and returns back to the I/O Manager. Eventually, some driver subroutine completes the IRP, whereupon the I/O Manager destroys the IRP and reports the ending status back to the originator of the request.
Windows 2000 allows applications and drivers to initiate operations and continue processing while the operations progress. Consequently, drivers ordinarily process time-consuming operations in an asynchronous way. That is, a driver accepts an IRP, initializes whatever state information it requires to manage the operation, and then returns to its caller after arranging for the IRP to be performed and completed in the future. The caller can then decide whether or not to wait for the IRP to finish.
As a multitasking operating system, Windows 2000 schedules threads for execution on the available processors according to eligibility and priority. The asynchronous operations a driver needs to perform for handling an I/O request often occur in the context of some unpredictable thread, the identification of which can differ from one invocation of the driver's asynchronous processing routines to the next. We use the term arbitrary thread context to describe the situation in which a driver doesn't know (or care) which thread happens to be current as it performs its work. Drivers should avoid blocking arbitrary threads, and this stricture generally results in a driver architecture that responds to hardware events by performing discrete operations and then returning.
In the Windows Driver Model, each hardware device has at least two device drivers. One of these drivers, which we call the function driver, is what you've always thought of as being "the" device driver. It understands all the details about how to make the hardware work. It's responsible for initiating I/O operations, for handling the interrupts that occur when those operations finish, and for providing a way for the end user to exercise whatever control over the device might be appropriate.
We call the other of the two drivers that every device has the bus driver. It's responsible for managing the connection between the hardware and the computer. For example, the bus driver for the PCI (Peripheral Component Interconnect) bus is the software component that actually detects that your card is plugged in to a PCI slot and determines what requirements your card has for I/O-mapped or memory-mapped connections with the host. It's also the software that turns the flow of electrical current to your card's slot on or off.
Some devices have more than two drivers. We use the generic term filter driver to describe these other drivers. Some filter drivers simply watch as the function driver performs I/O. More often, a software or hardware vendor supplies a filter driver to modify the behavior of an existing function driver in some way. "Upper" filter drivers see IRPs before the function driver, and they have the chance to support additional features that the function driver doesn't know about. Sometimes an upper filter can perform a workaround for a bug or other deficiency in the function driver or the hardware. "Lower" filter drivers see IRPs that the function driver is trying to send to the bus driver. In some cases, such as when the device is attached to a universal serial bus (USB), a lower filter can modify the stream of bus operations that the function driver is trying to perform.
A WDM function driver is often composed of two separate executable files. One file, the class driver, understands how to handle all of the WDM protocols that the operating system uses (and some of them can be very complicated) and how to manage the basic features of an entire class of devices. A class driver for the class of USB cameras is one example. The other file, called the minidriver, contains functions that the class driver uses to manage the vendor-specific features of a particular instance of that class. The combination of class plus minidriver adds up to a complete function driver.
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 on an IRP. Figure 1-5 illustrates this concept. Some routines, such as the DriverEntry and AddDevice routines, as well as dispatch functions for a few types of IRP, will be present in every such container. Drivers that need to queue requests—and most do—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—again, most do—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 required. 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 1-5. Contents of a WDM driver executable "package."