The Windows Driver Model Simplifies Management of Device Driver I/O Requests(WDM对设备驱动I/O请求管理的简化)

The Windows Driver Model Simplifies Management of Device Driver I/O Requests


      (WDM对设备驱动I/O请求管理的简化)








Ervin Peretz  译者:chuajiang


这个只是本人在学习WDM驱动开发时候顺便翻译的,主要是为了让自己能够对WDM中的IRPs有个深刻的了解,同时希望能够对看到这篇文章的你有用,这个是我最欣慰的了.在翻译过程中肯定会有很多的不当之处,希望大家能够谅解,只是为了学习交流用之,如能被转载,实在是诚惶诚恐,如再能注明出处,那更是受宠若惊了...
  http://www.microsoft.com/msj/0199/windowsdriver/windowsdriver.aspx
 上面连接地址是原文地址.
  
















 From the January 1999 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.








This article assumes you're fami liar with C, WDM


 Code for this article: WDMIRP.exe (2KB)


作者简介: Ervin Peretz is a USB and peripherals device driver developer in the Windows NT core drivers group at Microsoft. He can be reached at ervinp@microsoft.com.














Windows NT设备驱动使用IRPs来传输消息和数据。WDM为Windows 98和2000定义了一个公共的驱动结构,由于WDM驱动模型都是从旧有的WindowsNT中发展过来的,所以在我们讨论IRP在WDM驱动模型中的处理,完全可以应用到Windows NT当中去。


当我发现开发人员对于WDM模型的一知半解,甚至是比较有经验的驱动开发人员对于如何管理IRPs也不是非常了解的时候,我决定写这篇文章以使广大开发人员能够进一步了解IRPs在WDM中的处理过程。不像一些文章中包括整个驱动程序的源代码,我这里只给出一些通用性的函数,这些函数你可以直接拷贝到你的驱动代码当中。


这篇文章主要讲解给驱动开发人员带来很多麻烦的IRP排队和相关的WDM。在有了前面那些一般性的概念之后,我将会深入到同步以及其他一些相关的内容,进一步了解这些内容,可以使你成为一个成功的WDM开发人员。这里我假设你已经有了WDM相关的一些工作经验。如果想了解WDM的概念,可以查看下面两篇文章:Walter Oney's articles Surveying the New Win32 Driver Model for Windows 98 and Windows NT 5.0 in the November 1997 issue and Implementing the New Win32 Driver Model for Windows 98 and Windows NT 5.0 in the December 1997 issue of MSJ.






WDM Basics 


就象消息是Windows应用程序的源头一样,IRPs是WDM驱动程序的源头。在WDM当中,所有的I/O操作潜在当中都是以异步的方式进行的。初始化I/O的函数不必立即返回I/O操作的结果,而是可以通过一个I/O完成函数来返回结果。


设备驱动程序的结构通常是用一个驱动堆栈来表示,这些设备驱动被划分成底层驱动的一个个特殊的功能。比如说,一个带有USB接口的设备,有一个USB槽驱动(总线驱动)来驱动USB控制芯片,然后上层是一些设备类(Cameras)的mini驱动和类驱动,再上一层是厂商提供的驱动程序,这部分驱动用来控制连接到USB接口的照相机的特殊功能。具体结构如下图1所示。


<!--[if !vml]-->
<!--[endif]-->


<!--[if !vml]--><!--[endif]-->                       Figure 1  Driver Stack


IRP是一个内核或驱动申请的资源结构,这个结构代表了一个单独的I/O操作。I/O初始化驱动程序初始化IRP的资源结构,包括I/O请求类型,可选的完成例程,输入输出缓冲区等资源,然后把一个指向这个IRP的指针传给驱动堆栈。一旦接收到一个IRP请求,驱动程序就可以完成下面的一些操作:


<!--[if !supportLists]-->1.  <!--[endif]-->响应I/O请求并且成功的完成IRP


<!--[if !supportLists]-->2.  <!--[endif]-->完成IRP,返回一个错误


<!--[if !supportLists]-->3.  <!--[endif]-->传递IRP给下一层驱动


<!--[if !supportLists]-->4.  <!--[endif]-->排队IRP,推迟IRP的完成或传递


在任何情况下,一个工作稳定的WDM驱动程序从来不会阻塞或取消响应IRP,窗口程序能被连续的响应及优先响应是很有必要的。


有时在I/O被初始化之后,一个底下驱动就完成了IRP。内核通过指向IRP的指针来调用每个驱动程序的完成例程。因此,直到顶层的驱动程序的完成例程得到一个返回的结果或错误,IRP才返回到驱动堆栈。


