干净快速地取消 I/O 操作






4.84/5 (101投票s)
2007年2月12日
6分钟阅读

45589

696
本文介绍了干净快速地取消 I/O 操作的方法
引言
本文介绍了从 Windows 用户模式模块(应用程序、DLL 等)干净快速地取消 I/O 操作的方法。这适用于执行 I/O 操作(例如通过 `ReadFile`、`WriteFile`、`fwrite` 等进行磁盘读/写)、设备特定 IOCTL 等的组件。
问题
当一个正在执行 I/O 操作或等待挂起 I/O 操作的应用程序被终止或关闭时,它不会立即退出(即使其 GUI 消失)。在所有 I/O 操作被取消或完成之前,应用程序仍将运行。但这并不是最终用户关闭应用程序主窗口时预期的行为(最终用户期望应用程序立即退出)。此外,系统关机/重启也会因为这些应用程序而花费更长的时间(因为操作系统会等待这些应用程序终止一段时间)。
到目前为止(Windows 2003 及更早版本),I/O 操作只能由启动它的同一个线程取消(使用 `CancelIo` API)。因此,在应用程序退出期间,很难从单个线程(终止处理程序)取消所有未完成的 I/O 操作。
到目前为止的解决方案是:在应用程序退出期间,所有线程都需要被通知(使用事件或其他方式),并且在收到此类通知后,每个线程(它将等待异步 I/O 操作完成)将使用 `CancelIo` API 取消 I/O 操作。在所有线程完成取消后,应用程序退出例程将返回。但是这个解决方案仍然存在一个缺陷:在应用程序退出期间,如果任何线程正在等待同步 I/O 操作的完成(例如,它调用了 `ReadFile`、`WriteFile` 同步操作),那么退出例程无法取消此类同步 I/O 操作——它必须等到 I/O 操作完成。
在 DLL 的情况下(在应用程序退出期间,所有已加载 DLL 的 `DllMain()` 都将被调用以处理未完成操作的取消),即使是异步 I/O 取消解决方案(如上所述)也是不可能的。这是因为,当 `DllMain()` 被调用时,DLL 创建的其他线程不会运行(由于内置的互斥机制)。因此,当 `DllMain()` 在应用程序退出期间以参数 `DLL_PROCESS_DETACH` 被调用时,即使它通知其他线程(通过事件或其他方式),其他线程也不会运行,因此任何未完成的 I/O 操作都不会被取消。
建议解决方案
本文(以及代码)描述了如何确保应用程序(甚至 DLL)可以取消所有当前未完成的 I/O 操作(同步和异步),并尽快退出,而且这种取消以更干净的方式发生。
所提议解决方案的优点
如果应用程序实现本文描述的解决方案,它们将确保所有未完成的 I/O 操作以干净的方式取消,并且应用程序一旦被最终用户终止或关闭,就会尽快退出,从而提高用户响应能力,并有助于最大限度地减少系统关机/重启所需的时间。
背景
设备 I/O(输入/输出)操作是操作系统中所有应用程序的心脏和大脑。许多应用程序的最终目标是读取设备或写入设备(设备可以是任何东西,扬声器、视频、硬盘等)。应用程序调用 Windows 提供的 API(例如 `ReadFile`、`WriteFile` 等)来执行设备 I/O 操作。
如何实现所提议的解决方案?
该解决方案的基本思想是利用 Windows Vista/Longhorn 中引入的新 API,即 CancelSynchronousIo 和 CancelIoEx。
在理解如何使用这些 API 之前,让我们首先了解 I/O 操作的类型。在发出 I/O 请求时,应用程序有两种选择;I/O 操作是同步的还是异步的。顾名思义,同步 I/O 操作 API(`ReadFile`、`WriteFile` 等)只有在 I/O 操作完成后才会返回给调用者,而异步 I/O 操作 API(`ReadFileEx`、`WriteFileEx` 等)在 I/O 操作启动时会立即返回(稍后,应用程序需要检查异步 I/O 操作是否完成)。
也就是说,应用程序可以根据其要求设计为使用同步 I/O 或异步 I/O,甚至两者都使用。该解决方案将解决所有类型 I/O 操作的取消问题。
取消未完成 I/O 操作的解决方案需要在应用程序退出时执行的例程中实现(例如:`WM_CLOSE` 消息处理程序或在 DLL 中收到 `DLL_PROCESS_DETACH` 时在 `DllMain()` 中)。
为了取消同步 I/O 操作,Vista/Longhorn 中引入了 `CancelSynchronousIo` API。此 API 将线程句柄作为其参数,并使用它取消该线程当前正在进行的同步 I/O 操作。现在,有两种方法可以检索此应用程序创建的所有线程的线程句柄。
- 应用程序可以维护一个全局变量,其中包含所有已创建线程的句柄列表(具体来说,任何可能执行 I/O 操作的线程)。现在,在取消期间,可以遍历此全局列表以检索所有线程的句柄。
- 在取消 I/O 操作期间,我们可以使用 `CreateToolhelp32Snapshot`、`Thread32First`、`OpenThread`、`Thread32Next` 等 API 枚举与此应用程序相关的所有线程(并检索线程句柄)(有关这些 API 的用法,请参见代码)。
现在,在检索到此应用程序所有线程的线程句柄后,可以为每个线程调用 `CancelSynchronousIo` API(注意:此 API 将在将 I/O 操作标记为已取消后立即返回)。请参阅代码。
要取消异步 I/O 操作,机制有点复杂。应用程序需要维护一个设备句柄列表,这些设备正在执行 I/O 操作。每当应用程序打开一个设备进行异步 I/O 操作(例如,使用 `CreateFile`、`OpenFile` 等)时,它必须更新设备句柄的全局列表(并在使用 `CloseHandle` API 关闭设备时删除该句柄)。
现在,在取消 I/O 操作期间,应用程序需要遍历设备句柄列表,并对列表中找到的每个句柄调用 `CancelIoEx` API(请参阅代码)。
通过执行上述算法,应用程序可以确保所有未完成的 I/O 操作(同步和异步)都将快速、干净地取消,从而提高终止时的响应能力。
使用代码
代码中包含一个名为 _CleanCancelIOs.cpp_ 的文件,其中包含名为 `CancelAllIOs()` 的例程。要使用它,首先将此文件添加到您的应用程序项目中。现在,在应用程序的终止处理程序中(`WM_CLOSE` 消息处理程序或在 DLL 中收到 `DLL_PROCESS_DETACH` 时在 `DllMain()` 中),调用 `CancelAllIOs()` 例程。
为了处理异步 I/O 取消,您需要声明全局变量 " `HANDLE *g_hDeviceList` "(其中包含设备句柄列表)和 " `int g_nDevCount` "(其中包含列表中设备句柄的数量),如上一节所述。如前所述,您的应用程序需要在打开或关闭设备时更新这些全局变量。
现在,编译并运行您的应用程序。现在,您的应用程序可以快速、干净地处理 I/O 取消。
参考文献
微软有一些文章讨论了 I/O 取消。但它们没有提及应用程序如何在终止期间(通常只有在终止期间,应用程序才需要取消所有未完成的 I/O 操作)确切地取消所有未完成的 I/O 操作——这正是本文的重点。
历史
- 2007 年 2 月 12 日:原文