让您的驱动程序一次处理多个 IO 请求

    技术2022-05-19  30

    让您的驱动程序一次处理多个 I/O 请求

    您的用户模式应用程序向您的驱动程序发送大量 I/O 请求,但是驱动程序坚持一次处理一个请求。问题是什么?

    您可能认为驱动程序以一些模糊的方式发生阻塞或者应用程序中需要更多线程,但是解决方法经常简单得多:确保您的应用程序已经打开用于重叠 I/O 的设备。否则,I/O 管理器通过同步(使用文件对象中的锁)在调度 IRP 之前将 I/O 请求序列化。即使您的应用程序使用多个线程,一次(每个文件句柄)也只有一个请求可以通过。

    在应用程序中启用重叠 I/O要在应用程序中启用重叠 I/O,请将 dwFlagsAndAttributes 设置为 FILE_FLAG_OVERLAPPED(当您调用 CreateFile 来打开设备时)。(如果您跳过这个步骤,那么您不会得到重叠 I/O,即使其他方面正确无误。)

    CreateFile 返回一个可以用来访问设备的句柄。当您使用 CreateFile 创建的文件句柄来调用 ReadFile、WriteFile 或 DeviceIoControl 时,提供一个 OVERLAPPED 结构(包含一个事件对象的句柄,该事件对象在操作完成时发出)的指针。对于重叠操作,这些函数会立即返回;操作完成时发出事件对象。

    确保在函数调用中使用 OVERLAPPED 结构之前将其初始化为零,并且只设置函数需要的成员。例如,应该为 ReadFile 和 WriteFile 调用设置 Offset 成员,但是对于 DeviceIoControl,这个成员应该是零。而且,确保为每个请求使用单独的 OVERLAPPED 结构;在前面的异步操作完成之前重用这个结构会导致错误。此外,您的应用程序将需要原始结构为请求调用 HasOverlappedIoCompleted 或 GetOverlappedResult。

    打开用于重叠 I/O 的设备之后,可以简单地忽略 OVERLAPPED 结构来获得同步 I/O 吗?不可以,您必须为使用该句柄的所有函数调用(读、写或设备控制)提供一个 OVERLAPPED 结构。传递 NULL 会导致未定义的行为,即使驱动程序同步地完成请求。对于同步 I/O,只要应用程序等待 I/O 在返回之前完成,那么就可以在堆栈上声明 OVERLAPPED 结构。

    下列代码片断显示如何打开一个用于写重叠 I/O 的文件:

    #include <windows.h>#include <stdio.h> HANDLE hFile;  hFile = CreateFile(TEXT("myfile.txt"),     // file to create                   GENERIC_WRITE,          // open for writing                   0,                      // do not share                   NULL,                   // default security                   CREATE_ALWAYS,          // overwrite existing                   FILE_ATTRIBUTE_NORMAL | // normal file                   FILE_FLAG_OVERLAPPED,   // asynchronous I/O                   NULL);                  // no attr. template if (hFile == INVALID_HANDLE_VALUE) {     printf("Could not open file (error %d)/n", GetLastError());    return 0;}

    下列代码片断设置 OVERLAPPED 结构,调用 ReadFile,然后检查 I/O 请求的状态:

    OVERLAPPED gOverlapped; // set up overlapped structure fieldsgOverLapped.Offset     = 0; gOverLapped.OffsetHigh = 0; gOverLapped.hEvent     = hEvent;  // verify that sizeof(inBuffer >= nBytestoRead) // attempt an asynchronous read operationbResult = ReadFile(hFile, &inBuffer, nBytesToRead, &nBytesRead,     &gOverlapped) ;  // if there was a problem, or the async. operation's still pending ... if (!bResult) {     // deal with the error code     switch (dwError = GetLastError())     {         case ERROR_HANDLE_EOF:         {             // we have reached the end of the file             // during the call to ReadFile              // code to handle that         }          case ERROR_IO_PENDING:         {             // asynchronous i/o is still in progress              // do something else for a while             GoDoSomethingElse() ;              // check on the results of the asynchronous read             bResult = GetOverlappedResult(hFile, &gOverlapped,                 &nBytesRead, FALSE) ;              // if there was a problem ...             if (!bResult)             {                 // deal with the error code                 switch (dwError = GetLastError())                 {                     case ERROR_HANDLE_EOF:                     {                         // we have reached the end of                        // the file during asynchronous                        // operation                    }                      // deal with other error cases                 }   //end switch (dwError = GetLastError())              }         } // end case          // deal with other error cases, such as the default      } // end switch (dwError = GetLastError())  } // end if

    在驱动程序中处理重叠 I/O从驱动程序的角度来看,所有 I/O 请求都应该被视为异步。驱动程序不需要检查 I/O 请求是否真的异步,它应该简单地假设 I/O 请求异步并避免阻塞任何 I/O 请求。(驱动程序可以出于诸如获取锁等其他原因而阻塞,但是它不应该只是基于接收 I/O 请求而阻塞。)如果应用程序需要使用同步 I/O,那么它简单地打开设备而不指定重叠 I/O;然后 I/O 管理器如前所述将请求序列化,驱动程序方面无需任何特殊操作。

    当 I/O 管理器从应用程序接收 I/O 请求时,I/O 管理器创建一个 IRP 并调用您的驱动程序的调度例程。调度程序的角色是将请求发给驱动程序进行处理,不必自己处理所有过程。如果您的驱动程序不在调度例程中完成 IRP,那么调用 IoMarkIrpPending 并返回 STATUS_PENDING。(请记住,如果您通过调用 IoMarkIrpPending 将 IRP 标记为挂起,那么可以返回的唯一的状态码是 STATUS_PENDING。不得返回任何其他状态码。但是,您可以将 IRP 标记为挂起,同步完成它,并返回 STATUS_PENDING。)

    例如,考虑应用程序发送的 ReadFile 请求。在从 I/O 管理器接收到 IRP_MJ_READ IRP 时,您的调度例程可以简单地将 IRP 标记为挂起,调用 IoStartPacket,然后返回 STATUS_PENDING。(这里顺序很重要。如果您首先将请求放到队列中,那么它可以在第一个线程考虑将其标记为挂起之前被驱动程序的另一部分使用、处理、完成并释放。这将干扰 I/O 系统,并且还将在您试图在已经释放的内存池块的指针上调用 IoMarkIrpPending 时导致系统崩溃。

    您的 StartIo 例程通过发送命令到控制器来开始操作。当控制器完成命令时,您的中断服务例程 (ISR) 被发出。它运行并排队一个延迟的过程调用 (DpcForIsr)。DpcForIsr 将合适的状态值放到 IRP 中并调用 IoCompleteRequest 来真正完成操作。DpcForIsr 然后调用位于处理 DpcForIsr 的线程上下文中的 IoStartNextPacket(在这里您的 StartIo 例程将被再次调用)来启动另一个请求。

    同时,一旦您的驱动程序的调度例程返回,控制就返回到应用程序,因此应用程序不会在等待驱动程序完成 I/O 请求时发生阻塞。来自 ReadFile 的状态指示请求正在进行中(ReadFile 返回零,GetLastError 返回 ERROR_IO_PENDING),因此应用程序可以继续进行其他工作(可能发送更多 I/O 请求)。您的驱动程序调用 IoCompleteRequest 之后,应用程序被通知操作已经完成(通过 OVERLAPPED 结构中指定的事件对象),它可以检查该结构以获取状态信息。

    最后一点:如果您挂起 IRP,那么您应该支持 I/O 取消,从而调用方可以取消 I/O 请求(如果它将耗时过长)。最好使用可以安全取消的 IRP 排队例程(IoCsqXxx 例程)(特别是如果您的驱动程序频繁执行 I/O),因为这些例程提供一个正确处理取消的框架从而使得竞争条件不会发生。Windows DDK(%winddk%/src/general/cancel)中的 Cancel 例子展示了这些例程的使用:

    否则,您将需要在您的驱动程序中实现一个 Cancel 例程并在调用 IoStartPacket 时给该例程传递一个指针,因此 I/O 管理器将在 IRP 处于可取消状态时调用您的 StartIo 例程。在您的 StartIo 例程中,您将需要检查取消并防止相关的竞态条件。这种方法不推荐用于频繁执行 I/O 的驱动程序,因为 I/O 管理器在从设备队列插入和删除 IRP 时持有全局取消自旋锁,这会影响系统性能。

    请参阅WHDC 上的“Windows 驱动程序中的取消逻辑” 以获取这些和其他 IRP 取消技巧的详细讨论。

    您应该做什么?

    在您的应用程序中:

    当调用 CreateFile 来打开设备时,将 dwFlagsAndAttributes 设置为 FILE_FLAG_OVERLAPPED。

    当调用 ReadFile、WriteFile 或 DeviceIoControl 时,提供一个正确初始化的 OVERLAPPED 结构的指针。使用以 FILE_FLAG_OVERLAPPED 打开的设备的句柄时,决不要忽略 OVERLAPPED 结构。

    决不要为随后的请求重用 OVERLAPPED 结构。

    在您的驱动程序中:

    假设所有传入的 I/O 请求都是异步的。

    除非您在调度例程中完成 IRP,否则应该将 IRP 标记为挂起,从而应用程序不会被阻塞来等待请求完成。如果您将 IRP 标记为挂起,请记住只返回 STATUS_PENDING。

    支持 IRP 取消,首选使用可以安全取消的 IRP 排队例程 (IoCsqXxx),从而使得应用程序可以取消花费时间过长的 I/O 请求。

    更多信息:

    Platform SDK:存储创建和打开文件同步和重叠的输入输出

    WHDC处理 IRP:每个驱动程序编写人员都需要知道的技巧Windows 驱动程序中的取消逻辑可以安全取消的 IRP 排队的控制流程

    Windows Driver Kit (WDK):内核模式驱动程序体系结构处理 IRP


    最新回复(0)