异步磁盘 I/O 在 Windows 上显示为同步

本文可帮助你解决 I/O 的默认行为同步但显示为异步的问题。

原始产品版本: 窗户
原始 KB 编号: 156932

摘要

Microsoft Windows 上的文件 I/O 可以是同步的,也可以是异步的。 I/O 的默认行为是同步的,其中调用 I/O 函数并在 I/O 完成时返回。 异步 I/O 允许 I/O 函数立即将执行返回给调用方,但 I/O 直到将来的某个时间才假定完成。 当 I/O 完成时,操作系统会通知调用方。 相反,调用方可以使用操作系统的服务来确定未完成的 I/O 操作的状态。

异步 I/O 的优点是调用方在 I/O 操作完成时有时间执行其他工作或发出更多请求。 术语重叠 I/O 通常用于异步 I/O 和同步 I/O 的非重叠 I/O。 本文使用术语“异步”和“同步”进行 I/O 操作。 本文假定读者熟悉文件 I/O 函数,例如 CreateFileReadFileWriteFile

异步 I/O 操作通常与同步 I/O 相同。 本文稍后部分讨论的某些条件,使 I/O 操作同步完成。 调用方没有时间进行后台工作,因为 I/O 函数在 I/O 完成之前不会返回。

多个函数与同步和异步 I/O 相关。 本文使用 ReadFileWriteFile 作为示例。 好的替代方案是 ReadFileExWriteFileEx。 尽管本文仅专门讨论磁盘 I/O,但许多原则可以应用于其他类型的 I/O,例如串行 I/O 或网络 I/O。

设置异步 I/O

FILE_FLAG_OVERLAPPED打开文件时,必须在 中CreateFile指定标志。 此标志允许以异步方式对文件执行 I/O 操作。 下面是一个示例:

HANDLE hFile;

hFile = CreateFile(szFileName,
                      GENERIC_READ,
                      0,
                      NULL,
                      OPEN_EXISTING,
                      FILE_FLAG_NORMAL | FILE_FLAG_OVERLAPPED,
                      NULL);

if (hFile == INVALID_HANDLE_VALUE)
      ErrorOpeningFile();

为异步 I/O 编写代码时要小心,因为系统保留在需要时使操作同步的权利。 因此,最好编写程序以正确处理可能以同步或异步方式完成的 I/O 操作。 示例代码演示了此注意事项。

程序在等待异步操作完成时可以执行许多操作,例如排队其他操作或执行后台工作。 例如,以下代码正确处理读取操作的重叠和非重叠完成。 它只不过是等待未完成的 I/O 完成而已:

