Overlapped I/O模型深入分析
简述:
Overlapped I/O也称
Asynchronous I/O,异步I/O模型。异步I/O和同步I/O不同,同步I/O时,程序被挂起,一直到I/O处理完,程序才能获得控制。异步I/O,调用一个函数告诉OS,进行I/O操作,不等I/O结束就立即返回,继续程序执行,操作系统完成I/O之后,通知消息给你。Overlapped I/O只是一种模型,它可以由内核对象(hand),事件内核对象(hEvent), 异步过程调用(apcs) 和完成端口(I/O completion)实现。
Overlapped I/O的设计的目的:
取代多线程功能,(多线程存在同步机制,错误处理,在成千上万个线程
I/O中,线程上下文切换是十分消耗CPU资源的)。
Overlapped I/O模型是
OS为你传递数据,完成上下文切换,在处理完之后通知你。由程序中的处理,变为OS的处理。内部也是用线程处理的。
Overlapped数据结构:
typedef struct _OVERLAPPED {
DWORD Internal; 通常被保留,当
GetOverlappedResult()传回False并且GatLastError()并非传回ERROR_IO_PENDINO时,该状态置为系统定的状态。
DWORD
InternalHigh; 通常被保留,当
GetOverlappedResult()传回False时,为
被传输数据的长度。
DWORD
Offset;
指定文件的位置,从该位置传送数据,文件位置是相对文件开始
处的字节偏移量。调用 ReadFile或WriteFile函数之前调用进
程设置这个成员,读写命名管道及通信设备时调用进程忽略这
个成员;
DWORD
OffsetHigh;
指定开始传送数据的字节偏移量的高位字,读写命名管道及通
信设备时调用进程忽略这个成员;
HANDLE hEvent;
标识事件,数据传送完成时把它设为信号状态,调用ReadFile
WriteFile ConnectNamedPipe TransactNamedPipe
函数
前,调用进程设置这个成员. 相关函数
CreateEvent ResetEvent GetOverlappedResult
WaitForSingleObject CWinThread GetLastError
} OVERLAPPED, *LPOVERLAPPED;
二个重要功能:
1. 标识每个正在
overlapped 的操作。
2. 程序和系统之间提供了共享区域。参数可以在区域内双向传递。
OVERLAPPED和数据缓冲区释放问题
:
在请求时,不能释放,只有在
I/O请求完成之后,才可以释放。如果发出多个overlapped请求,每个overlapped读写操作,都必须包含文件位置(socket),另外,如果有多个磁盘,I/O执行次序无法保证。(每个overlapped都是独立的请求操作)。
内核对象(hand)实现:
例子:用
overlapped模型读一个磁盘文件内容。
1.把设备句柄看作同步对象,
ReadFile将设备句柄设为无信号。ReadFile 异步I/O字节位置必须在OVERLAPPED结构中指定。
2.完成
I/O,设置信息状态。为有信号。
3.
WaitForSingleObject或WaitForMultipleObject判断
或者异步设备调用
GetOverLappedResult函数。
int main()
{
BOOL rc;
HANDLE hFile;
DWORD numread;
OVERLAPPED overlap;
char buf[READ_SIZE];
char szPath[MAX_PATH];
CheckOsVersion();
GetWindowsDirectory(szPath, sizeof(szPath));
strcat(szPath, "//WINHLP32.EXE");
hFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Could not open %s/n", szPath);
return -1;
}
memset(&overlap, 0, sizeof(overlap));
overlap.Offset = 1500;
rc = ReadFile(
hFile,
buf,
READ_SIZE,
&numread,
&overlap
);
printf("Issued read request/n");
if (rc)
{
printf("Request was returned immediately/n");
}
else
{
if (GetLastError() == ERROR_IO_PENDING)
{
printf("Request queued, waiting.../n");
WaitForSingleObject(hFile, INFINITE);
printf("Request completed./n");
rc = GetOverlappedResult(
hFile,
&overlap,
&numread,
FALSE
);
printf("Result was %d/n", rc);
}
else
{
printf("Error reading file/n");
}
}
CloseHandle(hFile);
return EXIT_SUCCESS;
}
事件内核对象(hEvent):
内核对象
(hand)实现的问题:
不能区分那一个
overlapped操作,对同一个文件handle,系统有多个异步操作时(一边读文件头,一边写文件尾, 有一个完成,就会有信号,不能区分是那种操作。),为每个进行中的overlapped调用GetOverlappedResult是不好的作法。
事件内核对象
(hEvent)实现方案:
Overlapped成员
hEven标识事件内核对象。CreateEvent,为每个请求创建一个事件,初始化每个请求的hEvent成员(对同一文件多个读写请求,每个操作绑定一个event对象)。调用WaitForMultipleObject来等等其中一个(或全部)完成。
另外
Event对象必须是手动重置。使用自动重置(在等待event之前设置,
WaitForSingleObject()
和 WaitForMultipleObjects()函数永不返回)。
自动重置事件
WaitForSingleObject()
和 WaitForMultipleObjects()会等待事件到信号状态,随后又自动将其重置为非信号状态,这样保证了等待此事件的线程中只有一个会被唤醒。
手动重置事件
需要用户调用ResetEvent()才会重置事件。可能有若干个线程在等待同一事件,这样当事件变为信号状态时,所有等待线程都可以运行了。 SetEvent()函数用来把事件对象设置成信号状态,ResetEvent()把事件对象重置成非信号状态,两者均需事件对象句柄作参数。
相关例子如下:
int main()
{
int i;
BOOL rc;
char szPath[MAX_PATH];
CheckOsVersion();
GetWindowsDirectory(szPath, sizeof(szPath));
strcat(szPath, "//WINHLP32.EXE");
ghFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
if (ghFile == INVALID_HANDLE_VALUE)
{
printf("Could not open %s/n", szPath);
return -1;
}
for (i=0; i<MAX_REQUESTS; i++)
{
QueueRequest(i, i*16384, READ_SIZE);
}
printf("QUEUED!!/n");
MTVERIFY( WaitForMultipleObjects(
MAX_REQUESTS, ghEvents, TRUE, INFINITE
) != WAIT_FAILED );
for (i=0; i<MAX_REQUESTS; i++)
{
DWORD dwNumread;
rc = GetOverlappedResult(
ghFile,
&gOverlapped[i],
&dwNumread,
FALSE
);
printf("Read #%d returned %d. %d bytes were read./n",
i, rc, dwNumread);
CloseHandle(gOverlapped[i].hEvent);
}
CloseHandle(ghFile);
return EXIT_SUCCESS;
}
int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount)
{
int i;
BOOL rc;
DWORD dwNumread;
DWORD err;
MTVERIFY(
ghEvents[nIndex] = CreateEvent(
NULL, // No security
TRUE, // Manual reset - extremely important!
FALSE, // Initially set Event to non-signaled state
NULL // No name
)
);
gOverlapped[nIndex].hEvent = ghEvents[nIndex];
gOverlapped[nIndex].Offset = dwLocation;
for (i=0; i<MAX_TRY_COUNT; i++)
{
rc = ReadFile(
ghFile,
gBuffers[nIndex],
dwAmount,
&dwNumread,
&gOverlapped[nIndex]
);
if (rc)
{
printf("Read #%d completed immediately./n", nIndex);
return TRUE;
}
err = GetLastError();
if (err == ERROR_IO_PENDING)
{
// asynchronous i/o is still in progress
printf("Read #%d queued for overlapped I/O./n", nIndex);
return TRUE;
}
if ( err == ERROR_INVALID_USER_BUFFER ||
err == ERROR_NOT_ENOUGH_QUOTA ||
err == ERROR_NOT_ENOUGH_MEMORY )
{
Sleep(50); // Wait around and try later
continue;
}
break;
}
printf("ReadFile failed./n");
return -1;
}
异步过程调用(apcs):
事件内核对象
(hEvent)的问题:
事件内核对象在使用
WaitForMultipleObjects时,只能等待64个对象。需要另建两个数据组,并gOverlapped[nIndex].hEvent = ghEvents[nIndex]绑定起来。
异步过程调用
(apcs)实现方案:
异步过程调用,
callback回调函数,在一个Overlapped I/O完成之后,系统调用该回调函数。OS在有信号状态下(设备句柄),才会调用回调函数(可能有很多APCS等待处理了),传给它完成I/O请求的错误码,传输字节数和Overlapped结构的地址。
五个函数可以设置信号状态:
1. SleepEx
2. WaitForSingleObjectEx
3. WaitForMultipleObjectEx
4. SingalObjectAndWait
5. MsgWaitForMultipleObjectsEx
Main函数调用
WaitForSingleObjectEx, APCS被处理,调用回调函数
FileIOCompletionRoutine
VOID WINAPI FileIOCompletionRoutine(
DWORD dwErrorCode, // completion code
DWORD dwNumberOfBytesTransfered, // number of bytes transferred
LPOVERLAPPED lpOverlapped // pointer to structure with I/O information
)
{
int nIndex = (int)(lpOverlapped->hEvent);
printf("Read #%d returned %d. %d bytes were read./n",
nIndex,
dwErrorCode,
dwNumberOfBytesTransfered);
if (++nCompletionCount == MAX_REQUESTS)
SetEvent(ghEvent); // Cause the wait to terminate
}
int main()
{
int i;
char szPath[MAX_PATH];
CheckOsVersion();
MTVERIFY(
ghEvent = CreateEvent(
NULL, // No security
TRUE, // Manual reset - extremely important!
FALSE, // Initially set Event to non-signaled state
NULL // No name
)
);
GetWindowsDirectory(szPath, sizeof(szPath));
strcat(szPath, "//WINHLP32.EXE");
ghFile = CreateFile( szPath,
GENERIC_READ,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL
);
if (ghFile == INVALID_HANDLE_VALUE)
{
printf("Could not open %s/n", szPath);
return -1;
}
for (i=0; i<MAX_REQUESTS; i++)
{
QueueRequest(i, i*16384, READ_SIZE);
}
printf("QUEUED!!/n");
for (;;)
{
DWORD rc;
rc = WaitForSingleObjectEx(ghEvent, INFINITE, TRUE );
if (rc == WAIT_OBJECT_0)
break;
MTVERIFY(rc == WAIT_IO_COMPLETION);
}
CloseHandle(ghFile);
return EXIT_SUCCESS;
}
int QueueRequest(int nIndex, DWORD dwLocation, DWORD dwAmount)
{
int i;
BOOL rc;
DWORD err;
gOverlapped[nIndex].hEvent = (HANDLE)nIndex;
gOverlapped[nIndex].Offset = dwLocation;
for (i=0; i<MAX_TRY_COUNT; i++)
{
rc = ReadFileEx(
ghFile,
gBuffers[nIndex],
dwAmount,
&gOverlapped[nIndex],
FileIOCompletionRoutine
);
if (rc)
{
printf("Read #%d queued for overlapped I/O./n", nIndex);
return TRUE;
}
err = GetLastError();
if ( err == ERROR_INVALID_USER_BUFFER ||
err == ERROR_NOT_ENOUGH_QUOTA ||
err == ERROR_NOT_ENOUGH_MEMORY )
{
Sleep(50); // Wait around and try later
continue;
}
break;
}
printf("ReadFileEx failed./n");
return -1;
}
完成端口(I/O completion):
异步过程调用
(apcs)问题:
只有发
overlapped请求的线程才可以提供callback函数(需要一个特定的线程为一个特定的I/O请求服务)。
完成端口
(I/O completion)的优点:
不会限制
handle个数,可处理成千上万个连接。I/O completion port允许一个线程将一个请求暂时保存下来,由另一个线程为它做实际服务。
并发模型与线程池:
在典型的并发模型中,服务器为每一个客户端创建一个线程,如果很多客户同时请求,则这些线程都是运行的,那么
CPU就要一个个切换,CPU花费了更多的时间在线程切换,线程确没得到很多CPU时间。到底应该创建多少个线程比较合适呢,微软件帮助文档上讲应该是2*CPU个。但理想条件下最好线程不要切换,而又能象线程池一样,重复利用。I/O完成端口就是使用了线程池。