IOCP 完成埠

xianjian_x發表於2016-05-14

什麼是IOCP

先讓我們看看對IOCP的評價

I/O完成埠可能是Win32提供的最複雜的核心物件。[Advanced Windows 3rd] Jeffrey Richter
IOCP實現高容量網路伺服器的最佳方法。[Windows Sockets2.0:Write Scalable Winsock Apps Using Completion Ports]  
完成埠模型提供了最好的伸縮性。這個模型非常適用來處理數百乃至上千個套接字。 [Windows網路程式設計2nd] Anthony Jones & Jim Ohlund
I/O completion ports特別顯得重要,因為它們是唯一適用於高負載伺服器[必須同時維護許多連線線路]的一個技術。Completion ports利用一些執行緒,幫助平衡由I/O請求所引起的負載。這樣的架構特別適合用在SMP系統中產生的 scalable 伺服器。[Win32多執行緒程式設計] Jim Beveridge & Robert Wiener 

  看來我們完全有理由相信IOCP是大型網路架構的首選。那IOCP到底是什麼呢?

  微軟在Winsock2中引入了IOCP這一概念 。IOCP全稱I/O Completion Port,中文譯為I/O完成埠。IOCP是一個非同步I/O的API,它可以高效地將I/O事件通知給應用程式。與使用select()或是其它非同步方法不同的是,一個套接字[socket]與一個完成埠關聯了起來,然後就可繼續進行正常的Winsock操作了。然而,當一個事件發生的時候,此完成埠就將被作業系統加入一個佇列中。然後應用程式可以對核心層進行查詢以得到此完成埠。

  這裡我要對上面的一些概念略作補充,在解釋[完成]兩字之前,我想先簡單的提一下同步和非同步這兩個概念,邏輯上來講做完一件事後再去做另一件事就是同步,而同時一起做兩件或兩件以上事的話就是非同步了。你也可以拿單執行緒和多執行緒來作比喻。但是我們一定要將同步和堵塞,非同步和非堵塞區分開來,所謂的堵塞函式諸如accept(…),當呼叫此函式後,此時執行緒將掛起,直到作業系統來通知它,”HEY兄弟,有人連進來了”,那個掛起的執行緒將繼續進行工作,也就符合”生產者-消費者”模型。

  阻塞和同步看上去有兩分相似,但卻是完全不同的概念。大家都知道I/O裝置是個相對慢速的裝置,不論印表機,調變解調器,甚至硬碟,與CPU相比都是奇慢無比的,坐下來等I/O的完成是一件不甚明智的事情,有時候資料的流動率非常驚人,把資料從你的檔案伺服器中以Ethernet速度搬走,其速度可能高達每秒一百萬位元組,如果你嘗試從檔案伺服器中讀取100KB,在使用者的眼光來看幾乎是瞬間完成,但是,要知道,你的執行緒執行這個命令,已經浪費了10個一百萬次CPU週期。所以說,我們一般使用另一個執行緒來進行I/O。
  重疊IO[overlapped I/O]是Win32的一項技術,你可以要求作業系統為你傳送資料,並且在傳送完畢時通知你。這也就是[完成]的含義。這項技術使你的程式在I/O進行過程中仍然能夠繼續處理事務。事實上,作業系統內部正是以執行緒來完成overlapped I/O。你可以獲得執行緒所有利益,而不需要付出什麼痛苦的代價。

  完成埠中所謂的[埠]並不是我們在TCP/IP中所提到的埠,可以說是完全沒有關係。我到現在也沒想通一個I/O裝置[I/O Device]和埠[IOCP中的Port]有什麼關係。估計這個埠也迷惑了不少人。IOCP只不過是用來進行讀寫操作,和檔案I/O倒是有些類似。既然是一個讀寫裝置,我們所能要求它的只是在處理讀與寫上的高效。在文章的第三部分你會輕而易舉的發現IOCP設計的真正用意。
IOCP和網路又有什麼關係?

