为工作项排队:一次就足够

http://hi.baidu.com/guodxu/blog/item/5a374439d0af02f8b311c700.html

 

工作项是一个类型为 IO_WORKITEM 的结构(与工作例程和上下文区域关联)。驱动程序将一个工作项添加到系统工作队列;系统工作线程在以后移除该项并以 PASSIVE_LEVEL 运行关联的例程。但是,为已经在工作队列中的工作项排队将会破坏队列并导致驱动程序中出现很难检测的问题。

 

为工作项排队会怎样破坏工作队列?

  在内部,系统在一个双向链表中维护排队的工作项。当您调用 IoQueueWorkItem(或 ExQueueWorkItem)给队列添加一个项时,系统会检查链接。在可调式版本 (checked build) 中,如果项已经被链接到链表中,系统会发出断言。但是,在自由版本 (free build) 中,项被添加到链表,函数返回一个成功状态,并且链表被破坏。在以后的某些时候,破坏的链表可以导致严重和不可预期的问题。(在 Windows Vista 和 Windows Server 2008 中,试图对工作项进行第二次排队将导致错误检查 0xE4, WORKER_INVALID。)

  如果您熟悉延迟过程调用 (DPC) 队列,那么这种行为可能会让您感到惊讶,因为您可能期望两种类型的队列的操作方式相同。但是,DPC 队列的操作与工作队列不同。如果您试图为一个已经在 DPC 队列中的 DPC 排队,那么 KeInsertQueueDpc 会返回 FALSE。它不会对 DPC 进行第二次排队,并且 DPC 只运行一次。

 

什么时候会发生这种问题?

  如果驱动程序在给定的驱动程序例程每次运行时都对相同的工作项排队,那么它可能会在工作项仍然在队列中的时候对其再次排队。最可能发生的场景包括从以 DISPATCH_LEVEL 运行的例程对工作项排队,但是也可能在以 PASSIVE_LEVEL 排队的工作项上发生。

  例如,DPC 有时会在短时间(在驱动程序处理一系列中断期间)内被重复调用。DPC 以 DISPATCH_LEVEL 运行,通常对工作项排队以在以后以 PASSIVE_LEVEL 执行任务。但是,在单处理器机器上,只要队列中有任何 DPC 在等待以 DISPATCH_LEVEL 运行(或者只要有任何其他工作保持在 DISPATCH_LEVEL),系统就不会将 IRQL 降低到 PASSIVE_LEVEL。因此,如果 DPC 已经被第二次排队,那么系统工作线程将不会有机会运行工作项。在多处理器系统上,此问题出现的可能性比较低(虽然仍然可能发生)。只要 DPC 队列不为空,系统就会调度 DPC 来在下一个可用的处理器上运行。因此,不管在单处理器还是多处理器硬件上,DPC 都可以在系统工作线程运行之前多次运行。如果这样的话,原始的工作项仍然在队列中,并且在 DPC 试图再次排队时队列被破坏。

  要运行一个工作项,系统将其从队列中移除并重置链接。工作项被从队列中移除之后,您可以再次对其安全地排队。请记住,如果您第二次成功地对工作项排队,那么它可以在另一个处理器上或者在单处理器上的不同线程上下文中并发运行。您必须保护工作项例程使用的任何共享的、可写的数据。

 

在您的驱动程序中防止工作项队列破坏。

  因为这种错误依赖于计时、硬件活动和瞬态条件,所以在驱动程序中检测它会很困难。一个线索是由无效地址引起的错误检查,该无效地址在取消对工作项排队的函数位于堆栈上并且工作例程已经释放工作项时产生。但是,最好的方法是对您的驱动程序进行编码来防止这种问题。

  要避免破坏队列,驱动程序应该留意工作项是否在工作队列中。但是,驱动程序不能使用事件或相似的同步机制来确定工作例程何时开始,因为以 DISPATCH_LEVEL 运行的排队例程不能等待一个非零时期。也不能在循环中执行一个长度为零的等待。在单处理器系统上,长度为零的等待会导致死锁,因为工作线程只有在排队例程以 DISPATCH_LEVEL 运行完成后才能以 PASSIVE_LEVEL 运行。

  相反,驱动程序应该使用如下技术:

 
  • 驱动程序维护工作线程的一个工作列表。
 
  • 工作例程在每次运行时执行列表中的所有工作。

 

  • 当新工作到达时,驱动程序只在列表为空时才对工作项排队。

 

  • 如果排队例程需要工作线程已经处理的数据,那么驱动程序可以在每次对工作项排队时创建一个上下文或通过使用循环缓冲区或类似技术来维护更改历史记录。

  Windows DDK 的 SMB 电池示例展示了如何实现这种技术。设备发出警报来响应某些情况(例如电量不足),并且驱动程序维护一个挂起的警报的列表。驱动程序每次被通知警报时,SmbBattAlarm 函数都会添加一个记录到列表中。工作例程 (SmbBattWorkerThread) 从列表中读取警报记录并依次处理它们,直到列表为空。驱动程序并不会在每次被通知警报时对工作项排队。而只在新警报到达时列表为空的情况下才对工作项排队。

  下面的函数摘录自这个示例。随 Windows DDK 安装的完整示例位于 src/wdm/acpi/smbbatt/smbbatt.c。

