IOCP相關梳理

IOCP相關梳理


I/O completion ports are the mechanism by which an application uses a pool of threads that was created when the application was started to process asynchronous I/O requests. These threads are created for the sole purpose of processing I/O requests. Applications that process many concurrent asynchronous I/O requests can do so more quickly and efficiently by using I/O completion ports than by using creating threads at the time of the I/O request.

I/O完成端口(s)是一種機制,通過這個機制,應用程序在啓動時會首先創建一個線程池,然後該應用程序使用線程池處理異步I/O請求。這些線程被創建的唯一目的就是用於處理I/O請求。對於處理大量併發異步I/O請求的應用程序來說,相比於在I/O請求發生時創建線程來說,使用完成端口(s)它就可以做的更快且更有效率。


基本使用流程:

  1. 創建一個完成端口。第四個參數保持爲0,指定在完成端口上,每個處理器一次只允許執行一個工作者線程。
  2. 判斷系統內到底安裝了多少個處理器。
  3. 創建工作者線程,根據步驟2)得到的處理器信息,在完成端口上,爲已完成的I/O請求提供服務。
  4. 準備好一個監聽套接字,在端口上監聽進入的連接請求。
  5. 使用accept函數,接受進入的連接請求。
  6. 創建一個數據結構,用於容納“單句柄數據”,同時在結構中存入接受的套接字句柄。
  7. 調用CreateIoCompletionPort,將自accept返回的新套接字句柄同完成端口關聯到一起。通過完成鍵(CompletionKey)參數,將單句柄數據結構傳遞給CreateIoCompletionPort。
  8. 開始在已接受的連接上進行I/O操作。在此,我們希望通過重疊I/O機制,在新建的套接字上投遞一個或多個異步WSARecv或WSASend請求。這些I/O請求完成後,一個工作者線程會爲I/O請求提供服務,同時繼續處理未來的I/O請求,稍後便會在步驟3 )指定的工作者例程中,體驗到這一點。
  9. 重複步驟5)~ 8),直至服務器中止。

  • CreateIoCompletionPort

作用:1. 用於創建一個完成端口對象。
   2. 將一個句柄同完成端口關聯到一起。
   
最開始創建一個完成端口時,唯一感興趣的參數便是NumberOfConcurrentThreads(併發線程的數量);前面三個參數都會被忽略。NumberOfConcurrentThreads參數的特殊之處在於,它定義了在一個完成端口上,同時允許執行的線程數量。理想情況下,我們希望每個處理器各自負責一個線程的運行,爲完成端口提供服務,避免過於頻繁的線程“場景”切換。若將該參數設爲0,表明系統內安裝了多少個處理器,便允許同時運行多少個線程!可用下述代碼創建一個I/O完成端口:

hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

工作者線程與完成端口的關係:
成功創建一個完成端口後,便可開始將套接字句柄與對象關聯到一起。但在關聯套接字之前,首先必須創建一個或多個“工作者線程”,以便在I/O請求投遞給完成端口對象後,爲完成端口提供服務。在這個時候,大家或許會覺得奇怪,到底應創建多少個線程,以便爲完成端口提供服務呢?這實際正是完成端口模型顯得頗爲“複雜”的一個方面,因爲服務I/O請求所需的數量取決於應用程序的總體設計情況。在此要記住的一個重點在於,在我們調用CreateIoCompletionPort時指定的併發線程數量,與打算創建的工作者線程數量相比,它們代表的並非同一件事情。早些時候,我們曾建議大家用CreateIoCompletionPort函數爲每個處理器
都指定一個線程(處理器的數量有多少,便指定多少線程)以避免由於頻繁的線程“場景”交換活動,從而影響系統的整體性能。CreateIoCompletionPort函數的NumberOfConcurrentThreads參數明確指示系統:在一個完成端口上,一次只允許n個工作者線程運行。假如在完成端口上創建的工作者線程數量超出n個,那麼在同一時刻,最多隻允許n個線程運行。但實際上,在一段較短的時間內,系統有可能超過這個值,但很快便會把它減少至事先在CreateIoCompletionPort函數中設定的值。那麼,爲何實際創建的工作者線程數量有時要比CreateIoCompletionPort函數設定的多一些呢?這樣做有必要嗎?如先前所述,這主要取決於
應用程序的總體設計情況。假定我們的某個工作者線程調用了一個函數,比如Sleep或WaitForSingleObject,但卻進入了暫停(鎖定或掛起)狀態,那麼允許另一個線程代替它的位置。換言之,我們希望隨時都能執行儘可能多的線程;當然,最大的線程數量是事先在CreateIoCompletionPort調用裏設定好的。這樣一來,假如事先預計到自己的線程有可能暫時處於停頓狀態,那麼最好能夠創建比CreateIoCompletionPort的NumberOfConcurrentThreads參數的值多的線程,以便到時候充分發揮系統的潛力。一旦在完成端口上擁有足夠多的工作者線程來爲I/O請求提供服務,便可着手將套接字句柄同完成端口關聯到一起。這要求我們在一個現有的完成端口上,調用CreateIoCompletionPort函數,同時爲前三個參數——FileHandle,ExistingCompletionPort和CompletionKey——提供套接字的信息。其中, FileHandle參數指定一個要同完成端口關聯在一起的套接字句柄。ExistingCompletionPort參數指定的是一個現有的完成端口。CompletionKey(完成鍵)參數則指定要與某個特定套接字句柄關聯在一起的“單句柄數據”;在這個參數中,應用程序可保存與一個套接字對應的任意類型的信息。之所以把它叫作“單句柄數據”,是由於它只對
應着與那個套接字句柄關聯在一起的數據。可將其作爲指向一個數據結構的指針,來保存套接字句柄;在那個結構中,同時包含了套接字的句柄,以及與那個套接字有關的其他信息。

  • GetQueuedCompletionStatus
BOOL GetQueuedCompletionStatus(
    HANDLE CompletionPort,
    LPDWORD lpNumberOfBytes,
    PULONG_PTR lpCompletionKey,
    LPOVERLAPPED* lpOverlapped,
    DWORD dwMilliseconds
);

參數說明:
CompletionPort參數對應於要在上面等待的完成端口。lpNumberOfBytes參數負責在完成了一次I/O操作後(如WSASend或WSARecv),接收實際傳輸的字節數。lpCompletionKey參數爲原先傳遞進入CreateIoCompletionPort函數的套接字返回“單句柄數據”。如我們早先所述,大家最好將套接字句柄保存在這個“鍵”(Key)中。lpOverlapped參數用於接收完成的I/O操作的重疊結果。這實際是一個相當重要的參數,因爲可用它獲取每個I/O操作的數據。而最後一個參數,dwMilliseconds,用於指定調用者希望等待一個完成數據包在完成端口上出現的時間。假如將其設爲INFINITE,調用會無休止地等待下去。

  • PostQueuedCompletionStatus
BOOL PostQueuedCompletionStatus(
    HANDLE CompletionPort,
    DWORD dwNumberOfBytesTransferred,
    ULONG_PTR dwCompletionKey,
    LPOVERLAPPED lpOverlapped
);

參數說明:
CompletionPort參數指定想向其發送一個完成數據包的完成端口對象。而就dwNumberOfBytesTransferred、dwCompletionKey和lpOverlapped這三個參數來說,每一個都允許我們指定一個值,直接傳遞給GetQueuedCompletionStatus函數中對應的參數。這樣一來,一個工作者線程收到傳遞過來的三個GetQueuedCompletionStatus函數參數後,便可根據由這三個參數的某一個設置的特殊值,決定何時應該退出。例如,可用dwCompletionPort參數傳遞0值,而一個工作者線程會將其解釋成中止指令。一旦所有工作者線程都已關閉,便可使用CloseHandle函數,關閉完成端口,最終安全退出程序。

  • LPFN_ACCEPTEX (擴展函數AcceptEx的指針)
  • LPFN_CONNECTEX(擴展函數ConnectEx的指針)
  • WSAIoctl
  • WSASend(WriteFile,WSASendTo)
  • WSARecv(ReadFile, WSARecvFrom)

對於非重疊(非異步的)的操作,函數返回大於0的值表示操作成功。返回0表示連接中斷,此時需要釋放套接字資源。返回SOCKET_ERROR(-1),表示出錯,使用WSAGetLastError()獲取出錯的原因。.就非重疊操作而言,其語義與標準recv函數是相同的。
對於異步操作,若無錯誤發生且接收操作立即完成,則WSARecv()函數返回0,請注意在這種情況下完成指示(啓動指定的完成例程或設置一個事件對象)將早已發生。否則的話,將返回SOCKET_ERROR錯誤,應用程序可通過WSAGetLastError()來獲取相應的錯誤代碼。錯誤代碼WSA_IO_PENDING表示重疊操作成功啓動,過後將有完成指示。任何其他的錯誤表示重疊操作未能成功地啓動,以後也不會有完成指示。

  • GetLastError

