理解完成端口(IO completion port)

 

    關於完成端口網上有很多文章,不過我個人覺得大多都講得不夠清楚。給的例子要不就是給一個複雜的封裝,要不就是給一個簡單的收發數據。注意,完成端口不僅僅用於網絡數據的收發,它可以用於windows 平臺的各種IO操作。不過我這裏只關注在winsock編程中的應用。

    要寫出一篇真的讓人能夠明白的文章,不那麼容易。這裏我只暫時貼些我的理解。遲些時候如果有空的話,我倒有興趣寫個詳細的入門文章。

 

1.26.2008

Kevin Lynx

 

理解完成端口:

 

       就目前所瞭解的信息來說,完成端口通常都會與重疊IO有關聯。完成端口可被看作是一個隊列。各種操作都會被放到該隊列裏,程序在遲些時候查詢此隊列獲取之前提交的IO操作結果。

       注意,無論是重疊IO還是完成端口,都不僅僅用於socket的操作,他們是用於各種IO的操作。

 


IOCP
不是爲每一個客戶端連接建立一個線程。

要區分IOCP和事件通知模型的區別,事件通知模型是先得到事件,然後根據事件類型去獲取或者發送數據;而IOCP則是先提交動作(發送或接收),後得到通知,當得到通知時,通常就意味着之前提交的動作已經完成了。

 

When you use IOCP, you spawn a pool of threads once - and they are used to handle the network I/O in your application. Technically, in Windows 2000, you don't even have to spawn the pool yourself - you can let Windows take care of the spawning and management of the threads in the pool

IOCP其實也屬於一種同步對象,就像Windows裏的Event對象一樣。IOCP用於同步IO操作。在異步IO操作中,提交了一個異步操作後,某些時候就需要得知操作的結果,也就是同步一下。

 

http://www.codeproject.com/KB/IP/iocp_server_client.aspx

A server application is fairly meaningless if it cannot service multiple clients at the same time, usually asynchronous I/O calls and multithreading is used for this purpose. By definition, an asynchronous I/O call returns immediately, leaving the I/O call pending. At some point of time, the result of the I/O asynchronous call must be synchronized with the main thread. This can be done in different ways. The synchronization can be performed by:

  • Using events - A signal is set as soon as the asynchronous call is finished. The disadvantage of this approach is that the thread has to check or wait for the event to be set.

  • Using the GetOverlappedResult function - This approach has the same disadvantage as the approach above.

  • Using Asynchronous Procedure Calls (or APC) - There are several disadvantages associated with this approach. First, the APC is always called in the context of the calling thread, and second, in order to be able to execute the APCs, the calling thread has to be suspended in the so called alterable wait state.

  • Using IOCP - The disadvantage of this approach is that there are many practical thorny programming problems that must be solved. Coding IOCP can be a bit of a hassle.

 

這裏我要特別強調一下異步IO和非阻塞IO的區別,異步IO就是把IO提交給系統,讓系統替你做,做完了再用某種方式通知你;非阻塞IO就是你要通過某種方式不定時地向系統詢問你是否可以開始做某個IO,當可以開始後,還是要自己來完成IO

 

       不可以通過接受數據是否爲0來判斷客戶端是否斷開,只有當調用closesocket時纔可以通過這個方法判斷,如果是意外退出(斷電,網絡故障),則判斷不出。這個時候可以採用定時發送數據(心跳信息)來確認。

 

       創建IOCP程序,一般的步驟:

1.       創建一個單句柄數據結構體,該結構體裏一般都包含一個套接字數據。因爲IOCP實際上會有固定的幾個線程(工作線程),這些線程在IOCP結果隊列裏查詢IO操作結果。這些結果不止是在一個套接字上進行的操作(讀或寫),而是包括了所有與該IOCP對象關聯起來的套接字上的操作結果。因此,爲了區分某次操作結果屬於哪個套接字,就需要這個單句柄數據結構裏包含這個套接字句柄。

2.       創建一個以OVERLAPPED爲首個元素的數據結構體。該結構體實際上對應着一個IO操作(例如WSASend)。對於 WSASend, WSARecv以及查詢操作結果函數都需要一個OVERLAPPED參數(一般是指針),通常情況下我們需要更多的數據,因此定義的這個結構體裏通常包含了更多的數據(例如WSABUF,它可以用來容納WSARecv接受到的數據)。之所以要把OVERLAPPED 作爲這個結構體的第一個元素,是爲了在使用查詢函數GetQueuedCompletionStatus後,可以通過該函數返回的OVERLAPPED類型的指針得到我們這裏定義的結構體對象地址,從而獲取更多的數據。

