Except for Thread objects themselves, the driver must allocate storage for any dispatcher objects that are used. The objects must be permanently resident and are, therefore, usually allocated within the Device or Controller Extension. In any case, they must be in nonpaged memory. Also, the dispatch object must be initialized once with the proper KeInitializeXxx function before it is used. Since the initialization functions can only be called at PASSIVE_LEVEL IRQL, dispatcher objects are usually prepared in the DriverEntry or AddDevice routine. The following sections describe each category of dispatcher objects in greater detail. Event ObjectsAn event is a dispatcher object that must be explicitly set to the signaled or nonsignaled state. An event is analogous to a binary flag, allowing one thread to signal other threads of a specific occurrence by raising (set to signaled) the flag. This behavior can be seen in Figure 14.1, where thread A awakens B, C, and D by setting an event object. Figure 14.1. Event objects synchronize system threads.These objects actually come in two different flavors: notification events and synchronization events. The type is chosen when the object is initialized. These two types of events exhibit different behavior when put into the signaled state. As long as a notification event remains signaled, all threads waiting for the event come out of their wait state. A notification event must be explicitly reset to put it into the nonsignaled state. These events exhibit behavior like user-mode (Win32) manual-reset events. When a synchronization event is placed into the signaled state, it remains set only long enough for one call to KeWaitForXxx. It then resets itself to the nonsignaled state automatically. In other words, the gate stays open until exactly one thread passes through, and then it shuts. This is equivalent to a user-mode auto-reset event. To use an event, storage is first allocated for an item of type KEVENT, and then functions listed in Table 14.6 are called. Notice that either of two functions put an event object into the nonsignaled state. The difference is that KeResetEvent returns the state of the event before it became nonsignaled, and KeClearEvent does not. KeClearEvent is somewhat faster, so it should be used unless the previous state must be determined. The sample driver at the end of this chapter provides an example of using events. It has a worker thread that needs to pause until an interrupt arrives, so the thread waits for an event object. The driver's DpcForIsr routine sets the event into the signaled state, waking up the worker thread.
Sharing Events Between DriversIt is difficult for two unrelated drivers to share an Event object created with KeInitializeEvent. The event object is referenced only by pointer, and without some kind of explicit agreement (for example, an internal IOCTL), there is no simple way to pass a pointer from one driver to another. Even then, there is the issue of ensuring that the driver creating the Event stays loaded while another driver uses the object. The IoCreateSynchronizationEvent and IoCreateNotificationEvent functions allow the creation of named Event objects. As long as two drivers use the same Event name, they can each obtain pointers to the same Event object. Both functions behave like the Win32 CreateEvent system call. In other words, the first driver to make a call with a specific Event name causes the Event object to be created. Subsequent calls attempting to create a duplicate Event object simply return a handle to the existing Event object. There are two notable behaviors of the IoCreateXxxEvent functions. First, memory for the KEVENT object is not allocated by the driver. Storage is supplied by the system. When the last user of the Event releases it, the system deletes the object automatically. Second, the IoCreateXxxEvent calls return a handle to the event object, not a memory pointer. To use the Event object in calls to the KeXxx functions listed in Table 14.6, a pointer is required. To convert a handle into an object pointer, the following steps must be performed:
These functions can be called only from PASSIVE_LEVEL IRQL, which limits where a driver can use them. Mutex ObjectsA Mutex (short for mutual exclusion) is a dispatcher object that can be owned by only one thread at a time. The object becomes nonsignaled when a thread owns it and signaled when it is available (unowned). Mutexes provide an easy mechanism for coordinating mutually exclusive access to some shared resource, usually memory. Figure 14.2 shows threads B, C, and D waiting for a Mutex owned by thread A. When A releases the Mutex, one of the waiting threads wakes up and becomes its new owner. Figure 14.2. Mutex objects synchronize system threads.To use a Mutex, nonpaged storage for an item of type KMUTEX must be reserved. Functions listed in Table 14.7 can then be used. Be aware that when a Mutex is initialized, it is always set to the signaled state.
If a thread calls KeWaitForXxx on a Mutex it already owns, the thread never waits. Instead, the Mutex increments an internal counter to record the fact that this thread is making recursive ownership requests. When the thread wants to free the Mutex, it has to call KeReleaseMutex as many times as it requested ownership. Only then will the Mutex go into the signaled state. This is the same behavior exhibited by Win32 Mutex objects. It is also crucial that a driver release any Mutexes it might be holding before it makes a transition back into user mode. The kernel will bug-check if any driver threads attempt to return control to the I/O Manager while owning a Mutex. For example, a DriverEntry or Dispatch routine is not allowed to acquire a Mutex that would later be released by some other Dispatch routine or by a system thread. Semaphore ObjectsA Semaphore is a dispatcher object that maintains a count. The object remains signaled as long as its count is greater than zero, and nonsignaled when the count is 0. In other words, a Semaphore is a counting Mutex. Figure 14.3 shows the operation of the Semaphore. Threads B, C, and D are all waiting for a Semaphore whose current count is 0. When thread A calls KeReleaseSemaphore twice, the count increments to 2, and two of the waiting threads are allowed to resume execution. Waking up two threads also causes the Semaphore to decrement back to zero. Figure 14.3. Semaphore objects synchronize system threads.Again, the sample driver at the end of this chapter provides a good example. Its Dispatch routine increments a Semaphore each time it adds an IRP to an internal work queue. As a worker thread removes IRPs from the queue, it decrements the Semaphore and finally goes into a wait state when the queue is empty. To use the Semaphore, storage must be allocated for an item of type KSEMAPHORE. Then the functions listed in Table 14.8 can be used.
Timer ObjectsA Timer is a dispatcher object with a timeout value. When a Timer is started, it goes into the nonsignaled state until its timeout value expires. At that point, it becomes signaled. In chapter 10, a Timer object is used to force a CustomTimerDpc routine to execute. Since they are just kernel dispatcher objects, they can also be used in calls to KeWaitForXxx. Figure 14.4 illustrates the operation of the Timer object. Thread A starts the Timer and then calls KeWaitForSingleObject. The thread blocks until the Timer expires. At that point, the timer goes into the signaled state and the thread wakes up. Figure 14.4. Timer Objects synchronize system threads.Timer objects actually come in two different flavors: Notification Timers and Synchronization Timers. The type is chosen when the object is initialized. Although both types of Timers go into the signaled state when their timeout value expires, the period that the object remains signaled differs. When a Notification Timer times out, it remains in the signaled state until it is explicitly reset. While the Timer is signaled, all threads waiting for the Timer are awakened. When a Synchronization Timer expires, it remains in the Signaled state only long enough to satisfy a single KeWaitForXxx request. At that point, the Timer becomes nonsignaled automatically. To use a Timer, storage must be allocated for an item of type KTIMER and then the functions listed in Table 14.9 can be used.
Thread ObjectsSystem threads are also dispatcher objects, which means they have a signaled state. When a system thread terminates, its Thread object changes from the nonsignaled to the signaled state. This allows a driver to synchronize its cleanup operations by waiting for the Thread object. Notably, when PsCreateSystemThread is called, it returns a handle to the Thread object. To use a Thread object in a call to KeWaitForXxx, a pointer to the object is required rather than a handle. To convert a handle into an object pointer, the following steps must be performed:
These functions can be called only from PASSIVE_LEVEL IRQL, which limits the places in a driver where they can be used. Variations on the MutexThe Windows 2000 Executive supports two variations on Mutex objects. The following sections describe them briefly. In general, using these objects instead of kernel Mutexes can result in better driver performance. See the NT DDK documentation for more complete information. Fast MutexesA Fast Mutex is a synchronization object that acts like a kernel Mutex, except that it does not allow recursive ownership requests. By removing this feature, the Fast Mutex does not have to do as much work, and its speed improves. The Fast Mutex itself is an object of type FAST_MUTEX that is associated with one or more data items needing protection. Any code touching the data items must acquire ownership of the corresponding FAST_MUTEX first. Use the functions listed in Table 14.10 to work with Fast Mutexes. Notice that these objects have their own functions for requesting ownership. The KeWaitForXxx functions cannot be used to acquire Fast Mutexes.
Executive ResourcesAnother synchronization object that behaves very much like a kernel Mutex is an Executive resource. The main difference is the resource can either be owned exclusively by a single thread, or shared by multiple threads for read access. Since it is common (in the real world) for multiple readers to request simultaneous access to a resource, Executive Resource objects provide better throughput than standard kernel Mutexes. The Executive Resource itself is just an object of type ERESOURCE that is associated with one or more data items needing protection. Any code planning to touch the data items has to acquire ownership of the corresponding ERESOURCE first. Table 14.11 lists the functions that work with Executive Resources. Notice that these objects have their own functions for requesting ownership. The KeWaitForXxx functions cannot be used to acquire Executive Resources.
Synchronization DeadlocksDeadlock situations can occur whenever multiple threads compete for simultaneous ownership of multiple resources. Figure 14.5 shows the simplest form of this problem: Figure 14.5. Deadlock scenario.
A deadlock can occur using Events, Mutexes, or Semaphores. Even Thread objects can deadlock waiting for each other to terminate. There are two general approaches to solving deadlock problems.
Mutex objects provide some protection against the deadlocks through the use of level numbers. When a Mutex is initialized, a level number is assigned. Later, when a thread attempts to acquire the Mutex, the kernel will not grant ownership if that thread is holding any Mutex with a lower level number. By enforcing this policy, the kernel avoids deadlocks involving multiple Mutexes.
|