IRP的同步完成与异步完成

应用程序中对设备进行同步、异步操作,都必须得到驱动程序的支持。所有对设备的操作都会转化为IRP请求,并传递到相应的派遣函数中。可以有两种方式处理IRP请求,第一种是在派遣函数中直接结束IRP请求,即同步处理。另一种方法是在派遣函数中不结束IRP请求,而是让派遣函数直接返回。IRP在以后的某个时候再进行处理。

1)IRP的同步完成

   在应用程序中调用CreateFile win32API函数,这个函数用于打开设备。CreateFile Win32 API函数内部调用了ntdll.dll中的NtcreateFile函数。ntdll.dll中的NtCreateFile函数进入内核模式,然后调用ntoskrnl.exe中的NtCreateFile函数。内核模式中ntoskrnl.exe的NtCreateFile函数创建IRP_MJ_CREATE类型的IRP,然后调用相应驱动程序的派遣函数,并将IRP的指针传递给该派遣函数。派遣函数调用IoCompleteRequest,将IRP请求结束。操作系统按照原路返回,一直退到CrateFile Win32API函数。至此CreateFile函数返回。如果需要读取设备,应用程序应该调用ReadFile Win32 API函数。ReadFile Win32 API函数会调用ntdll.dll中的NTReadFile函数。ntdll.dll中的NtReadFile函数会进入内核模式,调用ntoskrnl.exe中的NtReadFile函数。ntoskrnl.exe中的NtReadFile函数创建IRP_MJ_READ类型的IRP,并将其传入相应的派遣函数中。

   对设备进行读取可以有三种方法:1)ReadFile函数同步读取。ReadFile函数内部会创建一个事件,这个事件连同IRP一起被传递到派遣函数中(这个事件是IRP的UserEvent子域)。派遣函数调用IoCompleterequest时,IocompleteRequest内部会设置IRP的userEvent事件。操作系统按照原路一直返回到ReadFile函数,ReadFile函数会等待这个事件。因为该事件已经被设置,所以无须等待。如果在派遣函数中没有调用IoCompleteRequest函数,该事件就没有被设置,ReadFile会一直等IRP被结束。2)如果是用ReadFile函数进行异步读取时,ReadFile内部不会创建事件,但ReadFile函数会接受Overlap参数。overlap参数中会提供一个事件,这个事件被用作同步处理。IoCompleteRequest内部会设置overlap提供的事件。在ReadFile退出前,它不会检测该事件是否被设置,因此可以不等待操作是否真的被完成。当Irp操作被完成后,overlap提供的事件被设置,这个事件会通知应用程序IRP请求被完成。3)如果是用ReadFileEx函数进行异步读取中,ReadFileEx不提供事件,当提供一个回调函数,这个回调函数的地址会作为IRP的参数传递给派遣函数。IoCompleteRequest会将这个完成函数插入APC队列。应用程序只要进入警惕模式,APC队列会自动出队列,完成函数被执行,这相当于通知应用程序操作已经完成。

 

2)IRP的异步完成 

   IRP被“异步完成”指的就是不在派遣函数中调用IoCompleteRequest内核函数。调用IoCompleteRequest函数意味着IRP请求的结束,也标志着本次对设备操作的结束。

 IRP被异步完成,而发起IRP的应用程序会有三种形式发起IRP请求,分别使用ReadFile函数同步设备,用ReadFile异步读取设备,用ReadFileEx异步读取函数。1)IRP是由ReadFile的同步操作引起的:当派遣函数退出时,由于IoCompleteRequest没有被调用,IRP请求没有被结束。ReadFile会一直等待,直到操作被结束。2)IRP是由ReadFile的异步操作引起的:当派遣函数退出时,由于IoCompleteRequest没有被调用,IRP请求没有被结束。但ReadFile会立刻返回,返回值为失败,但代表操作没有完成。通过调用GetLastError函数,可以得到这时的错误代码是ERROR_IO_PENDING。这不是真正的操作错误,而意味着ReadFile并没有真正完成操作,ReadFile只是异步的返回。当IRP请求被真正的结束,即调用了IoCompleteRequest,ReadFile函数提供的overlap的事件才会被设置。这个事件可以通知应用程序ReadFile的请求真正的被执行完毕。3)IRP是由ReadFileEx的异步操作引起:和ReadFile的异步操作类似,ReadFileEx会立即返回,但返回值是FALSE,说明读操作没有成功。这时候如果调用GetLastError函数,会发现错误码为Error_IO_PENDING,表明当前操作被“挂起”。当IRP被结束后,即调用了IoCompleteRequest后,ReadFileEx提供的回调函数被插入到APC队列中。一旦操作系统进入警惕状态时,线程的APC队列会自动出队列,进而ReadFileEx提供的回调函数被调用,这相当于操作系统通知应用程序操作真正的被执行完毕。

