您的位置:首页 > 编程语言

IOCP编程之重叠IO

2012-05-10 20:25 519 查看
其实这个标题有点“标题党”的味道,为了大家搜索方便我故意冠以IOCP编程之名,其实重叠IO编程并不一定需要IOCP,而IOCP编程就一定需要重叠IO。是不是已经被这句话给绕晕了?总之是为了更好的应用IOCP,所以要理解重叠IO。这篇文章的核心就是讨论重叠IO的来龙去脉。
在很久很久以前,在用C语言写DOS程序的年代,就有了很完整的IO标准库支撑,printf输出字符到屏幕,fopen,fwrite,fread等操作文件,甚至还有一些函数可以在屏幕上绘图,到了Windows时代,有了API,当然输出到屏幕的函数被GUI GDI的API代替,而文件的操作就被CreateFile、WriteFile、ReadFile等代替,在使用这些函数时,其实很多时候我们会感觉到“慢”,为什么呢?因为它们的工作方式就是等待输入或输出操作结束后才返回,而这些IO设备通常都是些慢的要死的设备,等它们完成工作再返回,通常CPU都打瞌睡了。
当然有些程序可以没有明显的屏幕输入输出操作,可是不同硬盘打交道的软件就很少了。假如访问硬盘比较频繁时,可以明显感觉到程序的性能下降。比如,为一个程序挂接了一个朝磁盘文件写入日志的功能,结果往往会发现,当打开日志时,程序就会慢的像蜗牛一样,而关闭日志系统一切又正常了,这时磁盘日志功能因为速度问题而变成了鸡肋。
上面说的工作方式,其实是一种被Windows系统称之为“同步”的方式,也就是说你的程序工作的步骤和那些慢速的IO设备是一致的,IO设备要多长时间完成操作,你的程序就要多长时间完成操作。这听起来有点恐怖,但似乎好像这又是合理的。其实这并不合理,比如还是那个磁盘日志系统,往往在写入日志记录的时候,根本不用等待这个写入的完成,程序逻辑可以自由的继续往下执行。其实大多数情形下,都会自然的希望程序这样去执行IO操作。
当然Windows平台也考虑到了这种情况,所以就提供了一种称之为“重叠IO”的操作方式来“异步”操作IO,目前支持重叠IO操作的系统对象除了文件,还有管道、串口、甚至SOCKET都可以用这种方式来操作。
具体的在Windows平台上,异步IO的原理其实比较简单,就是你调用完IO函数后,函数会立即返回,你的程序或者说当前线程会继续往下执行,而你需要建立一个“可警告(alert able)”的线程来等待接收IO操作完成的通知。这样调用IO函数与IO函数的完成返回就成了“异步”方式执行。对于调用者来说,它的目标就集中到了整个程序逻辑的合理性问题上,而不用去关心IO操作的结果。
当然也有些情况下,还是需要知道IO操作完成的结果,比如读取图片文件,然后显示,乍一想貌似这种情况使用“异步”方式是不很合理的,其实这时也完全可以用异步方式来操作,并提高一定的性能。假设需要显示的不止一张图片,那么就可以将所有的读取操作一次性调用完成,可能是几十个,也可能是几百个,然后在IO操作完成的回调函数中按照图片位置,做相应的显示即可。
虽然可以很容易的理解重叠IO的异步工作特性,但是对于这个奇怪的名字估计很多人还是比较迷惑的,为什么叫重叠呢?为什么不直接叫异步IO呢?其实这个名字正好显示了这种IO操作方式的精髓“重叠”。
其实能够理解重叠IO的异步特性或者原理,只是理解了它的一部分,或者说只是个表象。要理解重叠,就首先让我们回到那个磁盘日志系统上来,假设这是一个写入比较频繁的日志系统(其实很多日志系统都是如此),如前所述如果用“同步”的方式来写入,那么性能就会很低下,而如果要用“异步”方式操作,那么是不是需要等待一个完成通知之后,再进行下一个写入呢(这是很多初学者容易迷惑的地方)?其实不是,这里就是重叠的意思了,也就是说,你不必等到某个IO操作完成,就可以调用下一个IO操作,而这些IO操作可以被看做是堆叠在一起,等待完成,这就是重叠IO的真谛。这些“重叠”在一起的IO操作,将按照一定的顺序被完成,但是它们的完成通知并不是严格按照顺序回调,尤其是在多线程环境中,回调基本是随机的。调用顺序,和完成回调顺序是完全不同的两个概念,这一点一定要区别清楚。
理解了原理,就让我们具体来看看重叠IO如何编程。关于IOCP调用重叠IO的例子,可以参看本人博客中其他几篇关于IOCP的文章。
要想重叠IO就首先要有一个重叠结构,这个结构被命名做OVERLAPPED,如果你看到某个API函数参数中有这个字眼,你基本就可以确定这个函数是可以“重叠”操作的。当然要让某个系统对象打开重叠IO的特性,就需要在创建该对象时明确指定一些标志。 比如调用CreateFile、CreateNamePipe、WSASocket等。
重叠IO的完成通知有两种方式可以得到,一种是通过传递一个Event内核对象的句柄,另一种就是传递一个回调函数的指针。下面就让我们先来看一个重叠IO操作管道的例子:
#include <windows.h>

