管理PnP状态转换

来源:互联网 发布:linux 支持evernote吗 编辑:程序博客网 时间:2024/04/27 14:04
<script type="text/javascript"><!--google_ad_client = "pub-2050647664789618";//728x90, 创建于 07-11-28google_ad_slot = "5411034739";google_ad_width = 728;google_ad_height = 90;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>

[返回] [上一页] [下一页]

管理PnP状态转换


正如本章开头提到的,WDM需要跟踪设备的状态转换。状态跟踪还涉及到如何排队和取消IRP。而取消操作需要用到一个全局取消自旋锁,但该锁在多处理器机器上会造成性能瓶颈。IRP处理的标准模型不能解决所有这些问题。因此,在这一节中,我将介绍一种新的对象类型,DEVQUEUE,你可以在PnP请求处理中使用这种对象,它可以代替标准模型中的StartPacketStartNextPacket例程。DEVQUEUE是我自己的创造,使用它的例子驱动程序有PNPPOWER和CANCEL。关于IRP取消的其它讨论见Ervin Peretz的文章“The Windows Driver Model Simplifies Management of Device Driver I/O Requests” (Microsoft Systems Journal, January 1999)。我描述的一部分IRP取消逻辑还来自Peretz和其它Microsoft雇员的工作。

我以前描述的KDEVICE_QUEUE队列对象有三种状态:idle、busy-empty、busy-not empty。用于维护KDEVICE_QUEUE的支持例程假定设备当前不忙,你所做的仅是启动设备可执行的新请求。而这种行为正是我们管理PnP状态时所要克服的。图6-4显示了DEVQUEUE的状态。

图6-4. DEVQUEUE对象的状态

在READY状态中,队列的操作更象一个KDEVICE_QUEUE,它接收并发送请求到你的StartIo例程。在STALLED状态中,队列停止向StartIo运送IRP,即使设备处于空闲状态。在REJECTING状态,队列不接受新的IRP。图6-5显示了穿过队列的IRP流。

图6-5. DEVQUEUE中的IRP流

使用DEVQUEUE来排队和取消IRP

你可以为驱动程序要管理的每个请求队列定义一个DEVQUEUE对象。例如,如果你的设备用一个单独的队列来管理读写请求,你应该定义一个DEVQUEUE:

typedef struct _DEVICE_EXTENSION {  ...  DEVQUEUE dqReadWrite;   ...} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

表6-3列出了DEVQUEUE的支持函数

表6-3. DEVQUEUE的服务函数

支持函数 描述 AbortRequests 放弃当前及将来的请求 AllowRequests 允许请求,与AbortRequests相反 AreRequestsBeingAborted 检查新请求是否被取消 CancelRequest 普通的取消例程 CheckBusyAndStall 用一个原子操作检测空闲和停止 CleanupRequests 取消给定文件对象的所有请求,用于处理IRP_MJ_CLEANUP GetCurrentIrp 取当前正被处理的请求 InitializeQueue 初始化DEVQUEUE对象 RestartRequests 重启动一个停止的队列 StallRequests 停止队列 StartNextPacket 出队并启动下一个请求 StartPacket 启动或排队一个新请求 WaitForCurrentIrp 等待当前IRP完成

现在,我将讨论用于替代标准IRP处理模型中StartPacket和StartNextPacket函数的支持例程。对于每个队列,都应该提供一个独立的StartIo例程。DriverEntry例程将不在驱动程序对象的DriverStartIo指针域存储任何东西。相反,应该在AddDevice例程中初始化你的队列对象:

NTSTATUS AddDevice(...){  ...  PDEVICE_EXTENSION pdx = ...;  InitializeQueue(&pdx->dqReadWrite, StartIo);  ...}

对于使用DEVQUEUE队列的IRP,其派遣函数应该使用下面模式:

NTSTATUS DispatchWrite(PDEVICE_OBJECT fdo, PIRP Irp){  <some power management stuff you haven't heard about yet>  IoMarkIrpPending(Irp);  StartPacket(&pdx->dqReadWrite, fdo, Irp, OnCancel);  return STATUS_PENDING;}

即不调用IoStartPacket,而调用队列的StartPacket函数,其参数为队列对象地址、设备对象、IRP、取消例程。在派遣例程的开始处,你还要花一点代码来处理电源恢复问题,这将在第八章中讨论。

下面是用于DEVQUEUE的新StartIo例程:

VOID StartIo(PDEVICE_OBJECT fdo, PIRP Irp){  <some PnP stuff you haven't heard about yet>  // start request on device}

StartIo不必担心IRP的取消。在这里使用的取消例程与标准取消例程不同,它只是简单地把所有工作都委托给DEVQUEUE来处理:

VOID OnCancel(PDEVICE_OBJECT fdo, PIRP Irp){  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  CancelRequest(&pdx->dqReadWrite, Irp);}

CancelRequest将释放全局取消自旋锁,然后以线程安全和多处理器安全的方式取消IRP。

在请求完成时使用的DPC例程也与标准模型中的DPC例程稍有不同,见下面代码:

VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT device, PIRP junk, PVOID context){  PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite);  ...  StartNextPacket(&pdx->dqReadWrite, device);  <some PnP stuff you haven't heard about yet>  CompleteRequest(Irp, ...);}

就象IoStartNextPacket函数,StartNextPacket函数也从队列中删除下一个IRP并把它发送到你的(这种队列专用)StartIo例程。它也返回IRP地址,如果返回地址为NULL则代表该IRP因为某种原因被取消或放弃,所以试图完成这样的IRP是不正确的。你可以调用GetCurrentIrp来获得正在完成中的IRP的地址,不要使用来自DPC例程第三个参数的IRP指针,为此我命名该参数为junk以强化这一点。

DEVQUEUE也简化了IRP_MJ_CLEANUP的处理。实际的代码很短小:

NTSTATUS DispatchCleanup(PDEVICE_OBJECT fdo, PIRP Irp){  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);  CleanupRequests(&pdx->dqReadWrite, stack->FileObject, STATUS_CANCELLED);  return CompleteRequest(Irp, STATUS_SUCCESS, 0);}