int main()
{
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    ListeningSocket = socket(AF_INET, SOCK_STREAM, 0); 
    bind(ListeningSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
    listen(ListeningSocket, 5);
    int nlistenAddrLen = sizeof(ClientAddr);
    while(TRUE)
    {
        NewConnection = accept(ListeningSocket, (SOCKADDR*)&ClientAddr, &nlistenAddrLen);
        HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, (void*) NewConnection, 0, &dwTreadId);
        CloseHandle(hThread);
    }
    return 0;
}

  相信只要寫過網路的朋友,應該對這樣的結構在熟悉不過了。accept後執行緒被掛起,等待一個客戶發出請求,而後建立新執行緒來處理請求。當新執行緒處理客戶請求時,起初的執行緒迴圈回去等待另一個客戶請求。處理客戶請求的執行緒處理完畢後終結。

  在上述的併發模型中,對每個客戶請求都建立了一個執行緒。其優點在於等待請求的執行緒只需做很少的工作。大多數時間中,該執行緒在休眠[因為recv處於堵塞狀態]。
  但是當併發模型應用在伺服器端[基於Windows NT],Windows NT小組注意到這些應用程式的效能沒有預料的那麼高。特別的,處理很多同時的客戶請求意味著很多執行緒併發地執行在系統中。因為所有這些執行緒都是可執行的[沒有被掛起和等待發生什麼事],Microsoft意識到NT核心花費了太多的時間來轉換執行執行緒的上下文[Context],執行緒就沒有得到很多CPU時間來做它們的工作。

  大家可能也都感覺到並行模型的瓶頸在於它為每一個客戶請求都建立了一個新執行緒。建立執行緒比起建立程式開銷要小,但也遠不是沒有開銷的。

  我們不妨設想一下:如果事先開好N個執行緒,讓它們在那hold[堵塞],然後可以將所有使用者的請求都投遞到一個訊息佇列中去。然後那N個執行緒逐一從訊息佇列中去取出訊息並加以處理。就可以避免針對每一個使用者請求都開執行緒。不僅減少了執行緒的資源,也提高了執行緒的利用率。理論上很不錯,你想我等泛泛之輩都能想出來的問題,Microsoft又怎會沒有考慮到呢?!

  這個問題的解決方法就是一個稱為I/O完成埠的核心物件,他首次在Windows NT3.5中被引入。
  其實我們上面的構想應該就差不多是IOCP的設計機理。其實說穿了IOCP不就是一個訊息佇列嘛!你說這和[埠]這兩字有何聯絡。我的理解就是IOCP最多是應用程式和作業系統溝通的一個介面罷了。
  至於IOCP的具體設計那我也很難說得上來,畢竟我沒看過實現的程式碼,但你完全可以進行模擬,只不過效能可能…,如果想深入理解IOCP, Jeffrey Ritchter的Advanced Windows 3rd其中第13章和第14張有很多寶貴的內容,你可以拿來窺視一下系統是如何完成這一切的。

實現方法

  Microsoft為IOCP提供了相應的API函式,主要的就兩個,我們逐一的來看一下:

HANDLE CreateIoCompletionPort (
        HANDLE FileHandle,                  // handle to file
        HANDLE ExistingCompletionPort,      // handle to I/O completion port
        ULONG_PTR CompletionKey,            // completion key
        DWORD NumberOfConcurrentThreads     // number of threads to execute concurrently
);

  在討論各引數之前,首先要注意該函式實際用於兩個截然不同的目的:

1.用於建立一個完成埠物件
2.將一個控制程式碼[HANDLE]和完成埠關聯到一起

  在建立一個完成一個埠的時候,我們只需要填寫一下NumberOfConcurrentThreads這個引數就可以了。它告訴系統一個完成埠上同時允許執行的執行緒最大數。在預設情況下,所開執行緒數和CPU數量相同,但經驗給我們一個公式:

  執行緒數 = CPU數 * 2 + 2

  要使完成埠有用,你必須把它同一個或多個裝置相關聯。這也是呼叫CreateIoCompletionPort完成的。你要向該函式傳遞一個已有的完成埠的控制程式碼,我們既然要處理網路事件,那也就是將客戶的socket作為HANDLE傳進去。和一個完成鍵[對你有意義的一個32位值,也就是一個指標,作業系統並不關心你傳什麼]。每當你向埠關聯一個裝置時,系統向該完成埠的裝置列表中加入一條資訊紀錄。

另一個API就是

BOOL GetQueuedCompletionStatus(
    HANDLE CompletionPort,       // handle to completion port
    LPDWORD lpNumberOfBytes,     // bytes transferred
    PULONG_PTR lpCompletionKey,  // file completion key
    LPOVERLAPPED *lpOverlapped,  // buffer
    DWORD dwMilliseconds         // optional timeout value
);

  第一個引數指出了執行緒要監視哪一個完成埠。很多服務應用程式只是使用一個I/O完成埠,所有的I/O請求完成以後的通知都將發給該埠。簡單的說,GetQueuedCompletionStatus使呼叫執行緒掛起,直到指定的埠的I/O完成佇列中出現了一項或直到超時。同I/O完成埠相關聯的第3個資料結構是使執行緒得到完成I/O項中的資訊:傳輸的位元組數,完成鍵和OVERLAPPED結構的地址。該資訊是通過傳遞給GetQueuedCompletionSatatuslpdwNumberOfBytesTransferredlpdwCompletionKeylpOverlapped引數返回給執行緒的。

  根據到目前為止已經講到的東西,首先來構建一個frame。下面為您說明了如何使用完成埠來開發一個echo伺服器。大致如下:

1.初始化Winsock
2.建立一個完成埠
3.根據伺服器執行緒數建立一定量的執行緒數
4.準備好一個 socket 進行 bind 然後 listen
5.進入迴圈 accept 等待客戶請求
6.建立一個資料結構容納 socket 和其他相關資訊
7.將連進來的socket同完成埠相關聯
8.投遞一個準備接受的請求

以後就不斷的重複5至8的過程
那好,我們用具體的程式碼來展示一下細節的操作。

實現程式碼

服務端下載
github
https://github.com/xushichao/IOCP-server

客戶端下載

參考連結:
理解I/O完成埠模型

寫出最好的IOCP伺服器,關鍵的幾個問題

相關文章