[Previous] [Next]

System Threads

In all the device drivers considered so far in the book, we haven't been overly concerned about the thread context in which our driver subroutines have executed. Much of the time, our subroutines run in an arbitrary thread context, which means we can't block and can't directly access user-mode virtual memory. Some devices are very difficult to program when faced with the first of these constraints.

Some devices are best handled by polling. A device that can't asynchronously interrupt the CPU, for example, needs to be interrogated from time to time to check its state. In other cases, the natural way to program the device might be to perform an operation in steps with waits in between. A floppy disk driver, for example, goes through a series of steps to perform an operation. In general, the driver has to command the drive to spin up to speed, wait for the spin-up to occur, commence the transfer, wait a short while, and then spin the drive back down. You could design a driver that operates as a finite state machine to allow a callback function to properly sequence operations. It would be much easier, though, if you could just insert event and timer waits at the appropriate spots of a straight-line program.

Dealing with situations that require you to periodically interrogate a device is easy with the help of a system thread belonging to the driver. A system thread is a thread that operates within the overall umbrella of a process belonging to the operating system as a whole. I'll be talking exclusively about system threads that execute solely in kernel mode. In the next section, I'll describe the mechanism by which you create and destroy your own system threads. Then I'll give an example of how to use a system thread to manage a polled input device.

Creating and Terminating System Threads

To launch a system thread, you call PsCreateSystemThread. One of the arguments to this service function is the address of a thread procedure that acts as the main program for the new thread. When the thread procedure is going to terminate the thread, it calls PsTerminateSystemThread, which does not return. Generally speaking, you need to provide a way for a PnP event to tell the thread to terminate and to wait for the termination to occur. Combining all these factors, you'll end up with code that performs the functions of these three subroutines:











1 
2 



3 

4 





5 
6 
7 





8 

9 
typedef struct _DEVICE_EXTENSION {
  ...
  KEVENT evKill;
  PKTHREAD thread;
  };

NTSTATUS StartThread(PDEVICE_EXTENSION pdx)
  {
  NTSTATUS status;
  HANDLE hthread;
  KeInitializeEvent(&pdx->evKill, NotificationEvent, FALSE);
  status = PsCreateSystemThread(&hthread, THREAD_ALL_ACCESS,
    NULL, NULL, NULL, (PKSTART_ROUTINE) ThreadProc, pdx);
  if (!NT_SUCCESS(status))
    return status;
  ObReferenceObjectByHandle(hthread, THREAD_ALL_ACCESS, NULL,
    KernelMode, (PVOID*) &pdx->thread, NULL);
  ZwClose(hthread);
  return STATUS_SUCCESS;
  }

VOID StopThread(PDEVICE_EXTENSION pdx)
  {
  KeSetEvent(&pdx->evKill, 0, FALSE);
  KeWaitForSingleObject(pdx->thread, Executive, KernelMode, FALSE, NULL);
  ObDereferenceObject(pdx->thread);
  }

VOID ThreadProc(PDEVICE_EXTENSION pdx)
  {
  ...
  KeWaitForXxx(<at least pdx->evKill>);
  ...
  PsTerminateSystemThread(STATUS_SUCCESS);
  }

  1. Declare a KEVENT named evKill in the device extension to provide a way for a PnP event to signal the thread to terminate. This is the appropriate time to initialize the event.
  2. This statement launches the new thread. The return value for a successful call is a thread handle that appears at the location pointed to by the first argument. The second argument specifies the access rights you require to the thread; THREAD_ALL_ACCESS is the appropriate value to supply here. The next three arguments pertain to threads that are part of user-mode processes and should be NULL when a WDM driver calls this function. The next-to-last argument (ThreadProc) designates the main program for the thread. The last argument (pdx) is a context argument that will be the one and only argument to the thread procedure.
  3. To wait for the thread to terminate, you need the address of the underlying KTHREAD object instead of the handle you get back from PsCreateSystemThread. This call to ObReferenceObjectByHandle gives you that address.
  4. We don't actually need the handle once we have the address of the KTHREAD, so we call ZwClose to close that handle.
  5. A routine such as StopDevice—which performs the device-specific part of IRP_MN_STOP_DEVICE in my scheme of driver modularization—can call StopThread to halt the system thread. The first step is to set the evKill event.
  6. This call illustrates how to wait for the thread to finish. A kernel thread object is one of the dispatcher objects on which you can wait. It assumes the signalled state when the thread finally finishes. In Windows 2000, you always perform this wait to avoid the embarrassment of having your driver's image unmapped while one of your system threads executes the last few instructions of its shutdown processing. That is, don't just wait for a special "kill acknowledgment" event that the thread sets just before it exits—the thread has to execute PsTerminateSystemThread before your driver can safely unload. Refer also to an important Windows 98 compatibility note ("Waiting for System Threads to Finish") at the end of this chapter.
  7. This call to ObDereferenceObject balances the call to ObReferenceObjectByHandle that we made when we created the thread in the first place. It's necessary to allow the Object Manager to release the memory used by the KTHREAD object that formerly described our thread.
  8. The thread procedure will contain miscellaneous logic that depends on the exact goal you're trying to accomplish. If you block while waiting for some external event, you should call KeWaitForMultipleObjects and specify the evKill event as one of the objects.
  9. When you detect that evKill has been signalled, you call the PsTerminateSystemThread function, which terminates the thread. Consequently, it doesn't return. Note that you can't terminate a system thread except by calling this function in the context of the thread itself.

Using a System Thread for Device Polling

If you had to write a driver for a device that can't interrupt the CPU to demand service, a system thread devoted to polling the device may be the way to go. I'll show you one way to use a system thread for this purpose. This example is based on a hypothetical device with two input ports. One port acts as a control port; it delivers a 0 byte when no input data is ready and a 1 byte when input data is ready. The other port delivers a single byte of data and resets the control port.