用DEVQUEUE排队PnP请求

使用DEVQUEUE代替KDEVICE_QUEUE的关键原因是DEVQUEUE能简化PnP状态转换的管理。在我所有的例子驱动程序中,设备扩展都含有一个状态变量state。我还定义了一个枚举变量DEVSTATE,其值对应PnP状态。当你在AddDevice中初始化设备对象时,你应为每个设备队列调用InitializeQueue函数并指定设备为STOPPED状态:

NTSTATUS AddDevice(...){  ...  PDEVICE_EXTENSION pdx = ...;  InitializeQueue(&pdx->dqRead, StartIoReadWrite);  pdx->state = STOPPED;  ...}

AddDevice返回后,系统发出各种IRP_MJ_PNP请求,这些请求将指导设备进入各种PnP状态。

启动设备

刚初始化的DEVQUEUE将处于STALLED状态,StartPacket调用会把一个请求排入队列,不管设备是否处于空闲状态。你应该使队列保持STALLED状态直到你成功地处理了IRP_MN_START_DEVICE,如下面代码:

NTSTATUS HandleStartDevice(...){  status = StartDevice(...);  if (NT_SUCCESS(status))  {    pdx->state = WORKING;    RestartRequests(&pdx->dqReadWrite, fdo);  }}

你先把设备的当前状态记录为WORKING,然后为每个队列调用RestartRequests函数,该函数将释放在AddDevice运行后和你收到IRP_MN_START_DEVICE前所到达的所有IRP。

可以停止设备吗?

PnP管理器在停止你的设备前总是先询问,得到允许后才向你发送IRP_MN_STOP_DEVICE请求。询问以IRP_MN_QUERY_STOP_DEVICE请求的形式出现,你可以回答成功或失败。询问的基本含义是,“如果系统在几纳秒后向你发送IRP_MN_STOP_DEVICE,你能立即停止设备吗?”你可以以两种稍微不同的方式处理这个询问请求。第一种方式适合于可以迅速完成或者能容易地中途结束的IRP:

NTSTATUS HandleQueryStop(PDEVICE_OBJECT fdo, PIRP Irp){  Irp->IoStatus.Status = STATUS_SUCCESS;  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  if (pdx->state != WORKING)<--1    return DefaultPnpHandler(fdo, Irp);  if (!OkayToStop(pdx))<--2    return CompleteRequest(Irp, STATUS_UNSUCCESSFUL, 0);  StallRequests(&pdx->dqReadWrite);<--3  WaitForCurrentIrp(&pdx->dqReadWrite);  pdx->state = PENDINGSTOP;<--4  return DefaultPnpHandler(fdo, Irp);}
  1. 该语句用于处理引导设备这种特殊情况:如果你还没有完成设备初始化,PnP管理器将向你发送QUERY_STOP。你希望忽略这样的查询,这等于对PnP管理器说“是”。
  2. 在此,你执行某种调查函数,看是否可以回复到STOPPED状态。我们马上就要讨论这个问题。
  3. StallRequests把DEVQUEUE设置成STALLED状态,以便任何新的IRP能进入队列。WaitForCurrentIrp等待当前的请求完成。使设备停止有两个步骤,这两个步骤执行后我们就可以知道设备是否真的停止或仍在活动。
  4. 在此,我们没有理由犹豫,所以我们把设备的状态记录为PENDINGSTOP,然后把请求下传,这样其它驱动程序就可以有机会接受或拒绝这个查询。

另一种处理QUERY_STOP的方式适合于需要长时间才能完成并且不能被中途停止的IRP,例如磁带机的备份操作就不能被中途打断。在这种情况下,你可以使用DEVQUEUE的CheckBusyAndStall函数。如果你的设备忙,该函数返回TRUE,因此,你将以STATUS_UNSUCCESSFUL回答QUERY_STOP查询。如果设备空闲,该函数返回FALSE,在这种情况下,你还需要停止队列。(检测设备状态和停止队列操作需要一个自旋锁的保护,这就是为什么我要先写这个函数)

失败一个设备停止查询可以有多种原因。例如,用于分页机制的磁盘设备不能被停止。保存睡眠文件或dump文件的设备也不能被停止。(这些特征可以从IRP_MN_DEVICE_USAGE_NOTIFICATION请求中得到,我将在后面的“其它配置功能”中讨论这个请求) 此外还有其它原因。

即使你成功地回答了查询,但下层驱动程序可能会失败这个查询。即使所有驱动程序都成功地回答了查询,PnP管理器也可能决定不关闭你的设备。在这种情况下,你将收到另一个副功能码为IRP_MN_CANCEL_STOP_DEVICE的PnP请求,它通知设备不将被关闭。之后你应该清除在查询中设置的任何state值:

NTSTATUS HandleCancelStop(PDEVICE_OBJECT fdo, PIRP Irp){  Irp->IoStatus.Status = STATUS_SUCCESS;  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  if (pdx->state != PENDINGSTOP)    return DefaultPnpHandler(fdo, Irp);  NTSTATUS status = ForwardAndWait(fdo, Irp);  if (NT_SUCCESS(status))  {    pdx->state = WORKING;    RestartRequests(&pdx->dqReadWrite, fdo);  }  return CompleteRequest(Irp, status, Irp->IoStatus.Information);}

我们首先查看是否有一个未处理的停止操作。某些高级驱动程序可以否决一个查询,所以我们将看不到这样的查询,因此我们仍处于WORKING状态。如果我们没有处于PENDINGSTOP状态,我们就简单地下传该IRP。否则,我们发送CANCEL_STOP请求来同步低级驱动程序。即我们用自己的ForwardAndWait辅助函数来下传该IRP并等待其完成。我们等待低级驱动程序是因为我们要继续处理该IRP,并且低级驱动程序在我们向它发送IRP前也许会有工作要做。如果低级驱动程序成功地处理了IRP_MN_CANCEL_STOP_DEVICE,我们就改变state变量为WORKING状态,并且调用RestartRequests函数重新启动队列。

当设备停止时

如果所有设备驱动程序都成功地回答了查询并且PnP管理器也决定要关闭你的设备,你将收到一个IRP_MN_STOP_DEVICE请求。你的子派遣函数应该象这样:

NTSTATUS HandleStopDevice(PDEVICE_OBJECT fdo, PIRP Irp){  Irp->IoStatus.Status = STATUS_SUCCESS;  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  if (pdx->state != PENDINGSTOP);<--1  {    <complicated stuff>  }  StopDevice(fdo, pdx->state == WORKING);<--2  pdx->state = STOPPED;<--3  return DefaultPnpHandler(fdo, Irp);<--4}
  1. 我们希望系统在发送STOP请求前发送一个QUERY_STOP,因此我们的设备能够提前进入PENDINGSTOP状态并且让设备的所有队列都停止。然而,Windows 98中有一个bug,当我们需要REMOVE请求时,有时会得到STOP,而这个STOP之前却没有QUERY_STOP请求。因此,你必须采取一些动作以避免驱动程序拒绝任何新IRP,当收到一个REMOVE请求时,你不要真的删除你的设备对象或其它与真正REMOVE相关的动作。
  2. StopDevice是一个辅助函数,我们已经在取消设备配置时讨论过。
  3. 现在我们进入STOPPED状态。这与AddDevice刚执行完的情形几乎相同。即所有的队列都停止,设备没有I/O资源。仅有的不同是我们注册的接口仍然是允许的,这意味着应用程序还没有接到设备删除通知并且它们手中的设备句柄还是打开的。在这种情况下,应用程序仍然能打开新的设备句柄。但并不要紧,因为停止状态不会持续多长时间。
  4. 正如我们以前讨论过的,处理IRP_MN_STOP_DEVICE的最后一件事就是向下层驱动程序传递该请求。

可以删除设备吗?

与PnP管理器在停止设备前向你询问一样,它在删除设备前也向你询问,即IRP_MN_QUERY_REMOVE_DEVICE请求。你可以回答成功或失败。与停止查询相似,如果PnP管理器中途改变想法,它就发送IRP_MN_CANCEL_REMOVE_DEVICE请求。

NTSTATUS HandleQueryRemove(PDEVICE_OBJECT fdo, PIRP Irp){  Irp->IoStatus.Status = STATUS_SUCCESS;  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  if (OkayToRemove(fdo))<--1  {    StallRequests(&pdx->dqReadWrite);<--2    WaitForCurrentIrp(&pdx->dqReadWrite);    pdx->prevstate = pdx->state;<--3    pdx->state = PENDINGREMOVE;    return DefaultPnpHandler(fdo, Irp);  }  return CompleteRequest(Irp, STATUS_UNSUCCESSFUL, 0);}NTSTATUS HandleCancelRemove(PDEVICE_OBJECT fdo, PIRP Irp){  Irp->IoStatus.Status = STATUS_SUCCESS;  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  if (pdx->state != PENDINGREMOVE)<--4    return DefaultPnpHandler(fdo, Irp);  NTSTATUS status = ForwardAndWait(fdo, Irp);  if (NT_SUCCESS(status))  {    pdx->state = pdx->prevstate;<--5    if (pdx->state == WORKING)      RestartRequests(&pdx->dqReadWrite, fdo);  }  return CompleteRequest(Irp, status, Irp->IoStatus.Information);}
  1. OkayToRemove辅助函数回答这样的问题,“这个设备可以删除吗?”通常,这个回答将包含某些专用设备的成分,如设备是否存储分页文件或休眠文件,等等。
  2. 就象IRP_MN_QUERY_STOP_DEVICE一样,你还需要停止队列,并且如果需要,你还要等待一小段时间,直到当前请求完成。
  3. 如果你仔细观察图6-1,你会注意到当设备处于WORKING或STOPPED状态时驱动程序有可能收到一个QUERY_REMOVE。正确的做法是返回到原来的状态。所以我在设备扩展中放了一个prestate变量,它记录查询前的设备状态。
  4. 如果我们上面或下面的驱动程序否决了QUERY_REMOVE,则我们将收到CANCEL_REMOVE请求。如果我们根本就没有看到该查询,我们将仍旧处于WORKING状态并且不需要对该IRP做任何事。否则,我们就必须在处理前把该IRP发送到低级驱动程序,因为我们希望低级驱动程序在我们处理删除请求前做好准备。
  5. 在这里,如果我们成功地回答了QUERY_REMOVE,我们就恢复做过的步骤,回复到以前的状态。如果以前的状态是WORKING,则还要启动在处理查询时被停止的队列。