如果派遣函数不调用IoCompleteRequest函数,则需要告诉操作系统此IRP处于挂起状态。这需要调用内核函数IoMarkIrpPending。同事,派遣函数应该返回STATUS_PENDING。例如:

NTSTATUS HelloDDKRead( IN PDEVICE_OBJECT  pDevObj,

                                          IN PIRP pIrp)

{

  //……

  IoMarkIrpPending(pIrp);

  return STATUS_PENDING;

}

 

为了演示异步处理IRP,假设IRP_MJ_READ的派遣函数仅仅是返回挂起。应用程序关闭设备的时候会产生IRP_MJ_CLEANUP类型的IRP。在IRP_MJ_CLEANUP的派遣函数中结束那些挂起的IRP_MJ_READ。

为了能存储有哪些IRP_MJ_READ IRP被挂起,这里使用一个队列,也就是把每个挂起的IRP_MJ_READ的指针都插入队列,最后IRP_MJ_CLEANUP的派遣函数将一个个IRP出队列,并且调用IoComleteRequest函数将它们结束。

typedef struct _MY_IRP_ENTRY

{

   PIRP   pIRP;

   LIST_ENTRY   ListEntry;

} MY_IRP_ENTRY, *PMY_IRP_ENTRY;

  在设备扩展中加入“队列”这个变量,这样驱动程序的所有派遣函数都可以使用该队列。在driverEntry中初始化该队列,并在driverunload例程中回收队列。在IRP_MJ_READ的派遣函数中,将IRP插入堆栈,然后返回“挂起”状态。

 

NTSTATUS HelloDDkRead( IN PDEVICE_OBJECT  pDevObj,

 

 

 

                                          IN PIRP pIrp)

{

   kdPrint("Enter HelloDDKRead/n");

   PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;

   PMY_IRP_ENTRY pIrp_Entry = (PMY_IRP_ENTRY) ExAllocatePool(PagedPool, sizeof(MY_IRP_ENTRY));

   pIrp_Entry->pIrp = pIrp;

   //插入队列

   InsertHeadList(pDevExt->pIRPLinkListHead, &pIrp->entry->listEntry);

   //将IRP设置为挂起

   IoMarkIrpPending(pIrp);

   KdPrint("Leave HelloDDkRead/n");

   return STATUS_PENDING;

}

 

   在关闭设备的时候,会产生IRP_MJ_CLEANUP类型的IRP。其派遣函数抽取队列中每一个挂起的IRP,并调用IoCompleteRequest设置完成。

  NTSTATUS HelloDDKCleanUp(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)

{

  kdPrint("Enter HelloDDKCleanUp/n");

  PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;

 

  //将存在队列中的IRP逐个出队列,并处理

  PMY_IRP_ENTRY my_irp_entry;

while(!IsListempty(pDevExt->pIRPLinkListHead))

{

    //删除队列元素

    PLIST_ENTRY pEntry = RemoveHeadList(pDevExt->pIRPLinkListHead);

    //得到元素入口

    my_irp_entry = CONTAINING_RECORD(pEntry, MY_IRP_ENTRY, ListEntry);

    //设置IRP完成状态

    my_irp_entry->pIRP->IoStatus.Status = STATUSS_SUCCESS;

    my_irp_entry->pIRP->IoStatus.Information = 0;

    IoCompleteRequest( my_irp_entry->pIRP, IO_NO_INCREMENT );

    //回收内存

    ExFreePool(my_irp_entry);

}

//处理IRP_MJ_CLEANUP的IRP

  NTSTATUS status = STATUS_SUCCESS;

  pIrp->IoStatus.Status = status;

  pIrp->IoStatus.Information = 0;

  IoCompleteRequest( pIrp, IO_NO_INCREMENT );

  KdPrint("Leave HelloDDkCleanUp/n");

  return STATUS_SUCCESS;

}

 

在应用程序中异步操作该设备,先异步读两次,这样会创建两个IRP_MJ_READ,这两个IRP被插入队列。在关闭设备的时候,会导致驱动程序调用IRP_MJ_CLEANUP的派遣函数。

int main()