我们看一下前面提到的USB照相机的堆栈(图一)的例子,我这里只是介绍一下I/O初始化部分中有关IRP处理的部分。当ACME照相机插入USB槽后,USB hub(总线槽)驱动初始化一个PnP事件。照相机的PnP ID开始匹配系统的注册表或INF安装脚本中的相应的ID,匹配以后,系统开始装载mini驱动和类驱动以及ACME照相机驱动。Mini驱动通过把其内部的资源传递给类驱动中的class-specific的函数来注册自己。内核调用ACME照相机的驱动程序中的AddDevice函数,这个函数传递一个指向表示照相机的设备对象的指针给内核。ACME驱动程序调用IoCreateDevice函数来产生一个自己的设备对象,然后调用IoAttachDeviceToDeviceStack来连接它的新产生的设备对象到驱动堆栈的顶部(在这种情况下,这些设备对象连接到另一个的上面)。 


每一个设备对象都有一个StackSize的标记,用来表示设备对象在设备堆栈中所占空间的大小,对应的,还表示驱动程序在驱动堆栈中所占空间的大小。被发送给驱动堆栈的IRP必须有一个可写的私有区域(空间)或者是IRP堆栈空间以让每个驱动程序可以操作。当ACME照相机驱动程序想从照相机中读取视频帧数据的时候,就调用IoAllocateIrp,根据设备对象中StackSize中的值来产生一个有合适大小堆栈空间的IRP。


为了创建I/O操作,照相机驱动在IRP堆栈空间设置了一些值。我们约定每一个驱动程序都可以操作其下层驱动的IRP堆栈空间。照相机驱动程序调用IoGetNextIrpStackLocation来得到一个PIO_Stack_LOCATION指针,这个指针指向照相机类驱动程序的堆栈空间。照相机驱动通过设置类驱动堆栈空间的MajorFunction和MinorFunction来标示照相机驱动的具体操作。这些操作又涉及到IRP的I/O缓存区(这个缓存模式在后面介绍)。然后照相机驱动用IoSetCompletionRoutine来为IRP设置完成例程。初始化驱动需要这个完成例程,因为初始化驱动需要获取返回的结果。但是对于下层驱动,这个完成例程是可选的,因为下层驱动也许需要或者不需要后期处理。就象访问参数一样,指向当前驱动的完成例程的指针也被储存在下一层的驱动程序的堆栈空间中。设置完成例程如下所示:


IoSetCompletionRoutine( Irp,  


     MyCompletionRoutine,  


     DeviceExtension,  // context passed to completion routine 


     TRUE,             // InvokeOnSuccess 


     TRUE,             // InvokeOnError 


     TRUE);            // InvokeOnCancel 


  status = IoCallDriver(NextDeviceObject, Irp);
 <!--[if !vml]--><!--[endif]-->    照相机驱动程序调用IoCallDriver通过IRP指针把IRP传递给照相机的类驱动程序,这个类驱动程序通过驱动对象的MajorFunction数组接口来接收IRP。而类驱动可能仅仅是把这个IRP向下传递给mini驱动,而mini驱动现在的工作就是从IRP的I/O缓存区中得到结果并完成这个IRP。


在一些驱动结构中,这种中间层的驱动程序也许仅仅是把IRP传递给下一级的驱动程序,让底层驱动程序完成所有的工作。但是在我们这种情况下,假设USB hub驱动没有处理1MB的IRP,这时mini驱动排队IRP并且返回一个STATUS_PENDING,而mini驱动需要保持对这个IRP的控制权。在一些特殊设备的情形下,mini驱动必须马上响应IRP。例如:mini驱动也许会从USB hub驱动程序中产生一系列的IRP来请求小尺寸的USB请求包,而类驱动的工作就是把这些视频帧从这些mini小端口(mini驱动)多重发送给多个上层客户,这些mini端口的工作就是识别视频帧的边界并把这个视频帧返回给类驱动程序。当mini驱动接收到一个完成的视频帧时,它把这个帧复制到原始IRP的缓存中,把Irp->IoStatus.Status设置为STATUS_SUCESS,并调用IoCompleteRequest来完成IRP。照相机驱动的完成例程由内核进行调用,照相机驱动可以对视频帧进行如何想要得操作,同时可以调用IoFreeIrp来释放IRP。


前面讲述的例子有很多地方是可以变动和优化的,同时有很多的细节问题也没有提起,因为那需要更加深入的讨论。例如:当顶层驱动的完成例程释放IRP时,它必须返回STATUS_MORE_PROCESSING_REQUIRED,这将告诉IoCompleteRequest内核服务程序驱动程序将保留对于原先这个IRP的控制权,否则的话,内核会继续处理你已经释放了的IRP,这将会导致系统的Crash。


如果我们在操作每个视频帧的时候,不是申请又释放IRP,而是让照相机驱动程序保持一个IRP队列,然后重复的使用它们,这是一个极大的优化。完成例程会再一次的返回STATUS_MORE_PROCESSING_REQUIRED,因为驱动需要保留对于IRP的控制权。


Queuing the IRP 


大部分情况下,IRP是个很棘手的问题,驱动程序要么立即处理要么转发给另外一个驱动程序,当驱动程序必须等待处理一个IRP时,就象在USB照相机的mini驱动程序中的例子,它需要保留对这个IRP的控制权,又要立即返回,在这种情况下,驱动程序在一个私有的队列中排队IRP并且返回STATUS_PENDING。IRP有一个LIST_ENTRY位,这个位使这些IRP很容易排队。下面一部分及例程代码将介绍如何正确的处理排队问题。