同步删除

有时候I/O管理器发出的PnP请求会与其它I/O请求(如包含读写的请求)同时出现。这完全有可能,例如当你处理其它IRP时收到了IRP_MN_REMOVE_DEVICE请求。你必须自己避免这种麻烦产生,标准的做法是使用一个IO_REMOVE_LOCK对象和几个相关的内核模式支持例程。

防止设备被过早地删除的基本想法是在每一次开始处理请求时都获取删除锁,处理完成后释放删除锁。在你删除你的设备对象前,应确保删除锁未被使用。否则,你将等到这个锁的所有引用都被释放。图6-6显示了这个过程。

图6-6. 操作IO_REMOVE_LOCK

为了实现这个处理机制,你应该在设备扩展中定义一个锁变量:

struct DEVICE_EXTENSION {  ...  IO_REMOVE_LOCK RemoveLock;  ...};

还应该在AddDevice中初始化这个锁对象:

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo){  ...  IoInitializeRemoveLock(&pdx->RemoveLock, 0, 0, 0);  ...}

IoInitializeRemoveLock的最后三个参数分别代表标签值、锁的最大生存期、锁计数器的最大值,它们在free版本的操作系统中不被使用。第三个和第四个参数为0代表你不希望执行对应的错误检测(生存期和嵌套级),即使在checked版本的操作系统中。

无论何时,当你收到一个I/O请求时(除了IRP_MJ_CREATE),你就调用IoAcquireRemoveLock。如果删除设备的操作正在进行,则IoAcquireRemoveLock返回STATUS_DELETE_PENDING。否则,该函数将获得删除锁并返回STATUS_SUCCESS。一旦你完成一个I/O操作,就调用IoReleaseRemoveLock,该函数将释放删除锁以及目前未处理的删除操作。下面是一个纯粹假设的派遣函数,该函数正完成手头的IRP:

NTSTATUS DispatchSomething(PDEVICE_OBJECT fdo, PIRP Irp){  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp);  if (!NT_SUCCESS(status))    return CompleteRequest(Irp, status, 0);  ...  IoReleaseRemoveLock(&pdx->RemoveLock, Irp);  return CompleteRequest(Irp, <some code>, <info value>);}

IoAcquireRemoveLock和IoReleaseRemoveLock的第二个参数仅是一个标签值,checked版本的操作系统用它来匹配请求或释放调用。

获取和释放删除锁的调用与PnP派遣函数和删除设备子派遣函数中的附加逻辑正好吻合。首先,DispatchPnp必须遵守锁定设备和解锁设备的规则,所以它将包含下面代码,这些代码以前没有在“IRP_MJ_PNP派遣函数”中出现过:

NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp){  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp);  if (!NT_SUCCESS(status))    return CompleteRequest(Irp, status, 0);  ...  status = (*fcntab[fcn](fdo, Irp);  if (fcn != IRP_MN_REMOVE_DEVICE)    IoReleaseRemoveLock(&pdx->RemoveLock, Irp);  return status;}

换句话说,DispatchPnp先锁定设备,然后调用子派遣函数,最后解锁设备。而关于IRP_MN_REMOVE_DEVICE的子派遣函数也含有你没见过的专用逻辑:

NTSTATUS HandleRemoveDevice(PDEVICE_OBJECT fdo, PIRP Irp){  Irp->IoStatus.Status = STATUS_SUCCESS;  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  AbortRequests(&pdx->dqReadWrite, STATUS_DELETE_PENDING);<--1  DeregisterAllInterfaces(pdx);  StopDevice(fdo, pdx->state == WORKING);  pdx->state = REMOVED;  NTSTATUS status = DefaultPnpHandler(pdx->LowerDeviceObject, Irp);<--2  IoReleaseRemoveLockAndWait(&pdx->RemoveLock, Irp);<--3  RemoveDevice(fdo);  return status;}
  1. Windows 98不发送SURPRISE_REMOVAL请求,所以这个删除IRP就是第一个指出设备已经消失的IRP。调用StopDevice允许你释放设备占用的所有I/O资源。调用AbortRequests将使你完成任何排队的IRP,并且从现在开始将不再接受任何新的IRP。
  2. 我们已经做完了自己的工作,现在把这个请求传递到低层驱动程序。
  3. PnP派遣例程占有着删除锁。我们现在调用专用的IoReleaseRemoveLockAndWait函数来释放锁引用并等待所有对该锁的引用都被释放。一旦IoReleaseRemoveLockAndWait函数返回,任何后来的IoAcquireRemoveLock调用都将得到STATUS_DELETE_PENDING返回状态,指出设备正在删除。
