[Previous] [Next]

Executive Work Items

From time to time, you might wish that you could temporarily lower the processor's interrupt request level (IRQL) to carry out some task or another that must be done at PASSIVE_LEVEL. Lowering IRQL is, of course, a definite no-no. So long as you're running at or below DISPATCH_LEVEL, however, you can queue an executive work item to request a callback into your driver later. The callback occurs at PASSIVE_LEVEL in the context of a worker thread owned by the operating system. Using a work item can save you the trouble of creating your own thread that you only occasionally wake up.

I'll describe a simple way of using an executive work item. First declare a structure that starts with an unnamed instance of the WORK_QUEUE_ITEM structure. Here's an example drawn from the WORKITEM sample on the companion disc:

struct _RANDOM_JUNK {
  struct _WORK_QUEUE_ITEM;
  <other stuff>
  } RANDOM_JUNK, *PRANDOM_JUNK;

When you're ready, allocate an instance of this structure from the heap and initialize it:

PRANDOM_JUNK item = (PRANDOM_JUNK) ExAllocatePool(PagedPool,
  sizeof(RANDOM_JUNK);
ExInitializeWorkItem(item, (PWORKER_THREAD_ROUTINE) Callback,
  (PVOID) item);
<additional initialization>

In the call to ExInitializeWorkItem, the first argument (item) is the address of the WORK_QUEUE_ITEM embedded in your structure. ExInitializeWorkItem is actually a macro that simply references WORK_QUEUE_ITEM fields using this pointer; I didn't need to supply a cast here because I declared the WORK_QUEUE_ITEM as an unnamed structure member. The second argument (Callback) is the address of a callback routine elsewhere in your driver. The third and final argument is a context parameter that will eventually be used as the single argument to the callback routine. I used the item pointer here for reasons that will become apparent when I show you the callback routine. ExInitializeWorkItem merely initializes that part of your structure (that is, WORK_QUEUE_ITEM) that the system knows about. After calling ExInitializeWorkItem, you need to do any initialization of your own data members that might be required.

At this point, you're ready to ask the system to put your work item into a queue, which can be done using the ExQueueWorkItem function:

ExQueueWorkItem(item, QueueIdentifier);

QueueIdentifier can be either of these two values:

You choose the delayed or the critical work queue depending on the urgency of the task you're trying to perform. Putting your item into the critical work queue will give it priority over all noncritical work in the system at the possible cost of reducing the CPU time available for other critical work. In any case, the activities you perform in your callback can always be preempted by activities that run at an elevated IRQL.

After you queue the work item, the operating system will call you back in the context of a system worker thread having the characteristics you specified as the second argument to ExQueueWorkItem. You'll be at IRQL PASSIVE_LEVEL. What you do inside the callback routine is pretty much up to you except for one requirement: you must release or otherwise reclaim the memory occupied by the work queue item. Here's a skeleton for a work-item callback routine:

VOID Callback(PRANDOM_JUNK item)
  {
  PAGED_CODE();
  ...
  ExFreePool(item);
  }

This callback receives a single argument (item), which is the context parameter you supplied earlier in the call to ExInitializeWorkItem. This fragment also shows the call to ExFreePool that balances the allocation we did earlier. Since you must release the work item memory if you allocated it from the heap in the first place, it's often convenient to pass the work queue item address itself as the context parameter. That's what I did here, in fact, because the work queue item occupies the first several bytes of the RANDOM_JUNK structure.

I have one more important point to make about work items. You can't remove a work item from the system queue. If, however, you were to honor a PnP request to remove your device, it's possible (though pretty unlikely) for your driver to be removed from memory while a work item is still pending. The remove lock mechanism I described in Chapter 6, "Plug andPlay," gives you a perfect way to prevent this from happening, as follows:

In addition, your callback routine needs to take whatever steps are necessary to avoid accessing hardware that's been surprise-removed or depowered, and so on.

IoAllocateWorkItem, IoQueueWorkItem, and IoFreeItem

Windows 2000 provides a new set of functions—IoAllocateWorkItem, IoQueueWorkItem, and IoFreeItem—that Microsoft recommends you use instead of the executive support functions I just described. The new functions surround calls to the executive-level functions with code that claims a reference to a device object you specify. That reference prevents your device object from disappearing, but it doesn't hold off the processing of IRP_MN_REMOVE_DEVICE requests. So long as you understand that you must prevent the disappearance of your driver and any resources that your work-item callback will access until after the callback executes, there's no compelling reason to use the new functions.