图二显示了IRP队列管理的一些简单代码,后面会详细介绍。在WDM这样的多线程环境下,你必须使用自旋锁来保护好你私有的数据结构以免被其他线程修改。任何一个获取自旋锁的函数都不会被页交换出内存(后面会有解释为什么)。同时应该注意到当拥有自旋锁的时候,任何在驱动之外的函数调用都是很危险的。所谓的驱动之外的函数调用是指在另外一个驱动或内核中执行代码。


这个简单的代码是不完整的,因为它忽略了IRP可以在任何时间被取消掉。也就是说,在运行级别更高的驱动程序可以调用IoCancelIrp来强迫IRP立即完成,这个就是代码不完整的原因。首先确保被取消的IRP从你的IRP队列当中移除,或者稍后终止处理你不再拥有的IRP或者直接释放这个IRP;然后,当你插入或删除这个IRP的时候,你必须处理有关这个需要删除的IRP的所有细节问题,这个后面会继续讨论。


Figure 2   Simple and Incomplete IRP Queuing 


// the queue and spinlock should be part of each device context 


LIST_ENTRY    irpQueue; 


KSPIN_LOCK    irpQueueSpinLock; 






VOID InitIrpQueue() 





    InitializeListHead(&irpQueue); 


    KeInitializeSpinLock(&irpQueueSpinLock); 









VOID EnqueueIrp(PIRP Irp) 





    KIRQL oldIrql; 






    something is missing here! 






    KeAcquireSpinLock(&irpQueueSpinLock, &oldIrql); 


    InsertTailList( &irpQueue, &Irp->Tail.Overlay.ListEntry); 


    KeReleaseSpinLock(&irpQueueSpinLock, oldIrql); 









PIRP DequeueIrp() 





    KIRQL oldIrql; 


    PLIST_ENTRY listEntry; 


    PIRP nextIrp = NULL; 




    KeAcquireSpinLock(&irpQueueSpinLock, &oldIrql); 


    listEntry = RemoveHeadList(&irpQueue); 


    KeReleaseSpinLock(&irpQueueSpinLock, oldIrql); 


    if (listEntry){ 


        nextIrp = CONTAINING_RECORD(listEntry, IRP, Tail.Overlay.ListEntry); 


    } 




    // something is missing here! 


    return nextIrp; 


}




The Cancel Routine 


处理要删除的IRP的过程就是一个删除例程。不同于完成例程,每个驱动程序都有一个完成例程,在IRP中,一次最多只能有一个IRP的删除例程(译者注:由于一个I/O操作对应着一个IRP,因此其删除例程只能有一个),只有IRP的拥有者(译者注:某一层的驱动程序)才可以为IRP设置一个IRP删除例程。删除例程的目的就是,在删除IRP的时候,使驱动程序的数据结构保持一致,然后尽快地完成IRP。 


在IRP排队之后,想要设置一个取消IRP并完成它的取消例程不是简单的事情。IRP可以在任何时候被取消。在你的处理例程在处理IRP的时候,也许这个IRP就已经被取消掉了。同样的,在你正在把这个IRP从队列中删除的时候,它也可能被取消掉。


图三,四的简单代码中,增加了一些查询IRP是否被取消的语句。图五显示的是取消例程代码。注意在IrpCancelRoutine中获取了局部自旋锁,从队列中删除IRP,在释放全局取消锁之前释放局部自旋锁。这样做是为了防止一种极端现象,在这种情况下,在释放取消自旋锁和获取局部自旋锁这段时间,队列中的IRP被从队列中移除,然后完成,重新申请,又插入到同样队列的后面,然后又移除,这将导致在IrpCancelRoutine中的两个线程完成同一个IRP。这种比较错乱的情况在编写WDM驱动程序的时候是需要尽量避免的。










--------------------------------------------------------------------------------


Figure 3   IRP Queuing with Cancellation 


// the queue and spinlock should be part of each device context 


LIST_ENTRY    irpQueue; 


KSPIN_LOCK    irpQueueSpinLock; 






VOID InitIrpQueue() 





    InitializeListHead(&irpQueue); 


    KeInitializeSpinLock(&irpQueueSpinLock); 









NTSTATUS EnqueueIrp(PIRP Irp) 





    PDRIVER_CANCEL  oldCancelRoutine; 


    KIRQL oldIrql; 


    NTSTATUS status; 






    KeAcquireSpinLock(&irpQueueSpinLock, &oldIrql); 






    // must set a cancel routine before checking the Cancel flag 


    oldCancelRoutine = IoSetCancelRoutine(Irp, IrpCancelRoutine); 


    ASSERT(!oldCancelRoutine); 






    if (Irp->Cancel){ 


        // This IRP has already been cancelled, so complete it now. 


        // We must clear the cancel routine before completing the IRP. 


        // We must release the spinlock before calling out of the driver. 


        IoSetCancelRoutine(Irp, NULL); 


        KeReleaseSpinLock(&irpQueueSpinLock, oldIrql); 


        status = Irp->IoStatus.Status = STATUS_CANCELLED; 


        IoCompleteRequest(Irp, IO_NO_INCREMENT); 


    } 


    else { 


        // This macro sets a bit in the current stack location to indicate that 


        // the IRP may complete on a different thread. 


        IoMarkIrpPending(Irp); 






        InsertTailList( &irpQueue, &Irp->Tail.Overlay.ListEntry); 


        KeReleaseSpinLock(&irpQueueSpinLock, oldIrql); 


        status = STATUS_SUCCESS; 


    } 






    return status; 