MSDN:
The error code WSA_IO_PENDING indicates that the overlapped operation has been successfully initiated and that completion will be indicated at a later time.

int errcode = GetLastError();
if(errcode == WAIT_TIMEOUT)
{
    return UR_TIME_OUT;
}
else if(errcode == ERROR_OPERATION_ABORTED)
{
    return UR_ERROR;
}
else
{
    sockerr er(errcode);
    DWORD dwTrans;  
    DWORD dwFlags;  
    ::WSAGetOverlappedResult((SOCKET)m_socket, lpoverlapped, &dwTrans, FALSE, &dwFlags); 
    LogInfo("GetQueuedCompletionStatus errcode: ", er.errstr()," WSAGetLastError: ", WSAGetLastError());
    return UR_ERROR;
}
  • CloseHandle

《windows核心編程》:調用closehandle(HANDLE)表示創建者放棄對該內核對象的操作。如果該對象的引用對象記數爲0就撤消該對象。
 關閉一個內核對象。其中包括文件、文件映射、進程、線程、安全和同步對象等。在CreateThread成功之後會返回一個hThread的handle,且內核對象的計數加1,CloseHandle之後,引用計數減1,當變爲0時,系統刪除內核對象。
 若在線程執行完之後,沒有調用CloseHandle,在進程執行期間,將會造成內核對象的泄露,相當於句柄泄露,但不同於內存泄露,這勢必會對系統的效率帶來一定程度上的負面影響。但當進程結束退出後,系統會自動清理這些資源

  • WSAGetOverlappedResult
    返回指定套接口上一個重疊操作的結果。

說明:

A thread uses the GetQueuedCompletionStatus function to wait for a completion packet to be queued to the completion port, rather than waiting directly for the asynchronous I/O to complete. Threads that block their execution on a completion port are released in last-in-first-out (LIFO) order. This means that when a completion packet is queued to the completion port, the system releases the last thread to block its execution on the port.
調用GetQueuedCompletionStatus函數,某個線程就會等待一個完成包進入到完成端口的隊列中,而不是直接等待異步I/O請求完成。線程(們)就會阻塞於它們的運行在完成端口(按照後進先出隊列順序的被釋放)。這就意味着當一個完成包進入到完成端口的隊列中時,系統會釋放最近被阻塞在該完成端口的線程。

When a thread calls GetQueuedCompletionStatus, it is associated with the specified completion port until it exits, specifies a different completion port, or frees the completion port. A thread can be associated with at most one completion port.
調用GetQueuedCompletionStatus,線程就會將會與某個指定的完成端口建立聯繫,一直延續其該線程的存在週期,或被指定了不同的完成端口,或者釋放了與完成端口的聯繫。一個線程只能與最多不超過一個的完成端口發生聯繫。

The most important property of a completion port is the concurrency value. The concurrency value of a completion port is specified when the completion port is created. This value limits the number of runnable threads associated with the completion port. When the total number of runnable threads associated with the completion port reaches the concurrency value, the system blocks the execution of any subsequent threads that specify the completion port until the number of runnable threads associated with the completion port drops below the concurrency value. The most efficient scenario occurs when there are completion packets waiting in the queue, but no waits can be satisfied because the port has reached its concurrency limit. In this case, when a running thread calls GetQueuedCompletionStatus, it will immediately pick up the queued completion packet. No context switches will occur, because the running thread is continually picking up completion packets and the other threads are unable to run.
完成端口最重要的特性就是併發量。完成端口的併發量可以在創建該完成端口時指定。該併發量限制了與該完成端口相關聯的可運行線程的數目。當與該完成端口相關聯的可運行線程的總數目達到了該併發量,系統就會阻塞任何與該完成端口相關聯的後續線程的執行,直到與該完成端口相關聯的可運行線程數目下降到小於該併發量爲止。最有效的假想是發生在有完成包在隊列中等待,而沒有等待被滿足,因爲此時完成端口達到了其併發量的極限。此時,一個正在運行中的線程調用GetQueuedCompletionStatus時,它就會立刻從隊列中取走該完成包。這樣就不存在着環境的切換,因爲該處於運行中的線程就會連續不斷地從隊列中取走完成包,而其他的線程就不能運行了。

發佈了101 篇原創文章 · 獲贊 65 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章