注意
有时当某个IRP完成时,IRP_MN_REMOVE_DEVICE处理程序会被阻塞。这在Windows 98和Windows 2000中确实允许,因为它们的设计允许这种可能性,IRP_MN_REMOVE_DEVICE是在一个系统线程的上下文中发送的,所以允许阻塞。某些WDM功能甚至存在于OEM版本的Windows 95中,但你不能在那里阻塞设备删除请求。因此,如果你的设备需要运行在Windows 95中,你需要避免这种阻塞。

这就是防止设备在使用时被删除的锁定和解锁机制。为了运用这个机制,你还需要知道何时调用IoAcquireRemoveLock和IoReleaseRemoveLock函数。基本上,可以快速完成请求的IRP派遣函数应该获取和释放这种锁。

然而,需要排队IRP的派遣例程不应该获取删除锁。对于一个在队列中的IRP,你应该在StartIo函数中获取删除锁,并在DPC例程中释放删除锁。因此,我们扩展了StartIoDpcForIsr的框架例程:

VOID StartIo(PDEVICE_OBJECT fdo, PIRP Irp){  PDEVICE_EXTENSION pdx =(PDEVICE_EXTENSION) fdo->DeviceExtension;  NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp);<--1  if (!NT_SUCCESS(status))  {    CompleteRequest(Irp, status, 0);<--2    return;  }  // start request on device}VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT device, PIRP junk, PVOID context){  PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;  PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite);  ...  StartNextPacket(&pdx->dqReadWrite, device);  IoReleaseRemoveLock(&pdx->RemoveLock, Irp);<--3  CompleteRequest(Irp, ...);}
  1. 我们在这里获取了删除锁而不是在派遣例程中。我们不希望以在队列中存放一个IRP的方式来防止PnP管理器关闭我们的设备。另外,我们最好不让取消例程关心这个删除锁。
  2. IoAcquireRemoveLock只有在删除操作未决时才失败。该函数的返回值可以为STATUS_SUCCESS或STATUS_DELETE_PENDING。在失败的情况中,不要调用StartNextPacket函数,因为在设备将要消失时我们不能启动一个新操作。如果我们调用StartNextPacket函数,将递归调用StartIo函数,StartIo函数将再次获取删除锁并失败,而这又导致调用StartNextPacket,它又会去调用StartIo,最后由于堆栈溢出而导致BSOD。
  3. IoReleaseRemoveLock调用与StartIo中的IoAcquireRemoveLock调用相对。

在IRP_MJ_CREATE派遣例程中没有必要获取删除锁。第六章最后给出的兼容性讨论解释了这个问题,在Windows 98中处理IRP_MJ_CREATE请求时不应该获取删除锁,否则将导致死锁。在Windows 2000中你也不需要获取删除锁,尽管这不会带来什么坏处,因为当设备句柄打开时,PnP管理器不会向你发送IRP_MN_REMOVE_DEVICE。Service Pack-2包含了修订后的例子驱动程序和一个修订后的驱动程序向导。

如果用户使用设备管理器删除设备,而某应用程序正拥有该设备的打开句柄,那么操作系统将拒绝删除该设备并通知用户。如果设备被用户从计算机上物理地摘除并且没有使用设备管理器,那么一个良好的应用程序应该注意WM_DEVICECHANGE消息,该消息通知应用程序设备已经被卸载,应用程序接着应该关闭设备句柄,驱动程序应该延迟执行IRP_MN_REMOVE_DEVICE请求,直到句柄被真正关闭,其实这也是删除锁逻辑允许你做的。

DEVQUEUE如何工作

不同于本书中的其它例子,我将在这里给出DEVQUEUE对象的全部实现代码。

初始化DEVQUEUE

在DEVQUEUE.H头文件中,DEVQUEUE对象有如下声明:

typedef struct _DEVQUEUE {  LIST_ENTRY head;  KSPIN_LOCK lock;  PDRIVER_START StartIo;  LONG stallcount;  PIRP CurrentIrp;  KEVENT evStop;  NTSTATUS abortstatus;} DEVQUEUE, *PDEVQUEUE;

InitializeQueue按下面方式初始化这种对象:

VOID NTAPI InitializeQueue(PDEVQUEUE pdq, PDRIVER_STARTIO StartIo){  InitializeListHead(&pdq->head);<--1  KeInitializeSpinLock(&pdq->lock);<--2  pdq->StartIo = StartIo;<--3  pdq->stallcount = 1;<--4  pdq->CurrentIrp = NULL;<--5  KeInitializeEvent(&pdq->evStop, NotificationEvent, FALSE);<--6  pdq->abortstatus = (NTSTATUS) 0;<--7}
  1. 我们使用一个普通(非互锁)的双链表来排队IRP。因为我们用自己的自旋锁保护链表访问,所以不必使用互锁链表。
  2. 该自旋锁既保护队列的访问,也保护DEVQUEUE结构中的其它域。它还代替了全局取消自旋锁,既保护了全部的取消处理,也提高了系统性能。
  3. 每个队列都有自己的StartIo函数。
  4. 停止计数器指出提交到停止的StartIo的IRP的次数。初始化这个计数器为1代表IRP_MN_START_DEVICE处理程序必须调用RestartRequests释放一个IRP。
  5. CurrentIrp域记录着最近发往StartIo例程的IRP。初始化这个域为NULL指出设备开始是空闲的。
  6. 必要时,我们使用这个事件来阻塞WaitForCurrentIrp。我们将在StartNextPacket函数中设置这个事件,该函数在当前IRP完成时总被调用。
  7. 有两种情况我们必须拒绝到来的IRP。第一种情况是在我们真正提交设备删除请求后,当我们必须启动一个状态为STATUS_DELETE_PENDING的新IRP时。第二种情况是在低电源期间,参照我们管理的设备类型,我们应该把新IRP标上STATUS_DEVICE_POWERED_OFF失败代码。abortstatus域记录着我们拒绝的IRP所使用的状态代码。

