二.获取重叠I/O操作完成结果
当异步I/O请求挂起后,最终要知道I/O操作是否完成。一个重叠I/O请求最终完成后,应用程序要负责取回重叠I/O操作的结果。对于读,直到I/O完成,接收缓冲器才有效(参考IRP缓冲区管理)。对于写,要知道写是否成功,有几种方法可以做到这点,最直接的方法是调用(WSA)GetOverlappedResult,其函数原型如下。
WINBASEAPI BOOL WINAPI
GetOverlappedResult(
HANDLE hFile,
LPOVERLAPPED lpOverlapped,
LPDWORD lpNumberOfBytesTransferred,
BOOL bWait);
WINSOCK_API_LINKAGE BOOL WSAAPI
WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags);
l 参数一为的文件/套接字句柄。
l 参数二为参数一关联的(WSA)OVERLAPPED结构,在调用CreateFile、WSASocket或AcceptEx时指定。
l 参数三指向字节计数指针,负责接收一次重叠发送或接收操作实际传输的字节数。
l 参数四是确定命令是否等待的标志。Wait参数用于决定函数是否应该等待一次重叠操作完成。若将Wait设为TRUE,那么直到操作完成函数才返回;若设为FALSE,而且操作仍然处于未完成状态,那么(WSA)GetOverlappedResult函数会返回FALSE值。
如(WSA)GetOverlappedResult函数调用成功,返回值就是TRUE。这意味着我们的重叠I/O操作已成功完成,而且由参数三BytesTransfered参数指向的值已进行了更新。若返回值是FALSE,那么可能是由下述任何一种原因造成的:
■ 重叠I/O操作仍处在“待决”状态。
■ 重叠操作已经完成,但含有错误。
■ 重叠操作的完成状态不可判决,因为在提供给 WSAGetOverlappedResult函数的一个或多个参数中,存在着错误。
失败后,由BytesTransfered参数指向的值不会进行更新,而且我们的应用程序应调用(WSA)GetLastError函数,检查到底是何种原因造成了调用失败以使用相应容错处理。如果错误码为
ERROR/WSA_IO_INCOMPLETE(Overlapped I/O event is not in a signaled state)或
ERROR/WSA_IO_PENDING(Overlapped I/O operation is in progress),则表明I/O仍在进行。当然,这不是真正错误,任何其他错误码则真正表明一个实际错误。
下面介绍两种常用重叠I/O完成通知的方法。
1.使用事件通知
使用(WSA)GetOverlappedResult是直截了当的,它吻合重叠I/O的概念。毕竟,如果要等待I/O,也许使用常规I/O命令更好。对于大多数程序,反复检查I/O是否完成,并非最佳。解决方案之一是使用(WSA)OVERLAPPED结构中的hEvent字段,使应用程序将一个事件对象句柄同一个文件/套接字关联起来。
当指定OVERLAPPED参数给ReadFile/WriteFile或WSARecv/WSASend后,可以再为(WSA)OVERLAPPED最后一个参数提供自定义的事件对象(通过(WSA)CreateEvent创建)。
当I/O完成时,系统更改(WSA)OVERLAPPED结构对应的事件对象的传信状态,使其从“未传信”(unsignaled)变成“已传信”(signaled)。由于我们之前将事件对象分配给了(WSA)OVERLAPPED结构,所以只需简单地调用WaitForSingleObject/WaitForMultipleObjects或WSAWaitForMultipleEvents函数,从而判断出一个(一些)重叠I/O在什么时候完成。通过WaitForSingleObject/WaitForMultipleObjects或WSAWaitForMultipleEvents函数返回的索引可以知道这个重叠I/O完成事件是在哪个HANDLE(File或Socket)上发生的。
然后调用(WSA)GetOverlappedResult函数,将发生事件的HANDLE(File或Socket)传给参数一,将这个HANDLE对应的(WSA)OVERLAPPED结构传给参数二,这样判断重叠调用到底是成功还是失败。如果返回FALSE值,则重叠操作已经完成但含有错误。或者重叠操作的完成状态不可判决,因为在提供给 WSAGetOverlappedResult函数的一个或多个参数中存在着错误。失败后,由BytesTransfered参数指向的值不会进行更新,应用程序应调用(WSA)GetLastError函数,调查到底是何种原因造成了调用失败。
若(WSA)GetOverlappedResult函数返回TRUE,则根据先前调用异步I/O函数时设置的缓冲区(ReadFile/WriteFile.lpBuffer,WSARecv/WSASend.lpBuffers)和BytesTransfered,使用指针偏移定位就可以准确操作接受到的数据了。
利用事件对象来完成同步通知的方法比重复调用(WSA)GetOverlappedResult浪费处理器时间的方案要高效得多。但WaitForMultipleObjects/WSAaitForMultipleEvent支持的事件对象个数的上限为MAXIMUM_WAIT_OBJECTS/WSA_MAXIMUM_WAIT_EVENTS=64!
2.使用完成例程
对于文件重叠I/O操作,等待I/O操作结束的另外方法是使用ReadFileEx和WriteFileEx。这些命令只用于重叠I/O,当为它们的最后一个参数lpCompletionRoutine传递了一个完成例程指针(回调函数地址)时,I/O操作结束时将调用此函数进行处理。
完成例程指针LPOVERLAPPED_COMPLETION_ROUTINE定义如下:
// WINBASE.H
typedef VOID (WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
DWORD dwErrorCode,
DWORD dwNumberOfBytesTransfered,
LPOVERLAPPED lpOverlapped );
相应在Winsock 2中,WSARecv/WSASend最后一个参数lpCompletionROUTINE是一个可选的指针,它指向一个完成例程。若指定此参数(自定义函数地址),在重叠请求完成后,将调用完成例程处理。
Winsock 2中完成例程指针LPWSAOVERLAPPED_COMPLETION_ROUTINE定义略有不同:
// WINSOCK2.H
typedef void (CALLBACK * LPWSAOVERLAPPED_COMPLETION_ROUTINE)(
DWORD dwError,
DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags );
前三个参数同LPOVERLAPPED_COMPLETION_ROUTINE,参数四一般不用,置0。用完成例程完成一个重叠I/O请求之后,参数中会包含下述信息:
参数一dwError表明了一个重叠操作(由lpOverlapped指定)的完成状态是什么。
参数二BytesTransferred参数指定了在重叠操作实际传输的字节量是多大。
参数三lpOverlapped参数指定的是调用这个完成例程的异步I/O操作函数(ReadFileEx/WriteFileEx或WSARecv/WSASend)的(WSA)OVERLAPPED结构参数。
提交带有完成例程的重叠I/O请求时,(WSA)OVERLAPPED结构的事件字段hEvent一般不再使用。使用一个含有完成例程指针参数的异步I/O函数发出一个重叠I/O请求之后,一旦重叠I/O操作完成,作为我们的调用线程,必须能够通知完成例程指针所指向的自定义函数开始执行,提供数据处理服务。这样一来,便要求将调用线程置于一种“可警告的等待状态”,在I/O操作完成后,能自动调用完成例程。WSAWaitForMultipleEvents函数可用来将线程置于一种可警告的等待状态。这样做的代价是必须创建一个事件对象可用于WSAWaitForMultipleEvents函数。假定应用程序只用完成例程对重叠请求进行处理,便不需要引入事件对象。作为一种变通方法,我们的应用程序可用Win32的SleepEx函数将自己的线程置为一种可警告的等待状态。当然,亦可创建一个伪事件对象,不将它与任何东西关联在一起。假如调用线程经常处于繁忙状态,而且并不处于一种可警告的等待状态,那么完成例程根本不会被通知执行。
如前面所述,WSAWaitForMultipleEvents通常会等待同WSAOVERLAPPED结构关联在一起的事件对象。该函数也可用于将我们的线程设置为一种可警告的等待状态,为已经完成的重叠I/O请求调用完成例程进行处理(前提是将fAlertable参数设为TRUE)。使用一个含有完成例程指针的异步I/O函数提交了重叠I/O请求之后,WSAWaitForMultipleEvents的期望返回值是WAIT_IO_COMPLETION(One or more I/O completion routines are queued for execution),而不再是事件对象索引。从宏WAIT_IO_COMPLETION的注解可知,它的意思是有完成例程需要执行。SleepEx函数的行为实际上和WSAWaitForMultipleEvents差不多,只是它不需要任何事件对象。对SleepEx函数的定义如下:
WINBASEAPI DWORD WINAPI
SleepEx(
DWORD dwMilliseconds,
BOOL bAlertable );
其中,dwMilliseconds参数定义了SleepEx函数的等待时间,以毫秒为单位。假如将dwMilliseconds设为INFINITE,那么SleepEx会无休止地等待下去。bAlertable参数规定了一个完成例程的执行方式,若将它设置为FALSE,则使用一个含有完成例程指针的异步I/O函数提交了重叠I/O请求后,I/O完成例程不会被通知执行,而且SleepEx函数不会返回,除非超过由dwMilliseconds规定的时间;若将它设置为TRUE,则完成例程会被通知执行,同时SleepEx函数返回WAIT_IO_COMPLETION。
在完成例程处理模型中,投递重叠I/O请求的同时注册完成例程,待I/O完成时由系统回调,并克服了事件通知模型的个数限制。利用完成例程处理重叠I/O的Winsock程序的编写步骤如下:
1) 新建一个监听套接字,在指定端口上监听客户端的连接请求。
2) 接受一个客户端的连接请求,并返回一个会话套接字负责与客户端通信。
3) 为会话套接字关联一个WSAOVERLAPPED结构。
4) 在套接字上投递一个异步WSARecv请求,方法是将WSAOVERLAPPED指定成为参数,同时提供一个完成例程。
5) 在将fAlertable参数设为TRUE的前提下,调用WSAWaitForMultipleEvents,并等待一个重叠I/O请求完成。重叠请求完成后,完成例程会自动执行,而且WSAWaitForMultipleEvents会返回一个WAIT_IO_COMPLETION。在完成例程内,可随一个完成例程一道投递另一个重叠WSARecv请求。
6) 检查WSAWaitForMultipleEvents是否返回WAIT_IO_COMPLETION。
7) 重复步骤5 )和6 )。
当调用accept处理连接时,一般创建一个AcceptEvent伪事件,当有客户连接时,需要手动SetEvent(AcceptEvent);当调用AcceptEx处理重叠的连接时,一般为ListenSocket创建一个ListenOverlapped结构,并为其指定一个伪事件,当有客户连接时,系统自动将其置信。这些伪事件的作用在于,当含有完成例程指针的异步I/O操作(如WSARecv)完成时,设置了fAlertable的WSAWaitForMultipleEvents返回WAIT_IO_COMPLETION,并调用完成例程指针指向的完成例程对数据进行处理。
重叠I/O模型的缺点是它一般要为每一个I/O请求都开一个线程,当同时有成千上万个请求发生时,系统处理线程上下文切换是非常耗时的。所以这也就引出了更为先进的完成端口模型IOCP,用线程池来解决这个问题。
上一篇:《重叠I/O模型(1)》
参考:
《Windows 2000 Systems Programming Black Book》 Al Williams
《Network Programming for Microsoft Windows》 Anthony Jones,Jim Ohlund
评论