如何使用Windows OVERLAPPED优化你的应用
01.异步I/O基本原理
I/O即输入输出。在现代操作系统中,输入输出是计算机完整功能必不可少的一部分。处理器负责各种计算任务,然后通过各种输入输出设备与外界进行交互。常见的输入输出设备包括键盘、鼠标、显示器、硬盘、网络适配器接口等。有了硬件设备,在软件层面上,使得操作系统通过以一致的方式与设备驱动交互从而操控硬件设备。而应用程序通过统一的接口与系统内核进行交互。
在计算机程序执行过程中,设备I/O是最慢和最不可预测的操作之一。CPU在执行算术运算甚至绘制屏幕时的速度都要比从外设中读写数据或通过网络传输数据快得多。使用异步I/O可以使应用程序能够更好地使用资源,从而实现执行效率更高的应用程序。
当一个函数或者线程向设备发出I/O请求时,这个I/O请求被传递给设备驱动程序,由设备驱动程序完成实际的I/O操作任务。当设备驱动程序在读写I/O外设时需要等待设备的响应,发起I/O操作的函数或线程并不会立即返回,必须要等待I/O操作完成后才可以进行下一步操作。尤其在一些设备I/O操作较多的程序中,这种情况会严重拖慢整个应用的执行效率。如果使用异步I/O操作,发起IO操作的函数或线程会将数据放在指定的buffer中,并且立即返回继续执行下一步的操作,比较耗时的I/O操作会由操作系统完成。这样就可以实现比较高效的I/O操作,优化程序的执行效率。
当使用异步I/O操作时,在操作系统内核中由设备驱动程序完成I/O操作队列,操作系统通过消息通知应用程序数据已经发送、数据已经收到或者出现了读写错误。使用异步I/O是设计高性能、可伸缩应用程序的本质,这也是我们要重点讨论的内容。
在Windows中要使用异步的方式访问设备,需要首先通过调用CreateFile来打开设备或者创建文件,在文件的属性和标志位中设置 FILE_FLAG_OVERLAPPED 属性。FILE_FLAG_OVERLAPPED 通知Windows操作系统此设备或文件将以异步方式来操作。
BOOL ReadFile(
HANDLE hFile,
PVOID pvBuffer,
DWORD nNumBytesToRead,
PDWORD pdwNumBytes,
OVERLAPPED* pOverlapped);
BOOL WriteFile(
HANDLE hFile,
CONST VOID *pvBuffer,
DWORD nNumBytesToWrite,
PDWORD pdwNumBytes,
OVERLAPPED* pOverlapped);
当调用这两个读写函数时,该函数首先检查hFile参数中是否打开了FILE_FLAG_OVERLAPPED 标志位。如果使用了OVERLAPPED的标志位,则该函数执行异步设备I/O。当调用异步I/O中的任意一个函数时,可以为pdwNumBytes参数传递NULL。因为这些函数在I/O请求完成之前返回,检查传输的字节数是没有意义的。
02.OVERLAPPED结构体成员变量介绍
在使用异步I/O时,必须通过pOverlapped参数将地址传递给初始化的OVERLAPPED结构体。在此上下文中,单词“overlapped”意味着执行I/O请求的时间与线程执行其他任务的时间重叠。这是OVERLAPPED结构体的样子:
typedef struct _OVERLAPPED {
DWORD Internal; // [out] Error code
DWORD InternalHigh; // [out] Number of bytes transferred
DWORD Offset; // [in] Low 32-bit file offset
DWORD OffsetHigh; // [in] High 32-bit file offset
HANDLE hEvent; // [in] Event handle or data
} OVERLAPPED, *LPOVERLAPPED;
此结构体中包含五个成员变量。其中三个成员变量Offset、OffsetHigh 以及hEvent必须在调用ReadFile或WriteFile之前初始化。其他两个成员Internal和InternalHigh由设备驱动程序设置。下面对这些成员变量进行更详细的解释:
当程序读写一个文件时,Offset标记了此文件读写位置的偏移量。当操作系统进行文件操作时从文件指针指定的位置开始访问文件。操作完成后,操作系统自动更新文件指针,以便下次继续操作。
注意,对于非文件的设备I/O,Offset和OffsetHigh成员变量必须初始化为0,否则I/O请求会返回失败,GetLastError将会返回ERROR_INVALID_PARAMETER的错误。
hEvent可用于创建接收I/O完成之后由操作系统产生的事件。
Internal成员变量用来保存处理过的I/O操作错误码。一旦发出异步I/O的操作请求,设备驱动程序就会将Internal设置为STATUS_PENDING,表示没有发生错误,因为操作还没有开始。如果请求仍然在等待,则返回FALSE,如果I/O请求完成,则返回TRUE。当初设计OVERLAPPED结构体时,微软决定不暴露Internal和InternalHigh成员(这一点从名字也可以看出来)。随着时间的推移,微软意识到这些成员中包含的信息对开发人员有用,所以在后来的设计中开发人员可以从外部访问这两个变量。但是,为了保证兼容性微软并没有改变此成员变量的名字,因为系统内核也依赖于这两个变量。
03.接收异步I/O操作完成产生的事件
接下来我们讨论在I/O请求完成后设备驱动程序如何通知应用程序。Windows提供了四种不同的接收I/O完成通知的方法。下面对这些方法进行简单的介绍。这些方法按照复杂度的顺序排列,从最容易理解和实现的(发送设备内核对象的信号)到最难理解和实现的(I/O完成端口)。
完成端口是四种接收I/O操作完成事件的最佳方法。通过学习这四种方法,就可以了解微软为什么要将完成端口加到Windows中,以及完成端口如何补充其他方法所存在的不足。
04.代码片段
下面代码演示如何在应用程序中使用ReadFileEx进行异步读操作
#include <windows.h>
#include <stdio.h>
#define DEVICE_NAME "test.dat"
#define BUFFER_SIZE 512
//假设该文件大于或等于BUFFER_SIZE
VOID CALLBACK MyFileIOCompletionRoutine(
DWORD dwErrorCode, // 对于此次操作返回的状态
DWORD dwNumberOfBytesTransfered, // 告诉已经操作了多少字节,也就是在
//IRP里的Infomation
LPOVERLAPPED lpOverlapped // 这个数据结构
)
{
printf("IO operation end!\n");
}
int main()
{
HANDLE hDevice =
CreateFile("test.dat",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,
//此处设置FILE_FLAG_OVERLAPPED
NULL );
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Read Error\n");
return 1;
}
UCHAR buffer[BUFFER_SIZE];
//初始化overlap使其内部全部为零
//不用初始化事件!!
OVERLAPPED overlap={0};
//这里没有设置OVERLAP参数,因此是异步操作
ReadFileEx(hDevice, buffer, BUFFER_SIZE,&overlap,MyFileIOCompletionRoutine);
//做一些其他操作,这些操作会与读设备并行执行
//进入alterable
SleepEx(0,TRUE);
CloseHandle(hDevice);
return 0;
}
05.进一步资料
要实现异步I/O操作可以直接参考Windows 官方源码来做测试demo,同时也可以参考一些优秀的软件的代码片段来实现Windows 异步I/O的实现。这里列出几个可以做代码参考的链接。
1.Windows 官方文档 Data Access and Storage
Data Access and Storage - Win32 apps | Microsoft Docs
2.tcp2com项目
tcp2com download | SourceForge.net
https://sourceforge.net/projects/tcp2com/
此项目用于实现TCP与串口数据互相转发,代码实现完整,具有较高的参考价值。
参考链接:
[1] ReadFile function
https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile?redirectedfrom=MSDN
[2] WriteFile function
https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile?redirectedfrom=MSDN