#include <stdio.h>
#include <tchar.h>
#include <strsafe.h>
#define CONNECTING_STATE 0

#define READING_STATE 1

#define WRITING_STATE 2

#define INSTANCES 4

#define PIPE_TIMEOUT 5000
#define BUFSIZE 4096
typedef struct
{
OVERLAPPED oOverlap;

HANDLE hPipeInst;

TCHAR chRequest[BUFSIZE];

DWORD cbRead;
TCHAR chReply[BUFSIZE];
DWORD cbToWrite;

DWORD dwState;

BOOL fPendingIO;

} PIPEINST, *LPPIPEINST;

VOID DisconnectAndReconnect(DWORD);

BOOL ConnectToNewClient(HANDLE, LPOVERLAPPED);

VOID GetAnswerToRequest(LPPIPEINST);

PIPEINST Pipe[INSTANCES];

HANDLE hEvents[INSTANCES];

int _tmain(VOID)
{
DWORD i, dwWait, cbRet, dwErr;

BOOL fSuccess;

LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");

// The initial loop creates several instances of a named pipe

// along with an event object for each instance. An

// overlapped ConnectNamedPipe operation is started for

// each instance.

for (i = 0; i < INSTANCES; i++)

{
// Create an event object for this instance.

hEvents[i] = CreateEvent(

NULL, // default security attribute

TRUE, // manual-reset event

TRUE, // initial state = signaled

NULL); // unnamed event object

if (hEvents[i] == NULL)

{
printf("CreateEvent failed with %d.\n", GetLastError());

return 0;
}
Pipe[i].oOverlap.hEvent = hEvents[i];

Pipe[i].hPipeInst = CreateNamedPipe(

lpszPipename, // pipe name

PIPE_ACCESS_DUPLEX | // read/write access

FILE_FLAG_OVERLAPPED, // overlapped mode

PIPE_TYPE_MESSAGE | // message-type pipe

PIPE_READMODE_MESSAGE | // message-read mode

PIPE_WAIT, // blocking mode

INSTANCES, // number of instances

BUFSIZE*sizeof(TCHAR), // output buffer size

BUFSIZE*sizeof(TCHAR), // input buffer size

PIPE_TIMEOUT, // client time-out

NULL); // default security attributes

if (Pipe[i].hPipeInst == INVALID_HANDLE_VALUE)

{
printf("CreateNamedPipe failed with %d.\n", GetLastError());
return 0;
}
// Call the subroutine to connect to the new client
Pipe[i].fPendingIO = ConnectToNewClient(

Pipe[i].hPipeInst,

&Pipe[i].oOverlap);

Pipe[i].dwState = Pipe[i].fPendingIO ?

CONNECTING_STATE : // still connecting

READING_STATE; // ready to read

}
while (1)
{
// Wait for the event object to be signaled, indicating

// completion of an overlapped read, write, or

// connect operation.

dwWait = WaitForMultipleObjects(

INSTANCES, // number of event objects

hEvents, // array of event objects

FALSE, // does not wait for all

INFINITE); // waits indefinitely

// dwWait shows which pipe completed the operation.

i = dwWait - WAIT_OBJECT_0; // determines which pipe

if (i < 0 || i > (INSTANCES - 1))

{
printf("Index out of range.\n");

return 0;
}
// Get the result if the operation was pending.

if (Pipe[i].fPendingIO)

{
fSuccess = GetOverlappedResult(

Pipe[i].hPipeInst, // handle to pipe

&Pipe[i].oOverlap, // OVERLAPPED structure

&cbRet, // bytes transferred

FALSE); // do not wait

switch (Pipe[i].dwState)

{
// Pending connect operation

case CONNECTING_STATE:

if (! fSuccess)

{
printf("Error %d.\n", GetLastError());

return 0;
}
Pipe[i].dwState = READING_STATE;

break;

// Pending read operation

case READING_STATE:

if (! fSuccess || cbRet == 0)

{
DisconnectAndReconnect(i);

continue;

}
Pipe[i].dwState = WRITING_STATE;

break;

// Pending write operation

case WRITING_STATE:

if (! fSuccess || cbRet != Pipe[i].cbToWrite)

{
DisconnectAndReconnect(i);

continue;

}
Pipe[i].dwState = READING_STATE;

break;

default:

{
printf("Invalid pipe state.\n");

return 0;
}
}
}
// The pipe state determines which operation to do next.

switch (Pipe[i].dwState)

{
// READING_STATE:

// The pipe instance is connected to the client

// and is ready to read a request from the client.


case READING_STATE:

fSuccess = ReadFile(

Pipe[i].hPipeInst,

Pipe[i].chRequest,

BUFSIZE*sizeof(TCHAR),

&Pipe[i].cbRead,

&Pipe[i].oOverlap);

// The read operation completed successfully.

if (fSuccess && Pipe[i].cbRead != 0)

{
Pipe[i].fPendingIO = FALSE;

Pipe[i].dwState = WRITING_STATE;

continue;

}
// The read operation is still pending.

dwErr = GetLastError();

if (! fSuccess && (dwErr == ERROR_IO_PENDING))

{
Pipe[i].fPendingIO = TRUE;

continue;

}
// An error occurred; disconnect from the client.

DisconnectAndReconnect(i);

break;

// WRITING_STATE:

// The request was successfully read from the client.

// Get the reply data and write it to the client.

case WRITING_STATE:

GetAnswerToRequest(&Pipe[i]);

fSuccess = WriteFile(

Pipe[i].hPipeInst,

Pipe[i].chReply,

Pipe[i].cbToWrite,

&cbRet,

&Pipe[i].oOverlap);

// The write operation completed successfully.

if (fSuccess && cbRet == Pipe[i].cbToWrite)

{
Pipe[i].fPendingIO = FALSE;

Pipe[i].dwState = READING_STATE;

continue;

}
// The write operation is still pending.

dwErr = GetLastError();

if (! fSuccess && (dwErr == ERROR_IO_PENDING))

{
Pipe[i].fPendingIO = TRUE;

continue;

}
// An error occurred; disconnect from the client.

DisconnectAndReconnect(i);

break;

default:

{
printf("Invalid pipe state.\n");

return 0;
}
}
}

return 0;
}
// DisconnectAndReconnect(DWORD)

