IOCP程式設計之重疊IO
其實這個標題有點“標題黨”的味道,為了大家搜尋方便我故意冠以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程式設計,還可以用於命名管道這樣的程式設計介面。