if (!ReadFile(hFile,
               pDataBuf,
               dwSizeOfBuffer,
               &NumberOfBytesRead,
               &osReadOperation )
{
   if (GetLastError() != ERROR_IO_PENDING)
   {
      // Some other error occurred while reading the file.
      ErrorReadingFile();
      ExitProcess(0);
   }
   else
      // Operation has been queued and
      // will complete in the future.
      fOverlapped = TRUE;
}
else
   // Operation has completed immediately.
   fOverlapped = FALSE;

if (fOverlapped)
{
   // Wait for the operation to complete before continuing.
   // You could do some background work if you wanted to.
   if (GetOverlappedResult( hFile,
                           &osReadOperation,
                           &NumberOfBytesTransferred,
                           TRUE))
      ReadHasCompleted(NumberOfBytesTransferred);
   else
      // Operation has completed, but it failed.
      ErrorReadingFile();
}
else
   ReadHasCompleted(NumberOfBytesRead);

注意

&NumberOfBytesReadReadFile传入与传递到 GetOverlappedResult不同&NumberOfBytesTransferred。 如果操作已异步执行,则 GetOverlappedResult 用于确定操作完成后在操作中传输的实际字节数。 传入 &NumberOfBytesReadReadFile 是毫无意义的。

另一方面,如果操作立即完成,则 &NumberOfBytesRead 传入 ReadFile 对于读取的字节数有效。 在这种情况下,请忽略传入 的结构OVERLAPPED;不要将其与 或 WaitForSingleObject一起使用GetOverlappedResultReadFile

异步操作的另一个 OVERLAPPED 注意事项是,在结构挂起的操作完成之前,不得使用结构。 换句话说,如果有三个未完成的 I/O 操作,则必须使用三个 OVERLAPPED 结构。 如果重用结构 OVERLAPPED ,则 I/O 操作中将收到不可预知的结果,并且可能会遇到数据损坏。 此外,必须正确初始化它,以便在第一 OVERLAPPED 次使用结构之前或在完成早期操作后重用结构之前,任何剩余数据都不会影响新操作。

相同类型的限制适用于操作中使用的数据缓冲区。 在数据缓冲区的相应 I/O 操作完成之前,不得读取或写入数据缓冲区;读取或写入缓冲区可能会导致错误和数据损坏。

异步 I/O 仍显示为同步

但是,如果按照本文前面的说明进行操作,则所有 I/O 操作通常仍按发出的顺序同步完成,并且任何操作都不会 ReadFile 返回 FALSEGetLastError() 返回 ERROR_IO_PENDING,这意味着你没有时间进行任何后台工作。 为什么会出现这种情况?

有许多原因导致 I/O 操作同步完成,即使你已编码异步操作。

压缩

异步操作的一个障碍是新技术文件系统 (NTFS) 压缩。 文件系统驱动程序不会异步访问压缩文件;相反,所有操作都是同步的。 此阻碍不适用于使用类似于 COMPRESS 或 PKZIP 的实用工具进行压缩的文件。

NTFS 加密

与压缩类似,文件加密会导致系统驱动程序将异步 I/O 转换为同步。 如果文件已解密,I/O 请求将是异步的。

扩展文件

I/O 操作同步完成的另一个原因是操作本身。 在 Windows 上,对扩展其长度的文件执行的任何写入操作都将是同步的。

注意

应用程序可以使用 函数更改文件 SetFileValidData 的有效数据长度,然后发出 WriteFile,使前面提到的写入操作变得异步。

使用 SetFileValidData Windows XP 和更高版本) 上提供的 (,应用程序可以有效地扩展文件,而不会因零填充文件而造成性能损失。

由于 NTFS 文件系统不会将数据零填充到定义的 SetFileValidData有效数据长度 (VDL) ,因此此函数具有安全隐患,因为该文件可能分配以前被其他文件占用的群集。 因此, SetFileValidData 要求调用方默认启用新的 SeManageVolumePrivilege (,这仅分配给管理员) 。 Microsoft 建议独立软件供应商 (ISV) 仔细考虑使用此类函数的影响。

缓存

大多数 I/O 驱动程序 (磁盘、通信和其他) 都有特殊情况代码,其中,如果 I/O 请求可以立即完成,操作将完成,或 ReadFileWriteFile 函数将返回 TRUE。 在所有方面,这些类型的操作似乎是同步的。 对于磁盘设备,通常,当数据缓存在内存中时,I/O 请求可以立即完成。

数据不在缓存中

但是,如果数据不在缓存中,缓存方案可能会对你不利。 Windows 缓存使用文件映射在内部实现。 Windows 中的内存管理器不提供异步页错误机制来管理缓存管理器使用的文件映射。 缓存管理器可以验证请求的页是否在内存中,因此,如果发出异步缓存读取,并且页不在内存中,则文件系统驱动程序假定你不希望阻止线程,并且请求将由有限的工作线程池处理。 调用后 ReadFile ,控制将返回到程序,读取仍挂起。

这适用于少量请求,但由于工作线程池有限, (当前 16 MB 系统) 上的 3 个,因此在特定时间仍将只有少数请求排队到磁盘驱动程序。 如果对不在缓存中的数据发出大量 I/O 操作,缓存管理器和内存管理器将饱和,并且请求会同步。

缓存管理器的行为也可能取决于你是否按顺序或随机访问文件。 按顺序访问文件时,缓存的优势最为明显。 FILE_FLAG_SEQUENTIAL_SCAN调用中的 CreateFile 标志将优化此类访问的缓存。 但是,如果以随机方式访问文件,请使用 FILE_FLAG_RANDOM_ACCESS 中的 CreateFile 标志来指示缓存管理器优化其随机访问行为。

不要使用缓存

标志 FILE_FLAG_NO_BUFFERING 对异步操作的文件系统的行为影响最大。 这是保证 I/O 请求异步的最佳方式。 它指示文件系统根本不使用任何缓存机制。

注意

使用此标志存在一些限制,这些限制与数据缓冲区对齐和设备扇区大小有关。 有关详细信息,请参阅 CreateFile 函数文档中的函数参考,了解如何正确使用此标志。

实际测试结果

下面是示例代码的一些测试结果。 数字的大小在这里并不重要,并且因计算机而异,但数字之间的关系相互比较说明了标志对性能的一般影响。

预期会看到类似于以下其中一项的结果:

  • 测试 1

    Asynchronous, unbuffered I/O:  asynchio /f*.dat /n
    Operations completed out of the order in which they were requested.
       500 requests queued in 0.224264 second.
       500 requests completed in 4.982481 seconds.
    

    此测试表明,前面提到的程序快速发出了 500 个 I/O 请求,并且有大量时间执行其他工作或发出更多请求。

  • 测试 2

    Synchronous, unbuffered I/O: asynchio /f*.dat /s /n
        Operations completed in the order issued.
        500 requests queued and completed in 4.495806 seconds.
    

    此测试表明,此程序花费了 4.495880 秒调用 ReadFile 来完成其操作,但测试 1 只花了 0.224264 秒发出相同的请求。 在测试 2 中,程序没有额外的时间来执行任何后台工作。

  • 测试 3

    Asynchronous, buffered I/O: asynchio /f*.dat
        Operations completed in the order issued.
        500 requests issued and completed in 0.251670 second.
    

    此测试演示缓存的同步特性。 所有读取在 0.251670 秒内发出并完成。 换句话说,异步请求是同步完成的。 此测试还演示了数据在缓存中时缓存管理器的高性能。

  • 测试 4

    Synchronous, buffered I/O: asynchio /f*.dat /s
        Operations completed in the order issued.
        500 requests and completed in 0.217011 seconds.
    

    此测试演示了与测试 3 中相同的结果。 从缓存进行的同步读取比从缓存进行异步读取要快一点。 此测试还演示了数据在缓存中时缓存管理器的高性能。

总结

你可以确定哪种方法最好,因为这一切都取决于程序执行的操作的类型、大小和数量。

未指定任何特殊标志 CreateFile 的默认文件访问是同步和缓存操作。

注意

在此模式下,你确实会获得一些自动异步行为,因为文件系统驱动程序对修改的数据执行预测性异步预读和异步延迟写入。 尽管此行为不会使应用程序的 I/O 异步,但它是绝大多数简单应用程序的理想情况。

另一方面,如果应用程序不简单,可能需要执行一些分析和性能监视来确定最佳方法,类似于本文前面演示的测试。 分析 在 或 WriteFile 函数中ReadFile花费的时间,然后将此时间与完成实际 I/O 操作所需的时间进行比较非常有用。 如果大部分时间都花在实际颁发 I/O 上,则 I/O 正在同步完成。 但是,如果与完成 I/O 操作所需的时间相比,发出 I/O 请求所用的时间相对较小,则异步处理操作。 本文前面提到的示例代码使用 QueryPerformanceCounter 函数执行自己的内部分析。

性能监视可帮助确定程序使用磁盘和缓存的效率。 跟踪 Cache 对象的任何性能计数器将指示缓存管理器的性能。 跟踪物理磁盘或逻辑磁盘对象的性能计数器将指示磁盘系统的性能。

有几个实用工具有助于性能监视。 PerfMonDiskPerf 特别有用。 若要使系统收集有关磁盘系统性能的数据,必须先发出 DiskPerf 命令。 发出命令后,必须重启系统才能启动数据收集。

References

同步和异步 I/O