停止队列

停止IRP队列包含两个DEVQUEUE函数:

VOID NTAPI StallRequests(PDEVQUEUE pdq){  InterlockedIncrement(&pdq->stallcount);<--1}BOOLEAN NTAPI CheckBusyAndStall(PDEVQUEUE pdq){  KIRQL oldirql;  KeAcquireSpinLock(&pdq->lock, &oldirql);<--2  BOOLEAN busy = pdq->CurrentIrp != NULL;<--3  if (!busy)<--4    InterlockedIncrement(&pdq->stallcount);  KeReleaseSpinLock(&pdq->lock, oldirql);  return busy;}
  1. 为了停止请求,我们仅需要设置停止计数器为一个非零的值。没有必要使用一个自旋锁来保护这个增1操作,因为任何与我们竞争的设备也必须使用互锁的增1或减1函数来更改这个值。
  2. 因为CheckBusyAndStall函数需要以原子方式操作,所以我们应该先获取队列的自旋锁。
  3. CurrentIrp为非NULL则代表设备正忙于处理队列中的某个请求。
  4. 如果设备当前空闲,则该语句启动停止的队列,这能防止设备后来变为忙。

排队IRP

当某派遣函数调用StartPacket时,一个IRP就被加入到队列中:

VOID NTAPI StartPacket(PDEVQUEUE pdq, PDEVICE_OBJECT fdo, PIRP Irp, PDRIVER_CANCEL cancel){                  // StartPacket  KIRQL oldirql;<--1  KeAcquireSpinLock(&pdq->lock, &oldirql);  NTSTATUS abortstatus = pdq->abortstatus;<--2  if (abortstatus)  {                // aborting all requests now    KeReleaseSpinLock(&pdq->lock, oldirql);    Irp->IoStatus.Status = abortstatus;    IoCompleteRequest(Irp, IO_NO_INCREMENT);  }                // aborting all requests now  else if (pdq->CurrentIrp || pdq->stallcount)<--3  {                // queue this irp    IoSetCancelRoutine(Irp, cancel);<--4    if (Irp->Cancel && IoSetCancelRoutine(Irp, NULL))    {              // IRP has already been cancelled      KeReleaseSpinLock(&pdq->lock, oldirql);      Irp->IoStatus.Status = STATUS_CANCELLED;      IoCompleteRequest(Irp, IO_NO_INCREMENT);    }              // IRP has already been cancelled    else    {              // queue IRP      InsertTailList(&pdq->head, &Irp->Tail.Overlay.ListEntry);<--5      KeReleaseSpinLock(&pdq->lock, oldirql);    }              // queue IRP  }                // queue this irp  else<--6  {                // start this irp    pdq->CurrentIrp = Irp;    KeReleaseSpinLock(&pdq->lock, DISPATCH_LEVEL);    (*pdq->StartIo)(fdo, Irp);    KeLowerIrql(oldirql);  }                // start this irp}                  // StartPacket
  1. 获取自旋锁可以使我们访问DEVQUEUE中的域不受其它支持例程的干扰,主要是StartNextPacket,该函数试图访问同一个队列。
  2. 如我以前描述的,我们有时需要拒绝到来的IRP。如果abortstatus为非零,我们就完成该IRP。我们的调用者将得到STATUS_PENDING,所以需要由我们自己来做完成操作。
  3. 如果设备当前正忙,或者驱动程序的其它例程停止了设备队列,我们需要排队该IRP以便以后处理。
  4. 我们可能与IoCancelIrp的一个实例竞争,该函数试图取消这个IRP。我们先用IoSetCancelRoutine例程在该IRP中安装我们自己的取消例程,该例程执行一个互锁的数据交换。然后我们测试Cancel标志。如果Cancel标志被设置,我们的取消例程可能被调用,也可能没被调用,这取决于我们的代码与IoCancelIrp执行的确切顺序。如果我们的取消例程被调用,则第二次调用IoSetCancelRoutine将返回NULL;这样我们就能把该IRP加入队列,并利用取消例程立即提取该IRP并完成它。如果我们的取消例程没有被调用,我们将在这里完成该IRP。
  5. 这里我们真正地排队该IRP。IRP的Tail.Overlay.ListEntry域被设计成这样使用。
  6. 最后一种情况是,当队列处于READY状态并且设备当前不忙。我们设置DEVQUEUE中的CurrentIrp指针,释放自旋锁,然后在DISPATCH_LEVEL级上调用StartIo例程。

拥有自旋锁的程序可以修改CurrentIrp,因此我们确信在测试CurrentIrp时不会得到含糊的结果。另一方面,停止计数器在StallRequests中被增1却没有自旋锁的保护。很明显,问题发生的唯一机会就是当计数器被从0增到1时,由于我们并不关心计数器有什么非零值,因此我们的例程可能与此同时执行。假设有一个StallRequests调用要把计数器的值从0增到1,如果我们阻止增1操作而发现计数器的值为0,我们将前进并启动下一个请求。因为StallRequests的调用者愿意使设备处于忙状态(如果调用者不愿意,它应使用CheckBusyAndStall函数),所以这样做可以。如果我们发现计数器已增到1,我们就排队该IRP,这也符合StallRequests调用者的意图。

出队IRP

出队大部分IRP的函数是StartNextPacket,该函数在一个DPC例程中被调用:

PIRP NTAPI StartNextPacket(PDEVQUEUE pdq, PDEVICE_OBJECT fdo){  KIRQL oldirql;<--1  KeAcquireSpinLock(&pdq->lock, &oldirql));  PIRP CurrentIrp = (PIRP) InterlockedExchangePointer (&pdq->CurrentIrp, NULL);<--2  if (CurrentIrp)<--3    KeSetEvent(&pdq->evStop, 0, FALSE);  while (!pdq->stallcount && !pdq->abortstatus && !IsListEmpty(&pdq->head))<--4  {    PLIST_ENTRY next = RemoveHeadList(&pdq->head);<--5    PIRP Irp = CONTAINING_RECORD(next, IRP, Tail.Overlay.ListEntry);    if (!IoSetCancelRoutine(Irp, NULL))<--6    {      InitializeListHead(&Irp->Tail.Overlay.ListEntry);      continue;    }    pdq->CurrentIrp = Irp;    KeReleaseSpinLockFromDpcLevel(&pdq->lock);<--7    (*pdq->StartIo)(fdo, Irp);    KeLowerIrql(oldirql);    return CurrentIrp;  }  KeReleaseSpinLock(&pdq->lock, oldirql);  return CurrentIrp;}
  1. 我们首先获取该队列的自旋锁,以便我们能不受干扰地访问队列对象的内部结构。
  2. 我们将把当前IRP的地址作为返回值,并且还设置CurrentIrp指针为NULL。由于使用了自旋锁,我们不必使用原子操作来提取并置空CurentIrp的值。
  3. 某些例程可能在WaitForCurrentIrp中等待当前请求的完成。调用KeSetEvent将满足那些等待。
  4. 这一系列检测决定我们是否能出队一个请求。队列不是停止的,也不处于REJECTING状态,队列至少应包含一个请求,这样我们调用RemoveHeadList才有意义。
  5. 这行代码删除队列中最旧的一个IRP。
  6. 置空IRP中的取消例程指针将阻止IoCancelIrp企图取消该IRP。如果试图取消该IRP的IoCancelIrp函数此时正在另一个CPU上运行,我们应该从IoSetCancelRoutine函数得到NULL返回值。当CancelRequest又获得控制时,它将需要获取队列的自旋锁以便继续进一步处理。在这一点上,它将盲目地从当前队列中删除该IRP。在该IRP自己的连接域上调用InitializeListHead将使CancelRequest在做进一步处理时更安全。
  7. 在这里我们把刚出队的IRP送到StartIo进行处理。

RestartRequests函数平衡StallRequests调用或CheckBusyAndStall调用。它把队列的第一个IRP送到StartIo例程,幸好,我们可以利用StartNextPacket:

VOID NTAPI RestartRequests(PDEVQUEUE pdq, PDEVICE_OBJECT fdo){  if (InterlockedDecrement(&pdq->stallcount) > 0)    return;  StartNextPacket(pdq, fdo);}

取消IRP

StartPacket寄存了由其调用者提供的取消例程,该例程然后又把工作委托给队列的CancelRequest函数:

VOID NTAPI CancelRequest(PDEVQUEUE pdq, PIRP Irp){  KIRQL oldirql = Irp->CancelIrql;  IoReleaseCancelSpinLock(DISPATCH_LEVEL);  KeAcquireSpinLockAtDpcLevel(&pdq->lock);  RemoveEntryList(&Irp->Tail.Overlay.ListEntry);  KeReleaseSpinLock(&pdq->lock, oldirql);  Irp->IoStatus.Status = STATUS_CANCELLED;  IoCompleteRequest(Irp, IO_NO_INCREMENT);}

我们在拥有全局取消自旋锁的情况下被调用,而我们立刻又释放了这个自旋锁。这之后,所有的保护工作由队列的自旋锁来代替。当IoCancelIrp调用IoAcquireCancelSpinLock时,它在IRP的CancelIrql域保存了以前的中断请求级,我们最后需要回复到那个IRQL,因此,我们把这个值保存到oldirql变量中。

注意
IoCancelIrp的调用者有责任确保该IRP没有被完成。

IRP_MJ_CLEANUP也能取消IRP,该请求在我们收到IRP_MJ_CLOSE请求前收到。DEVQUEUE的CleanupRequests函数几乎与标准模型中的DispatchCleanup例程完全相同。两者仅有的不同之处是CleanupRequests函数需要获取队列的自旋锁:

VOID NTAPI CleanupRequests(PDEVQUEUE pdq, PFILE_OBJECT fop, NTSTATUS status){  LIST_ENTRY cancellist;<--1  InitializeListhead(&cancellist);  KIRQL oldirql;  KeAcquireSpinLock(&pdq->lock, &oldirql);  PLIST_ENTRY first = &pdq->head;  PLIST_ENTRY next;  for (next = first->Flink; next != first; )<--2  {    PIRP Irp = CONTAINING_RECORD(next, IRP, Tail.Overlay.ListEntry);    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);<--3    PLIST_ENTRY current = next;    next = next->Flink;<--4    if (fop && stack->FileObject != fop)      continue;    if (!IoSetCancelRoutine(Irp, NULL))<--5      continue;    RemoveEntryList(current);<--6    InsertTailList(&cancellist, next);  }  KeReleaseSpinLock(&pdq->lock, oldirql);<--7  while (!IsListEmpty(&cancellist))  {    next = RemoveHeadList(&cancellist);    PIRP Irp = CONTAINING_RECORD(next, IRP, Tail.Overlay.ListEntry);    Irp->IoStatus.Status = status;    IoCompleteRequest(Irp, IO_NO_INCREMENT);  }}
  1. 我们的策略是,在队列自旋锁的保护下把需要删除的IRP移到我们私有的队列中。因此,第一步就是初始化这个私有队列并获取自旋锁。
  2. 该循环遍历整个队列,直到返回队列头。注意,for语句的第三个子句没有循环自增步骤。
  3. 如果我们为处理IRP_MJ_CLEANUP请求而被调用,则fop参数就是将要被关闭的文件对象的地址。我们要排除同属于这个文件对象的IRP,因此我们需要先找到该IRP的堆栈单元。
  4. 如果我们决定从队列中删除这个IRP,我们就不能方便地找到主队列中的下一个IRP,因此我们在这里使用这样的语句,这里也执行了循环的自增步骤。
  5. 这个尤其明智的语句是由Jamie Hanrahan添加的。我们需要知道是否有人试图取消我们当前正在寻找的IRP。它们仅能到达CancelRequest试图获取自旋锁的地方。然而在到达那里之前,它们必须执行IoCancelIrp中置空取消例程指针的语句。如果我们用IoSetCancelRoutine函数发现那个指针为空,我们就能确信有人真的要取消这个IRP。因此我们就在这个循环中简单地跳过该IRP,稍后我们再允许取消例程完成该IRP。
  6. 在这里我们把IRP从主队列中取出并放入我们自己的队列。
  7. 一旦完成IRP的移动,我们就可以释放自旋锁。然后我们取消找到的所有IRP。

CleanupRequests可以在驱动程序的任何地方被调用。例如,从我以前提到的IRP_MN_REMOVE_DEVICE处理程序中调用,它给出一个NULL文件对象指针(指出清除所有IRP)和一个为STATUS_DELETE_PENDING的状态代码。

等待当前的IRP

IRP_MN_STOP_DEVICE的处理程序需要等待当前IRP的完成,调用WaitForCurrentIrp

VOID NTAPI WaitForCurrentIrp(PDEVQUEUE pdq){  KeClearEvent(&pdq->evStop);<--1  ASSERT(pdq->stallcount != 0);<--2  KIRQL oldirql;<--3  KeAcquireSpinLock(&pdq->lock, &oldirql);  BOOLEAN mustwait = pdq->CurrentIrp != NULL;  KeReleaseSpinLock(&pdq->lock, oldirql);  if (mustwait)    KeWaitForSingleObject(&pdq->evStop, Executive, KernelMode, FALSE, NULL);}
  1. 每次调用StartNextPacket函数,它都使evStop事件进入信号态。为了不使我们的等待被错误地满足,我们先清除该事件。
  2. 不先停止队列就调用该函数是无意义的。此外,StartNextPacket将启动下一个IRP,因此,设备将再次进入忙状态。
  3. 如果设备当前忙,我们将在evStop事件上等待,直到某段代码调用了StartNextPacket使该事件进入信号态。我们需要用自旋锁保护对CurrentIrp的探测操作,通常,测试一个指针是否为NULL并不是一个原子事件。如果指针现在为NULL,以后它就不能被改变,因为我们假定队列是停止的。

放弃请求

设备的突然删除要求我们立即停止所有试图触及硬件的未决IRP。另外,我们还要能拒绝所有后来的IRP。AbortRequests函数可以完成这个任务:

VOID NTAPI AbortRequests(PDEVQUEUE pdq, NTSTATUS status){  pdq->abortstatus = status;  CleanupRequests(pdq, NULL, status);}

abortstatus的设置将使队列进入REJECTING状态,这样所有后来的IRP都将被拒绝,其返回的状态值由我们的调用者提供。在这里,以NULL文件对象指针调用CleanupRequests函数将清空整个队列。

我们不应该碰任何当前在硬件上活动的IRP。不使用HAL访问硬件的驱动程序,例如USB驱动程序,将依靠hub和主控制器驱动程序来废弃当前IRP。而使用HAL的驱动程序可能会挂起系统,或至少使IRP进入不稳定状态,因为不存在的硬件不会生成中断使IRP完成。为了处理这种情况,你应该调用AreRequestsBeingAborted

NTSTATUS AreRequestsBeingAborted(PDEVQUEUE pdq){  return pdq->abortstatus;}

在这个例程中使用队列自旋锁是愚蠢的。假定我们要以线程安全和多处理器安全的方式捕获abortstatus的瞬间值,那么一旦我们释放该自旋锁,这个值就将变成无用值。

注意
如果你的设备可以以这种方式删除,即简单地挂起未决的请求,你应该有一个看门狗定时器(Watchdog Timer)在运行,它可以在一段时间后杀死这些IRP。看门狗定时器见第九章。

有时我们需要恢复上一个AbortRequest调用的作用。AllowRequests可以做到这一点:

VOID NTAPI AllowRequests(PDEVQUEUE pdq){  pdq->abortstatus = (NTSTATUS) 0;}
 
原创粉丝点击