VOID SmbBattAlarm ( IN PVOID    Context, IN UCHAR    Address, IN USHORT   Data )

 {

   PSMB_ALARM_ENTRY        newAlarmEntry;

  ULONG                   compState;
PSMB_BATT_SUBSYSTEM     subsystemExt  = (PSMB_BATT_SUBSYSTEM) Context;
// // Allocate a new list structure from non-paged pool. //
newAlarmEntry = ExAllocatePoolWithTag (NonPagedPool, sizeof (SMB_ALARM_ENTRY), 'StaB');

  if (!newAlarmEntry)

  {

     BattPrint (BAT_ERROR, ("SmbBattAlarm:couldn't" "allocate alarm structure/n"));

    return;

  }
newAlarmEntry->Data     = Data;

  newAlarmEntry->Address  = Address;

// // Add this alarm to the alarm list //
ExInterlockedInsertTailList( &subsystemExt->AlarmList, &newAlarmEntry->Alarms, &subsystemExt->AlarmListLock );

// // Add 1 to the WorkerActive value.

  // If this is the first alarm queue a work item. //
if (InterlockedIncrement (&subsystemExt->WorkerActive) == 1)

  {

     IoQueueWorkItem (subsystemExt->WorkerThread, SmbBattWorkerThread, DelayedWorkQueue, subsystemExt);

  }
BattPrint(BAT_TRACE, ("SmbBattAlarm:EXITING/n"));

}

VOID SmbBattWorkerThread ( IN PDEVICE_OBJECT   Fdo, IN PVOID            Context )
{

   PSMB_ALARM_ENTRY        alarmEntry;

   PLIST_ENTRY             nextEntry;

  ULONG                   selectorState;

  ULONG                   batteryIndex;
BOOLEAN                 charging = FALSE;

  BOOLEAN                 acOn = FALSE;
PSMB_BATT_SUBSYSTEM     subsystemExt = (PSMB_BATT_SUBSYSTEM) Context;

do {
// // Check to see if we have more alarms to process.

  // If so retrieve the next one and decrement the

  // worker active count. //
nextEntry = ExInterlockedRemoveHeadList( &subsystemExt->AlarmList, &subsystemExt->AlarmListLock );
ASSERT (nextEntry != NULL);
alarmEntry = CONTAINING_RECORD (nextEntry, SMB_ALARM_ENTRY, Alarms);

// // Code here handles the specific alarm conditions.

  // It's been deleted for this tip because it's

  // device-specific. //
. . .

// // Free the alarm structure //
ExFreePool (alarmEntry);
} while (InterlockedDecrement (&subsystemExt->WorkerActive) != 0);

BattPrint(BAT_TRACE, ("SmbBattWorkerThread:EXITING/n"));

}

 

  当新警报到达时,SmbBattAlarm 函数被调用。这个函数创建一个描述警报的记录,将其添加到警报列表,并递增 WorkerActive 变量,该变量跟踪列表中的记录数量。SmbBattWorkerThread 在从列表中移除记录时递减这个变量。因此,如果变量的值是零,那么驱动程序可以确定工作项不在队列中。驱动程序使用 InterlockedXxx 函数来递增和递减 WorkerActive 变量,因为 SmbBattAlarm 和 SmbBattWorkerThread 都会改变这个值。在这个例子中,SmbBattAlarm 不使用与 SmbBattWorkerThread 函数已经处理的警报相关的数据,因此既不需要循环缓冲区也不需要上下文。

 

您应该做什么?

了解工作项是否已经被排队。

设计工作例程来在它们每次运行时执行所有可用的工作。

如果您的驱动程序对一个来自 DPC(或其他 DISPATCH_LEVEL 例程)的工作项重复排队,那么不要使用事件或类似的同步机制来等待工作项完成。

如果工作项和对其排队的例程共享可写的数据,那么应该同步访问以确保两个例程都操作正确的数据。

如果排队例程需要工作项已经处理的数据,那么分配一个上下文或维护一个更改历史记录。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值