In the sample I'll show you, we spawn the system thread when we process the IRP_MN_START_DEVICE request. We terminate the thread when we receive a Plug and Play request such as IRP_MN_STOP_DEVICE or IRP_MN_REMOVE_DEVICE that requires us to release our I/O resources. The thread spends most of its time blocked. When the StartIo routine begins to process an IRP_MJ_READ request, it sets an event that the polling thread has been waiting for. The polling thread then enters a loop to service the request. In the loop, the polling thread first blocks for a fixed polling interval. After the interval expires, the thread reads the control port. If the control port is 1, the thread reads a data byte. The thread then repeats the loop until the request is satisfied, whereupon it goes back to sleep until StartIo receives another request.

The thread routine in the POLLING sample is as follows:





1 

2 














3 

4 







5 



6 

7 







8 













9 
VOID PollingThreadRoutine(PDEVICE_EXTENSION pdx)
  {
  NTSTATUS status;
  KTIMER timer;
  KeInitializeTimerEx(&timer, SynchronizationTimer);

  PVOID mainevents[] = {
    (PVOID) &pdx->evKill,
    (PVOID) &pdx->evRequest,
    };

  PVOID pollevents[] = {
    (PVOID) &pdx->evKill,
    (PVOID) &timer,
    };

  ASSERT(arraysize(mainevents) <= THREAD_WAIT_OBJECTS);
  ASSERT(arraysize(pollevents) <= THREAD_WAIT_OBJECTS);

  BOOLEAN kill = FALSE;

  while (!kill)
    {    // until told to quit
    status = KeWaitForMultipleObjects(arraysize(mainevents),
      mainevents, WaitAny, Executive, KernelMode, FALSE,
      NULL, NULL);
    if (!NT_SUCCESS(status) || status == STATUS_WAIT_0)
      break;
    ULONG numxfer = 0;
    LARGE_INTEGER duetime = {0};
    #define POLLING_INTERVAL 500
    KeSetTimerEx(&timer, duetime, POLLING_INTERVAL, NULL);

    PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite);

    while (TRUE)
      {    // read next byte
      if (Irp->Cancel)
        {
        status = STATUS_CANCELLED;
        break;
        }
      status = AreRequestsBeingAborted(&pdx->dqReadWrite);
      if (!status)
        break;
      status = KeWaitForMultipleObjects(arraysize(pollevents),
        pollevents, WaitAny, Executive, KernelMode, FALSE,
        NULL, NULL);
      if (!NT_SUCCESS(status))
        {
        kill = TRUE;
        break;
        {
      if (status == STATUS_WAIT_0)
        {
        status = STATUS_DELETE_PENDING;
        kill = TRUE;
        break;
        }
      if (pdx->nbytes)
        {
        if (READ_PORT_UCHAR(pdx->portbase) == 1)
          {
          *pdx->buffer++ = READ_PORT_UCHAR(pdx->portbase + 1);
          --pdx->nbytes;
          ++numxfer;
          }
        }
      if (!pdx->nbytes)
        break;
      }    // read next byte
    KeCancelTimer(&timer);
    StartNextPacket(&pdx->dqReadWrite, pdx->DeviceObject);
    if (Irp)
      {
      IoReleaseRemoveLock(&pdx->RemoveLock, Irp);
      CompleteRequest(Irp, STATUS_SUCCESS, numxfer);
      }
    }    // until told to quit

  PsTerminateSystemThread(STATUS_SUCCESS);
  }

  1. We'll be using this kernel timer later to control the frequency with which we poll the device.
  2. We'll call KeWaitForMultipleObjects twice in this function to block the polling thread until something of note happens. These two arrays provide the addresses of the dispatcher objects on which we'll wait. The ASSERT statements verify that we're waiting for few enough events such that we can use the array of wait blocks that's built in to the thread object.
  3. This loop terminates when an error occurs or when evKill becomes signalled. We'll then terminate the entire polling thread.
  4. This wait terminates when either evKill or evRequest becomes signalled. Our StartIo routine will signal evRequest to indicate that an IRP exists for us to service.
  5. The call to KeSetTimerEx starts our timer counting. This is a repetitive timer that expires once based on the due time and periodically thereafter. We're specifying a 0 due time, which will cause us to poll the device immediately. The POLLING_INTERVAL is measured in milliseconds.
  6. This inner loop terminates when either the kill event becomes signalled or we're done with the current IRP.
  7. While we're going about our business in this loop, the current IRP might get cancelled, or we might receive a PnP or power IRP that requires us to abort this IRP.
  8. In this call to KeWaitForMultipleObjects, we take advantage of the fact that a kernel timer acts like an event object. The call finishes when either evKill is signalled (meaning we should terminate the polling thread altogether) or the timer expires (meaning we should execute another poll).
  9. This is the actual polling step in this driver. We read the control port, whose address is the base port address given to us by the PnP Manager. If the value indicates that data is available, we read the data port.

The StartIo routine that works with this polling routine first sets the buffer and nbytes fields in the device extension; you saw the polling routine use them to sequence through an input request. Then it sets the evRequest event to wake up the polling thread.

You can organize a polling driver in other ways besides the one I just showed you. For example, you could spawn a new polling thread each time an arriving request finds the device idle. The thread services requests until the device becomes idle, whereupon it terminates. This strategy would be better than the one I illustrated if long periods elapse between spurts of activity on the device, because the polling thread wouldn't be occupying virtual memory during the long intervals of quiescence. If, however, your device is more or less continuously busy, the first strategy might be better because it avoids repeating the overhead of starting and stopping the polling thread.