--------------------------------------------------------------------------------


Figure 4   IRP Dequeuing with Cancellation 


PIRP DequeueIrp() 





    KIRQL oldIrql; 


    PIRP nextIrp = NULL; 






    KeAcquireSpinLock(&irpQueueSpinLock, &oldIrql); 






    while (!nextIrp && !IsListEmpty(&irpQueue)){ 


        PLIST_ENTRY  listEntry = RemoveHeadList(&irpQueue); 






        nextIrp = CONTAINING_RECORD(listEntry, IRP, Tail.Overlay.ListEntry); 


        IoSetCancelRoutine(nextIrp, NULL); 


        if (nextIrp->Cancel){ 


            // This IRP was just cancelled. 


            // The cancel routine may or may not have been called, 


            // but it doesn't matter because it will not find the IRP in the 


            // list. 


            // Must release the spinlock when calling outside the driver to 


            // complete the IRP. 


            KeReleaseSpinLock(&irpQueueSpinLock, oldIrql); 


            nextIrp->IoStatus.Status = STATUS_CANCELLED; 


            IoCompleteRequest(nextIrp, IO_NO_INCREMENT); 


            KeAcquireSpinLock(&irpQueueSpinLock, &oldIrql); 


            nextIrp = NULL; 


        } 


    } 






    KeReleaseSpinLock(&irpQueueSpinLock, oldIrql); 


    return nextIrp; 













--------------------------------------------------------------------------------


Figure 5   IrpCancelRoutine 


VOID IrpCancelRoutine(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) 





    KIRQL oldIrql; 


    PIRP firstIrp = NULL, irpToComplete = NULL; 






    // In this implementation, I don't assume that the IRP being cancelled is 


    // in the queue; 


    // I only complete the IRP if it IS in the queue. 






    KeAcquireSpinLock(&irpQueueSpinLock, &oldIrql); 






    while (!IsListEmpty(&irpQueue)){ 


        PLIST_ENTRY listEntry; 


        PIRP thisIrp; 






        listEntry = RemoveHeadList(&irpQueue); 


        thisIrp = CONTAINING_RECORD(listEntry, IRP, Tail.Overlay.ListEntry); 






        if (thisIrp == Irp){ 


            // This is the IRP being cancelled; we'll complete it after we 


            // release the spinlock 


            ASSERT(thisIrp->Cancel); 


            irpToComplete = thisIrp; 






            // keep looping so that order of the remaining IRPs is preserved 


        } 


        else { 


            // This is not the IRP being cancelled, so put it back 


            if (thisIrp == firstIrp){ 


                // finished going through the list 


                InsertHeadList(&irpQueue, listEntry); 


                break; 


            } 


            else { 


                InsertTailList(&irpQueue, listEntry); 






                if (!firstIrp){ 


                    firstIrp = thisIrp; 


                } 


            } 


        } 


    } 






    KeReleaseSpinLock(&irpQueueSpinLock, oldIrql); 






    // Finally, release the global cancel spinlock whether or not we are 


    // completing this IRP 


    IoReleaseCancelSpinLock(Irp->CancelIrql); 






    if (irpToComplete){ 


        // complete this cancelled IRP only if it was in the list 


        irpToComplete ->IoStatus.Status = STATUS_CANCELLED; 


        IoCompleteRequest(irpToComplete, IO_NO_INCREMENT); 


    } 

















很可能在获取你的局部自旋锁的时候,马上释放全局取消自旋锁,当你释放自旋锁的时候,你必须注意你正在恢复的是哪个IRQL(中断请求级)。只有在你完成的局部自旋锁的时候,你才可以恢复IRQL到Irq->CancelIrql,并且你应该在退出取消例程的时候使IRQL恢复到Irp->CancelIrql的级别。在这一步你应该避免任何的一些变动。


IRP处理函数应该使用EnqueueIrq函数来保留Irq的控制权,下面是IRP处理函数的一些代码:




NTSTATUS IrpHandlerFunction(IN PDEVICE_OBJECT DeviceObject,  


                             IN PIRP Irp) 


 { 


     NTSTATUS status; 
     status = EnqueueIrp(Irp); 


     if (NT_SUCCESS(status)){ 


         return STATUS_PENDING; 


     } 


     else { 


         // in case of failure, EnqueueIrp() completed this IRP 


         return status; 


     } 


 }