3.       每一次接受到新的連接時(accept),都將這個新的套接字與完成端口相關聯。並且創建一個單句柄對象(也就是完成鍵)。每一個套接字都有一個關聯的單句柄對象。而每一個IO操作都有一個關聯的OVERLAPPED相關的數據結構(上一步定義的結構體)。

4.       可以在任何時候提交異步IO請求,例如WSASend, WSARecv。這裏需要爲OVERLAPPED相關的結構體指定操作類型。一個典型的結構體爲(即第二步定義的結構體):

              struct IOContext

{

              /// 很多函數需要此參數

              OVERLAPPED ol;

              /// 存放接受數據

              char buf[MAX_BUF];

              ///

              WSABUF wsabuf;

              /// 操作類型,提交IO操作時指定該值,在查詢操作結果時,可以重新獲取到該值

              int op_type;

};

在創建該結構體的變量時,爲op_type指定一個值。然後將此結構體的地址給WSASend之類的函數。在工作者線程中執行查詢時,實際上得到了該結構體的地址(結構體變量),那麼,就可以獲取op_type的值。

注意:查詢結構只能獲取IO操作的字節數(以及IO操作結果數據),不能知道IO操作的類型。所以IO操作的類型實際上是在這裏用戶自己指定的。

當執行WSASend時,設置op_typeSEND(自己定義的常量),執行WSARecv時,指定READ。然後在查詢結果時,可以根據op_type知道這個操作結果是什麼類型。如果是SEND,那麼就表示之前提交的WSASend操作。

IOCP是一個異步操作機制,之所以是異步,就是因爲可以隨時提交IO操作。提交之後具體的操作由系統爲你完成。完成後就需要某種機制來得知操作結果。IOCP設置的這個結果隊列就是一種機制。

 

      

 

5.       可以通過PostQueuedCompletionStatus手動地往結果隊列裏放置一個操作結果。通常這個函數都用於讓工作者線程退出。例如:

                   PostQueuedCompletionStatus( cp_handle, 0, NULL, NULL );

然後在工作者線程裏:

         ret = GetQueuedCompletionStatus( cp_handle, &transfer_bytes, (PULONG_PTR) &hc,

              (LPOVERLAPPED*)&ic, INFINITE );

         if( ic == NULL )

         {

              printf( "ic == NULL/n" );

              /*

                   使用PostQueuedCompletionStatus傳遞過來的數據,

                   這裏約定ic==NULL時退出

              */

              break;

         }

        

6.     如果不提交任何IO操作,那麼結果隊列裏很有可能一直都是空的。那麼GetQueued這個查詢函數就會一直得不到數據。

 

7.     縱觀IOCP程序,一個比較複雜的地方在於資源的釋放。在接收到一個新的連接時,會爲這個連接創建單句柄數據,執行IO操作的話,還要創建OVERLAPPED相關結構體變量。這些變量的地址都會在工作者線程中通過GetQueued..函數獲取,並在工作者線程中使用。一個比較直接的做法是在工作者線程中釋放這些資源。

 

 

 

推薦些文章:

幾種socket模型的代碼:http://blog.csdn.net/mlite/archive/2006/04/30/699340.aspx 

                  又一個簡單的IOCP代碼:http://www.go321.cn/html/app/cpp/20070526/30207.html

                  另一個例子,那幅圖有點意義:http://www.3800hk.com/Article/cxsj/vc/wllbcvc/2005-08-25/Article_54111.html

                  codeproject上的文章:http://www.codeproject.com/KB/IP/iocp_server_client.aspx

                  codeproject上的另一篇:http://www.codeproject.com/KB/IP/jbsocketserver1.aspx

                  MSDN上的,前部分由點意義:http://msdn.microsoft.com/msdnmag/issues/1000/winsock/

                

                 其實完成端口的例子在細節上有很多方法,例如accept, AcceptEx之類,對於accept的處理尤其多。這些無疑又給初學者帶來了迷惑。我覺得只要把握住幾個要點就行了:異步操作,結果隊列,數據的傳送(提交操作時傳進去,查詢時取出來),工作者線程。

                        

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章