A thread is a unit of execution. Each thread maintains an independent program counter and hardware context that includes a private set of CPU registers. Each thread maintains a priority value that determines when it gains control of the system processor(s). In general, the higher a thread's priority, the more likely it is to receive control. Threads can operate in user mode or kernel mode. A system thread is one that runs exclusively in kernel mode. It has no user-mode context and cannot access user address space. Just like a Win32 thread, a system thread executes at or below APC_LEVEL IRQL and it competes for use of the CPU based on its scheduling priority. When To Use ThreadsThere are several reasons to use threads in a driver. One example is working with hardware that has the following characteristics:
Such a device could be managed using a CustomTimerDpc routine. Depending on the amount of device activity, however, this approach could saturate the DPC queues and slow down other drivers. Threads, on the other hand, run at PASSIVE_LEVEL and do not interfere with DPC routines. Fortunately, most modern hardware is designed to operate in a way that allows good system performance. (Granted, there are many contradictions to this statement.) Legacy hardware, however, is legendary in forcing drivers to poll (and retry) for state transitions, thus burdening the driver with considerable and messy design. The most notable examples are floppy disks and other devices attached to floppy controllers. Another need for threads occurs with devices that take excessive time to initialize. The driver must monitor the initialization process, polling the state transitions throughout. A separate thread is needed because the Service Control Manager gives drivers only about 30 seconds to execute their DriverEntry routine. Otherwise, the Service Control Manager forcibly unloads the driver. The only solution is to put the long-running device start-up code in a separate thread and return promptly from the DriverEntry routine with STATUS_SUCCESS. Finally, there may be some operations that can be performed only at PASSIVE_LEVEL IRQL. For example, if a driver has to access the registry on a regular basis, or perform file operations, a multi-thread design should be considered. Creating and Terminating System ThreadsTo create a system thread, a driver uses PsCreateSystemThread, described in Table 14.1. Since this function can only be called at PASSIVE_LEVEL IRQL, threads are usually created in the DriverEntry or AddDevice routines. When a driver unloads, it must ensure that any system thread it may have created has terminated. System threads must terminate themselves, using PsTerminateSystemThread, described in Table 14.2. Unlike Win32 user-mode threads, there is no way to forcibly terminate a system thread. This means that some kind of signaling mechanism needs to be set up to let a thread know it should exit. As discussed later in this chapter, Event objects provide a convenient mechanism for this.
Managing Thread PriorityIn general, system threads running in a driver should have priorities near the low end of the real-time range. The following code fragment demonstrates this: VOID ThreadStartRoutine( PVOID pContext ) { ... KeSetPriorityThread( KeGetCurrentThread(), LOW_REALTIME_PRIORITY ); ... } Be aware that real-time threads have no quantum timeout value. This means that the CPU is relinquished only when the thread voluntarily enters a wait state, or when preempted by a thread of higher priority. Therefore, drivers cannot depend upon round-robin scheduling. System Worker ThreadsFor occasional, quick operations at PASSIVE_LEVEL IRQL, creating and terminating a separate thread may not be very efficient. The alternative is to have one of the kernel's system worker threads perform the task. These threads use a callback mechanism to do work on behalf of any driver. It is not difficult to use system worker threads. First, allocate storage for a WORK_QUEUE_ITEM structure. The system will use this block to keep track of the work request. Next, call ExInitializeWorkItem to associate a callback function in the driver with the WORK_QUEUE_ITEM. Later, when a system thread is needed to execute the callback function, call ExQueueWorkItem to insert the request block into one of the system work queues. The request can be executed either by a worker thread with a real-time priority or by one with a variable priority. Keep in mind that all drivers are sharing the same group of system worker threads. Requests that take a very long time to complete may delay the execution of requests from other drivers. Tasks involving lengthy operations or long time delays should utilize a private driver thread rather than the system work queues.
|