Using Dispatcher Objects

< BACK  NEXT >
[oR]

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 Objects

An 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.
graphics/14fig01.gif

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.

Table 14.6. Functions that Operate on Event Objects
How to use event objects
If you want to THEN call IRQL
Create an event KeInitializeEvent PASSIVE_LEVEL
Create a named event IoCreateSynchronizationEvent PASSIVE_LEVEL
IoCreateNotificationEvent
Modify event state KeSetEvent <= DISPATCH LEVEL
KeClearEvent
KeResetEvent
Wait for a timer KeWaitForSingleObject PASSIVE_LEVEL
KeWaitForMultipleObjects
Interrogate an event KeReadStateEvent <= DISPATCH_LEVEL

Sharing Events Between Drivers

It 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:

  1. Call ObReferenceObjectByHandle. This function obtains a pointer to the Event object itself and increments the object's pointer reference count.

  2. When the handle itself is no longer needed (and it is usually not needed at all), call ZwClose to release it. This function decrements the object's handle reference count.

  3. When the Event object is no longer needed, call ObDereferenceObject to decrement the Event object's pointer reference count and possibly delete the Event object.

These functions can be called only from PASSIVE_LEVEL IRQL, which limits where a driver can use them.

Mutex Objects

A 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.
graphics/14fig02.gif

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.

Table 14.7. Functions that Operate on Mutex Objects
How to use mutex objects
If you want to THEN call IRQL
Create a Mutex KeIntializeMutex PASSIVE_LEVEL
Request Mutex ownership KeWaitForSingleObject PASSIVE_LEVEL
KeWaitForMultipleObjects
Give up Mutex ownership KeReleaseMutex PASSIVE_LEVEL
Interrogate Mutex KeReadStateMutex <= DISPATCH_LEVEL

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 Objects

A 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.
graphics/14fig03.gif

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.

Table 14.8. Functions that Operate on Semaphore Objects
How to use semaphore objects
If you want to THEN call IRQL
Create a semaphore KeIntializeSemaphore PASSIVE_LEVEL
Decrement semaphore KeWaitForSingleObject PASSIVE_LEVEL
KeWaitForMultipleObjects
Increment semaphore KeReleaseSemaphore <= DISPATCH_LEVEL
Interrogate semaphore KeReadStateSemaphore Any

Timer Objects

A 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.
graphics/14fig04.gif

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.

Table 14.9. Functions that Operate on Timer Objects
How to use timer objects
If you want to THEN call IRQL
Create a Timer KeIntializeTimerX PASSIVE_LEVEL
Start a one-shot Timer KeSetTimer <= DISPATCH_LEVEL
Start a repeating Timer KeSetTimerEx <= DISPATCH_LEVEL
Stop a Timer KeCancelTimer <= DISPATCH_LEVEL
Wait for a Timer KeWaitForSingleObject PASSIVE_LEVEL
KeWaitForMultipleObjects
Interrogate a Timer KeReadTimerState <= DISPATCH_LEVEL

Thread Objects

System 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:

  1. Call ObReferenceObjectByHandle. This function provides a pointer to the Thread object itself and increments the object's pointer reference count.

  2. When the handle itself is no longer needed (and it is usually not needed at all), call ZwClose to release it. This decrements the object's handle reference count.

  3. After the thread terminates, call ObDereferenceObject to decrement the Thread object's pointer reference count and possibly delete the Thread object.

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 Mutex

The 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 Mutexes

A 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.

Table 14.10. Functions that Operate on Fast Mutexes
How to use Fast Mutexes
If you want to THEN call IRQL
Create a Fast Mutex ExInitializeFastMutex <= DISPATCH_LEVEL
Request Fast Mutex ownership ExAcquireFastMutex < DISPATCH_LEVEL
Give up Fast Mutex ownership ExReleaseFastMutex < DISPATCH_LEVEL

Executive Resources

Another 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.

Table 14.11. Functions that Operate on Executive Resources
How to use Executive resources
If you want to THEN call IRQL
Create ExInitializeResourceLite <= DISPATCH_LEVEL
Acquire ExAcquireResourceExclusiveLite < DISPATCH_LEVEL
ExAcquirResourceSharedLite < DISPATCH_LEVEL
ExTryToAcquireResourceExclusiveLite < DISPATCH_LEVEL
ExConvertExclusiveToSharedLite < DISPATCH_LEVEL
Release ExReleaseResourceforThreadLite <= DISPATCH_LEVEL
Interrogate ExIsResourceAcquiredSharedLite <=DISPATCH_LEVEL
ExIsResourceAcquiredExclusiveLite <= DISPATCH_LEVEL
Delete ExDeleteResourceLite <=DISPATCH_LEVEL

Synchronization Deadlocks

Deadlock 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.
graphics/14fig05.gif
  1. Thread A acquires resource X.

  2. Thread B acquires resource Y.

  3. Thread A requests ownership of resource Y and goes into a wait state until B releases Y.

  4. Thread B then requests ownership of resource X. This causes B to go into a wait state until A releases X. The result is a deadlock, or deadly embrace.

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.

  • Use the Timeout arguments of the KeWaitForXxx functions to limit the time of the wait. While this technique may help detect a deadlock, it does not really correct the underlying problem.

  • Force all the threads using a given set of resources to acquire them in the same order. In the previous example, if A and B had both gone after resource X first and then Y second, there would have been no deadlock.

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.

< BACK  NEXT >


The Windows 2000 Device Driver Book(c) A Guide for Programmers
The Windows 2000 Device Driver Book: A Guide for Programmers (2nd Edition)
ISBN: 0130204315
EAN: 2147483647
Year: 2000
Pages: 156

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net