{

   HANDLE hDevice = CreateFile("////.//HelloDDK",

                                                  GENERIC_READ | GENERIC_WRITE,

                                                  0,         

                                                  NULL,

                                                  OPEN_EXISTING,

                                                  FILE_ATTRIBUTE_NOMAL|FILE_FLAG_OVERLAPPED,

                                                  NULL);

   if(hDevice == INVALID_HANDLE_VALUE)

  {

     printf("Open Device Failed!");

     return 1;

  }

   OVERLAPPED overlap1 = {0};

   OVERLAPPED overlap2 = {0};

   UCHAR buffer[10];

   ULONG ulRead;

   BOOL bRead = ReadFile(hDevice, buffer, 10,&ulRead, &overlap1);

   if(!bRead && GetLastError() == ERROR_IO_PENDING)

   {

       printf("The Operation is pending/n");

   }

    bRead = ReadFile(hDevice, buffer, 10, &ulRead,&overlap2);

   if(!bRead && GetLastError() == ERROR_IO_PENDING)

   {

       printf("The Operation is pending/n");

   }

  

   //迫使程序中止2秒

   Sleep(2000);

   //创建IRP_MJ_CLEANUP   IRP

   CloseHandle(hDevice);

   return 0;

}

 

  还有另外一个办法可以将“挂起”的IRP逐个结束,这就是取消IRP请求。内核函数IoSetCancelRoutine可以设置取消IRP请求的回调函数,其声明如下:

  PDRIVE_CANEL  IoSetCancelRoutine( IN PIRP Irp, IN PDRIVER_CANEL CancelRoutine);

IoSetCancelRoutine可以将一个取消例程与该IRP关联,一旦取消IRP请求的时候,这个取消例程会被执行。IoSetCancelRoutine函数可以用来删除取消例程,当输入的CancelRoutine参数为空指针的时候,则删除原来设置的取消例程。

  程序员可以用IoCancelIrp函数指定取消IRP请求。在Io CancelIrp内部,需要进行同步。DDK在IoCancelIrp内部使用一个叫做cancel的自旋锁用来进行同步。

  IoCancelIrp在内部会首先获得该自旋锁,IoCancelIrp会调用取消回调例程,因此,释放该自旋锁的任务就留给了取消回调例程。获得取消自璇的函数是IoAcquireCancelSpinLock函数,而释放取消自旋锁的函数是IoReleaseCancelSpinLock函数。

  在应用程序中,可以调用Cancel Win32 API函数取消IRP请求。在CancelIo的内部会枚举所有没有被完成的IRP,然后依次调用IoCancelIrp。另外,如果应用程序没有调用CancelIo函数,应用程序在关闭设备时同样会自动调用CancelIo。

 VOID  CancelReadIRP( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)

{

       KdPrint("Enter CancelReadIRP/n");

   

       PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;

       Irp->IoStatus.Status = STATUS_CANCELLED;

       //结束IRP操作字节数

       Irp->IoStatus.Information = 0;

       IoCompleteRequest(Irp, IO_NO_INCREMENT);

       //释放cancel自旋锁

       IoReleaseCancelSpainLock(Irp->CancelIrql);

       KdPrint("Leave CancelReadIRP/n");

}

 

NTSTATUS HelloDDkRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)

{

     KdPrint("Enter HelloDDkRead/n");

     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;

     IoSetCancelRoutine(pIrp, CancelReadIRP);

     IoMarkIrpPending(pIrp);

     kdPrint("Leave HelloDDkRead/n");

     return STATUS_PENDING;

}

int main()

{

   HANDLE hDevice = CreateFile("////.//HelloDDK",

                                                  GENERIC_READ | GENERIC_WRITE,

                                                  0,         

                                                  NULL,

                                                  OPEN_EXISTING,

                                                  FILE_ATTRIBUTE_NOMAL|FILE_FLAG_OVERLAPPED,

                                                  NULL);

   if(hDevice == INVALID_HANDLE_VALUE)

  {

     printf("Open Device Failed!");

     return 1;

  }

   OVERLAPPED overlap1 = {0};

   OVERLAPPED overlap2 = {0};

   UCHAR buffer[10];

   ULONG ulRead;

   BOOL bRead = ReadFile(hDevice, buffer, 10,&ulRead, &overlap1);

   if(!bRead && GetLastError() == ERROR_IO_PENDING)

   {

       printf("The Operation is pending/n");

   }

    bRead = ReadFile(hDevice, buffer, 10, &ulRead,&overlap2);

   if(!bRead && GetLastError() == ERROR_IO_PENDING)

   {

       printf("The Operation is pending/n");

   }

  

   //迫使程序中止2秒

   Sleep(2000);

   //显示调用CancelIo,其实在关闭设备时会自动运行CancleIo

   CancelIo(hDevice);

   //创建IRP_MJ_CLEANUP   IRP

   CloseHandle(hDevice);

   return 0;

 

}

   在设置取消例程中要注意同步问题是,当退出取消例程时,一定要释放Cancel自旋锁,否则会导致系统崩溃。另外,cancel自旋锁是全局锁,所有驱动程序都会使用这个自旋锁,因此,占用自旋锁时间不宜过长。

 

     

你可能感兴趣的:(IRP的同步完成与异步完成)