注意在EnqueueIrq函数中IoMarkIrpPending的使用,这个宏设置当前IRP堆栈空间中的位标志,使内核清楚,IRP的完成可能会在不同的线程当中完成,而不是在正在调用的本线程中。这个位直到IRP完成以后才有效,但是有一规定是在你即将为一个IRP返回STATUS_PENDING时需要调用IoMarkIrpPending。一个常见的错误就是在EnqueueIrp函数返回Success的时候调用IoMarkIrpPending。这样是不正确的,因为一旦IRP被插入队列当中,除非你获取了队列的自旋锁,否则的话你不能操作这个IRP。


当然,我这里所讨论的任何关于排队IRPs的方法同样可以应用到保留IRPs的控制权的操作。如果你返回STATUS_PENDING,但是在私有指针变量中还保留一个IRP的指针,而不是保留一个IRP队列的话,你最好能有一个自旋锁来保护这个指针,并且为这个保留的IRP设置一个取消例程,这样就可以让你在细节问题上有一个很好的认识(This can come up in some subtle ways.)。例如:假设你保留有IRP的控制权,并且把这个IRP指针以上下文的形式传递给定时函数,你必须设置一个取消例程用来取消定时器。定时器的回调函数必须获取像取消例程一样的自旋锁,并且检测IRP是否被取消了。如果IRP完成了,定时器回调函数必须首先清除取消例程,在进行这一操作的时候,需要获取自旋锁然后再释放自旋锁。


Questions on IRP Cancellation 


这个时候,你也许会问自己一个问题:如果IRP能够在任何时候被取消掉,那么对IRP操作的时候,对我来说,怎么样才算是合法的呢?如果更高一级的驱动程序可以取消和释放一个我拥有控制权的IRP,那为什么在我读取IRP空间的MajorFunction值,Cancel值等其他数据,这些对IRP操作是合法的呢?


这两个问题的答案就在于IRP可以有两种状态:可以被取消和不可以被取消。拥有取消例程则可以使IRP被取消。取消一个没有取消例程的IRP的时候,除了立即设置Cancel标志外,不会有其他的立即效果。只要这个IRP是需要立即处理的(棘手的),它不久后还是会被完成的,只有在一个驱动程序排队IRP的时候,才会无限期的推迟它的完成,那么取消这个IRP就会变成一个问题了。一个驱动程序在对IRP进行排队的时候,首先需要检测Cancel标志位,如果被设置的话,需要立即完成这个IRP。如果要取消一个已经排队的IRP的话,取消例程将会将会把它从队列中删除然后完成它,因此取消一个IRP也许不会直接完成这个IRP,只是确保这个IRP将会被完成。


以下是内核函数IoCancelIrp的伪代码:


IoCancelIrp:     acquire the global cancel spinlock     set the IRP's Cancel field to TRUE     check the IRP's CancelRoutine field         if CancelRoutine is set, clear the         CancelRoutine field, call the cancel routine,         and return TRUE;otherwise, release the cancel         spinlock and return FALSE


请注意,在IoCancelIrp函数中,如果IRP有一个取消例程,那么它不会释放全局取消自旋锁,这就是为什么在你的取消例程调用IoReleaseCancelSpinLock来释放它显得极端的重要,否则的话,全局取消自旋锁将不会被释放,而系统也将一直处在等待当中。


在你调用取消例程以后,IoCancelIrp为什么不为你释放取消自旋锁呢?调用IoCompleteRequest来完成IRP是一个驱动之外的调用。如果你的取消例程完成IRP,而内核还一直为你占有取消自旋锁,那么系统就会发生死锁现象。因此取消例程必须释放全局取消自旋锁,然后再完成取消的IRP。那么为什么IoCancelIrp没有为你释放取消自旋锁和完成IRP呢?你没有必要让IoCancelIrp来完成传递给它的IRP。在我的IrpCancelRoutine的实现当中,比如说:我只需要完成我队列中的IRP,这意味着取消例程不会访问驱动程序外面的资源,由于取消自旋锁被它所占有。让IoCancelIrp来取消IRP就会是花很大的精力原理驱动程序。


最后,好问的读者也许已经注意到了在IoReleaseCancelSpinLock函数之前应该会有一个IoAcquireCancelSpinLock函数。我们展开一下内核代码,你可以看到WDM DDK中的WDM.h的头文件。为什么你需要一个局部自旋锁来保护你的IRP队列呢?由于你的取消例程已经处理了全局取消自旋锁,为什么在EnqueueIrp和DequeueIrp中不用全局取消自旋锁来同步IRP队列呢?千万,千万,不要这样做。如果让驱动程序一直竞争全局取消自旋锁的话,将会导致系统全面地变慢。我不知道为什么驱动程序需要访问一下IoAcquireCancelSpinLock函数。为什么在每个IRP中要用一个全局取消自旋锁来代替局部取消自旋锁?因为取消自旋锁被用在内核的其他地方-----这就是我能告诉你的全部了。


Alternative IRP Queuing Implementations 


一定要确保你的插入队列/移除队列和取消IRP的代码是稳定的,有几个稳定的并且是我认为最简单的代码的实现,我已经在前面给出了他们的代码。下面是一些是它们的伪代码,其代码实现在图三中。(不包括局部自旋锁的获取和释放。) EnqueueIrp:     Set the cancel routine     Check if IRP is cancelled         If cancelled, clear the cancel routine and         complete the IRP         If not cancelled, queue the IRP  DequeueIrp:     Remove the first IRP from the queue     Clear its cancel routine     If this IRP was cancelled, complete it and loop     Otherwise, return the IRP  IrpCancelRoutine:     Release the global cancel spinlock     If the cancelled IRP is in the list, dequeue and     complete it




EnqueueIrp在把IRP插入IRP队列之前检测IRP的取消状态位,如果发现此IRP是要被取消的,那么仅仅需要立即完成这个IRP就可以了。在插入IRP队列的时候,如果只是先检测Irp->Cancel,然后再设置取消例程。仅仅这样做是有缺陷的,因为IRP在你检测完它的Irp->Cancel后可以立即变成需要被取消的IRP,这将导致你会把一个需要取消的IRP插入IRP队列当中。通过一开始就设置取消例程,竞争局部自旋锁来执行这个IRP的取消例程代码。


在IrpCancelRoutine中不会自动完成传递给它的IRP,这个函数只完成那些已经在IRP队列中的IRP。在EnqueueIrp发现IRP已经被取消的情况下,IrpCancelRoutine也许会或不会被调用。但是如果IrpCanceRoutine被调用,它就会等待获取局部自旋锁。当它得到自旋锁的时候,它在IRP队列中找不到这个IRP,那么IrpCancelRoutine就不会完成这个IRP。如果IRP已经被取消了,那么IRP的完成与否,将取决于EnqueuIrp函数,而不管IrpCancelRoutine是否被调用。这将确保IRP被完成并且不会被完成两次,有些事情你需要明确的避免其发生因为这些会导致系统崩溃。IrpCancelRoutine函数不能假设在函数被调用的时候IRP在队列当中(这样当在IRP队列中没有找到这个IRP时,系统就不会陷入死循环当中)。


DequeueIrp也是类似的情形。如果它在IRP队列头的IRP被取消,它仅仅是完成IRP然后继续,而不用担心IrpCancelRoutine是否被访问。如果IrpCancelRoutine被访问,它就会一直等待被DequeueIrp占有的局部自旋锁。由于DequueIrp已经从IRP队列中移除了IRP,IrpCancelRoutine将不会完成IRP。这样DequeueIrp可以自由完成被取消的IRP而不论IrpCancelRoutine是否被调用。


这样实现的好处就是,在EnqueueIrp或DequeueIrp发现IRP已经被取消的情况下,它不必知道IrpCancelRoutine是否被调用。因此就相关的这种IRP而言,IrpCancelRoutine就是一个nop指令。


另外一种代码的实现方式是,如果IrpCancelRoutine被访问,就让它来完成IRP。下面是这种方式实现的伪代码(再一次的忽略局部自旋锁的获取和释放)


EnqueueIrp:     Set cancel routine     Queue the IRP     Check if IRP is cancelled If cancelled, test and clear the cancel routine         If the cancel routine was not already clear,             our cancel routine was not called, so             dequeue and complete the IRP  DequeueIrp:     Remove the first IRP from the queue     Test and clear its cancel routine     If the IRP was cancelled, then         If the cancel routine was not already called,              complete the IRP Loop     Otherwise, return the IRP  IrpCancelRoutine:     Release the global cancel spinlock     Dequeue and complete the IRP passed in as an         Argument




在这里我把复杂性的代码从IrpCancelRoutine移到EnqueueIrp和DequeueIrp里面去了,因为这个版本的IrpCancelRoutine一直要完成IRP----不仅仅是IRP在队列中的情况,还有一些潜在的实现-----在完成一个被取消的IRP时EnqueueIrp和DequeueIrp必须考虑IrpCancelRoutine是否被调用。


Irp->Cancel 被设置为TURE表示这个IRP被取消了,但是它并不表示是否调用了一个取消例程来处理这个IRP。确定取消例程是否被调用可以检测先前被IoSetCancelRoutine返回的取消例程(看IoCancelIrp的伪代码)。因为可选的IrpCancelRoutine函数一直会完成IRP,而可供调用的EnqueueIrp和DequeueIrp函数只有在取消例程没有被调用的情况下才必须完成要取消的IRP。我个人不喜欢这种工作调用的方式,因为EnqueueIrp和DequeueIrp必须考虑IrpCancelRoutine是否被调用。


任何的实现这些IRP操作功能的代码在插入队列,从队列中删除,取消IRP时都必须获取局部自旋锁。你的局部自旋锁唯一要做的事情,就是在EnqueueIrp函数设置取消例程和检测Cancel位的中间这段时间(或者在检测Cancel位和排队IRP之间,这取决于你的实现代码),要保证IRP不被释放。


不管你是用什么方式实现IRP的管理,必须牢记一点,那就是IRP一半很少被取消的。因此,优化代码的重点可以放在取消IRP处理这方面。


Reusing IRPs 


在驱动程序申请IRP包内存资源的时候,也许你想重复使用这个IRP包空间,而不是重新申请一个然后又释放它。你编写的驱动程序是在这些IRPs相关驱动的顶层,在这种情况下,在你完成IRP的例程的时候应该返回一个STATUS_ MORE_PROCESSING_REQUIRED,然后内核就会阻止处理这个IRP,把它留给你的驱动程序专用。


当你把这些个复用的IRP分发到驱动堆栈的时候,需要确定你已经使用IoInitializeIrp重新初始化了这些个IRPs。这样就可以清除IRP的取消状态位,否则的话,如果这个复用的IRP之前是被取消的,并且取消状态位一直有效,那么第一个处理这个IRP的驱动程序在插入IRP队列的时候,发现这是一个需要取消的IRP,会不做任何的处理,立即完成并返回STATUS_CANCELLED。


Buffering Methods 


如果IRP发送数据或者从设备中接受数据,那么内核或驱动的初始化程序需要把数据缓存区与IRP相关联(IRP需要知道数据缓存区在哪里)。更低一层的驱动需要知道如何通过指针能够得到这个数据缓存区。


有三种数据缓存的方式可以和IRP相连接,在WDM.h中可以找到这些代码。METHOD_BUFFERED方式,数据缓存的指针是Irp->AssociatedIrp.SystemBuffer;METHOD_IN_DIRECT和METHOD_OUT_DIRECT方式,IRP有一个内存描述表(MDL),调用函数MmGetSystemAddressForMdl(Irp->MdlAddress)来获取可用的缓存区指针;METHOD_NEITHER方式,缓存区的指针是Irp->UserBuffer。


对于使用IOCTL的IRPs(IrpSp->MajorFunction==IRP_MJ_DEVICE_CONTROL,IrpSp是指向当前IRP堆栈的指针),缓存模式由IRP的控制代码(IrpSp->Parameters.DeviceIoControl.IoControlCode)的低两位决定。读写IRPs (IrpSp->MajorFunction == IRP_MJ_READ or IRP_MJ_WRITE)所使用的缓存方式由设备对象的标志位决定。DO_ BUFFERED_IO位对应着METHOD_BUFFERED,DO_DIRECT_IO位对应着METHOD_IN_ DIRECT 和 METHOD_OUT_DIRECT,如果没有被设置,那么就对应着METHOD_NEITHER模式。


在WDM模型中,I/O操作的输入输出由完成IRP的相关驱动程序来决定。因此,如果你调用在照相机堆栈中的下层驱动来读入一个视频帧,并且你想使用直接缓冲区模式,可以直接使用METHOD_OUT_ DIRECT模式,因为这同时也可以传递IOCTL IRP缓存区长度。如果调用者正在发送数据给设备,相应的驱动程序就会接收到调用者传递过来的数据,同时,数据的长度也被IrpSp->Parameters.DeviceIoControl.InputBufferLength传递了过来。如果调用者从设备中读取数据,在低下驱动程序给调用者传递数据的时候,数据的长度(缓存区的长度)也通过IrpSp->Parameters. DeviceIoControl.OutputBufferLength被传递给调用者。我认为这是一很直观的。


Locked Versus Pageable Code 


大部分驱动程序代码与数据都可以被锁定在内存或被交换出内存。如果被锁定的话,一旦驱动程序被装载,就将一直驻留在内存中。如果可以被交换的话,则代码和数据可以被内存管理器从内存中交换到硬盘当中,如此为其他程序留出内存空间。一般来说驱动程序都是被锁定的,除非你设置它们为可交换的。设置部分驱动代码为可交换的是很有效的(conscientious)一件事情,特别是当你的驱动程序装载入内存,但是并不马上运行的时候。(比如:照相机可以被插入槽中但不立即工作)。虽然说这样做有时候是很有效的一件事情,但是在你把它们设置为可交换的时候,你也将面临着一些危胁系统的情况,因此在这样做之前,你最好能够理解下面所介绍的所有内容。


在任何时候,一个线程都运行在一个特定的IRQL上。一般的级别是PASSIVE_LEVEL(0)。中断处理例程(ISRs)运行在IRQL>2级别上,派发例程(DPCs)运行在DISPATCH_LEVEL(2)上。内核函数的完成例程可以运行在APC_LEVEL(1)上。最需要注意的就是页缺失故障运行在IRQL>=DISPATCH_LEVEL会导致系统的Crash。因此只有运行在IRQL<DISPATCH_LEVEL的函数可以设置为可交换的。获取自旋锁意味着其运行的IRQL会提高,如果函数占有自旋锁的话,其下面的代码将会运行在更高一级的IRQL上,这个函数以及它所操作的数据必须保留在内存中(被锁定)。


如果函数不是ISR或DPC的话,并且一旦有占有自旋锁的情况它将停止运行(即不会被占有自旋锁的代码所调用,自己也不会获取自旋锁),那么你可以把你的驱动程序设置为可交换的。如果你使用MS的VC++编译器的话,你可以使用下面的预编译指令设置可交换的代码,为了测试它用下面的代码来验证你的可交换函数的IRQL。同样要记住锁住在IRQL>=DISPATCH_LEVEL被操作的数据。#ifdef ALLOC_PRAGMA 


     #pragma alloc_text(PAGE, MyPageableFunction) 


 #endif 




 VOID MyPageableFunction (…) 


 { 


     ASSERT(KeGetCurrentIrql() < DISPATCH_LEVEL); 


}






Using the Device Queue 


内核提供了一些封装函数(IoStartPacket,IoStartNextPacket等),这些内核函数通过在设备对象中排队IRPs来为你进行一些顶层IRP的处理工作。使用设备的队列会限制你的驱动程序进行半双工的传输操作,也就是说,由于在设备对象中,某一时刻只有一个队列并且只有不超过一个IRP作为当前的IRP,因此你的驱动程序在所给的设备对象中只能处理一个IRP。设备队列函数在MSDN中有详细的文档,这里我就不多讲了。有一点重要的就是,如果你的驱动程序使用设备队列,那么取消例程必须把需要取消的IRP从设备对象的CurrentIrp移除掉。


另外,注意设备队列函数使用全局取消自旋锁来同步设备对象的IRP队列操作。前面我们提到过,在常规性的代码调用(regularly traversed code paths)中(除了IRP的取消处理)使用全局取消自旋锁会使系统变得很慢。


Unsupported IRPs 


如果你的驱动程序没有处理的IRP被发送给你的派发例程,你应该做以下两件事中的其中一中。


1.如果你的驱动程序位于驱动堆栈的中间,你应该只是把这个IRP简单的传递给下层的驱动程序。


IoCopyCurrentIrpStackLocationToNext(Irp); return IoCallDriver(deviceObject, Irp);


2.如果你的驱动程序在驱动堆栈的底部(这意味着你的驱动需要实现一些总线的操作),      你应该以默认的状态去完成这个IRP,这种默认的完成方式就是STATUS_ NOT_SUPPORTED。     这种默认的状态有可能会被这个IRP的上一层的驱动所改变。为了让上一层的驱动改变一个你驱动程序     所不支持的IRP的默认状态,你应该在你的派发例程中仅仅返回默认的状态,而不做任何的处理,如下所示: status = Irp->IoStatus.Status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; 




编写一个稳定运行的WDM样本例程是很简单的一件事,难得是编写在比较复杂情况下的稳定WDM程序。下面是编写WDM驱动时的Tips,这些是很有用的,都经过我验证的。


WDM IRP-Handling Tips


(在IRP的处理函数中,保留IRP的控制权,并返回STATUS_PENDING) 
(在完成例程中保留IRP的控制权,并返回STATUS_MORE_PROCESSING_REQUIRED) 
(如果你在你的完成例程中释放了IRP的话,也请返回STATUS_MORE_PROCESSING_REQUIRED) 
(如果你为一个IRP排队(不管这个IRP是否完成),你一定要设置取消例程) 
(在设置取消例程之后,马上检测还没有被取消的IRP) 
(在你占有局部自旋锁后,调用驱动程序的外部代码来完成被取消的IRP之前,不管取消例程是否已经完成IRP操作,你的取消例程必须调用IoReleaseCancelSpinLock来释放全局取消自旋锁。) 
(只有取消例程需要释放取消自旋锁。如果你发现你排队的IRP已经被取消了,而你又没有设置取消例程,那么IoCancelIrp会为你释放取消自旋锁,IoCancelIrp要么调用取消例程,要么释放取消自旋锁,它不会两个都为你做的。) 
(当从队列中移除IRP时,Irp->Cancel位的TURE表示这个IRP已经被取消,并不意味着已经调用了你的取消例程。要调用你的取消例程,可以使用IoSetCancelRoutine(Irp,NULL)来测试或清除取消例程),如果返回值为NULL,那么取消例程被调用,否则取消例程没有被调用。 
(当一直占有局部自旋锁的时候,在为IRP返回一个STATUS_PENDING时,调用IoMarkIrpPending。) 
如果你要重复使用一个IRP时,在使用前一定要调用IoInitializeIrp,这个函数可以清除Cancel位。 
在占有自旋锁的时候,不要访问驱动程序之外的代码。 
只有运行在IRQL<DISPATCH_LEVEL的函数代码可以被设置为可页交换的。函数任何一条语句,如果在运行的时候占有自旋锁的话,那么这些代码会在更高一级的IRQL上运行,那么这些代码和相关的数据都必须是不能被交换的(锁定在内存里面)。 
对于不支持的IRPs,你的派发例程要么把这个IRP传递给下一个驱动,要么不改变默认状态的完成它。




本文来自CSDN博客,转载请标明出处:file:///F:/新建文件夹/WDM对设备驱动I-O请求管理的简化%20-%20chuajiang的专栏%20-%20CSDN博客.mht
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值