// This function is called when an error occurs or when the client

// closes its handle to the pipe. Disconnect from this client, then

// call ConnectNamedPipe to wait for another client to connect.

VOID DisconnectAndReconnect(DWORD i)

{
// Disconnect the pipe instance.

if (! DisconnectNamedPipe(Pipe[i].hPipeInst) )

{
printf("DisconnectNamedPipe failed with %d.\n", GetLastError());
}
// Call a subroutine to connect to the new client.

Pipe[i].fPendingIO = ConnectToNewClient(

Pipe[i].hPipeInst,

&Pipe[i].oOverlap);

Pipe[i].dwState = Pipe[i].fPendingIO ?

CONNECTING_STATE : // still connecting

READING_STATE; // ready to read

}
// ConnectToNewClient(HANDLE, LPOVERLAPPED)

// This function is called to start an overlapped connect operation.

// It returns TRUE if an operation is pending or FALSE if the

// connection has been completed.

BOOL ConnectToNewClient(HANDLE hPipe, LPOVERLAPPED lpo)

{
BOOL fConnected, fPendingIO = FALSE;

// Start an overlapped connection for this pipe instance.

fConnected = ConnectNamedPipe(hPipe, lpo);

// Overlapped ConnectNamedPipe should return zero.

if (fConnected)

{
printf("ConnectNamedPipe failed with %d.\n", GetLastError());

return 0;
}
switch (GetLastError())

{
// The overlapped connection in progress.

case ERROR_IO_PENDING:

fPendingIO = TRUE;

break;
// Client is already connected, so signal an event.

case ERROR_PIPE_CONNECTED:

if (SetEvent(lpo->hEvent))

break;

// If an error occurs during the connect operation...

default:
{
printf("ConnectNamedPipe failed with %d.\n", GetLastError());
return 0;
}
}
return fPendingIO;

}
VOID GetAnswerToRequest(LPPIPEINST pipe)
{
_tprintf( TEXT("[%d] %s\n"), pipe->hPipeInst, pipe->chRequest);
StringCchCopy( pipe->chReply, BUFSIZE, TEXT("Default answer from server") );
pipe->cbToWrite = (lstrlen(pipe->chReply)+1)*sizeof(TCHAR);
}
上面这个例子直接来源于MSDN,我没有做任何修改,下面就来解释下例子中的一些代码。
1、例子中使用的Event方式来获得IO操作完成的通知的;
2、例子中封装了一个自定义的OVERLAPPED结构,这与IOCP线程池方式操作IO时是一样的;
3、例子中使用CreateNamedPipe+FILE_FLAG_OVERLAPPED标志,创建了一个可以重叠操作的命名管道对象,这是使用重叠IO的第一步;
4、例子中为每个客户端的IO操作都定义了一个自定义OVERLAPPED结构,然后为其中的Event字段创建了Event对象;
5、接着就是那个精彩的“While死循环”,首先循环已开始使用GetOverlappedResult函数得到IO操作的结果,其次是一个状态迁移的逻辑,就是从Connect迁移到Read再迁移到Write,然后迁移到断开重新等待连接,最后就是根据状态投递对应的IO操作,然后又进入等待。
这个例子中,演示了重叠IO的基本异步操作特性,是个Echo服务器。从中主要需要理解和掌握的就是重叠IO的核心的理念——我们不用去理会IO操作什么时候结束,我们只需要关注我们在什么时候需要调用IO操作,剩下的就是IO机构自己去完成操作,并返回给我们最终的完成结果。另一个需要我们理解的理念就是,重叠IO模型不仅可用于SOCKET编程,还可以用于命名管道这样的编程接口。
原文网址:http://gamebabyrocksun.blog.163.com/blog/static/57153463201161485322985/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: