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編程,還可以用於命名管道這樣的編程接口。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM