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

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

原始产品版本: Windows
原始 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 则用于确定操作完成后在操作中传输的实际字节数。 &NumberOfBytesRead传入ReadFile是毫无意义的。

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

另一个具有异步操作的注意事项是,在结构挂起的操作完成之前,不得使用 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 函数执行自己的内部分析。

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

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

参考

同步和异步 I/O