楊勇
摘 要 完成端口模型(IOCP)在各種網絡并發I/O 處理的模型中,是效率最高的。為進一步提高完成端口的執行性能,可以對模型處理流程中的各步驟作進一步優化。連接池技術可以實現SOCKET的重復利用。對象池技術改善完成端口模型對內存資源的利用效率,WSARecv函數采用零字節投遞處理重疊I/O,可降低操作系統資源開銷。
關鍵詞 IOCP 完成端口 連接池 對象池
中圖分類號:TP393.05 文獻標識碼:A DOI:10.16400/j.cnki.kjdkx.2016.12.016
Abstract The completion port model (IOCP) is the most efficient model for concurrent I/O processing in a variety of network. In order to further improve the performance of the completion port, we can further optimize the steps in the process of model processing. Connection pool technology can be used to achieve the reuse of SOCKET. Object pool technology to improve the completion port model of memory resource utilization efficiency, WSARecv function using zero byte delivery processing overlap I/O, can reduce the operating system resource overhead.
Keywords IOCP; Completion port; connection pool; object pool
0 引言
完成端口(IOCP)對網絡服務器管理多個連接套接字具有非常高的效率,有優秀的系統延展性。與普通多線程模型處理并發連接相比較。完成端口的優勢在于:其一,普通線程模型對于用戶連接是一對一的,一個連接對應一個線程。如果當前在線連接達到千以上,則系統同時運行千個以上的線程,系統運行速度會大幅下降,因為線程創建、退出需要耗費大量系統資源,線程數量太多,線程間切換耗費的CPU時間片也越多。每個線程運行所分到的CPU時間片太少,線程運行速度顯著變慢。 針對多線程模型缺陷,線程池模型(Thread Pool)可以減少建立、退出線程的系統資源開銷。但是對于并發連接高峰時段, 線程池模型并不能減少并發運行線程數量。完成端口則在線程池模型的基礎上做進一步的優化,是目前效率最高,系統資源占用最小的線程池模型。并發線程太多的原因是服務于每一個連接的線程不能快速退出。每個連接在請求和應答過程中,數據傳輸可能由于網絡或者用戶操作等原因造成傳輸延遲,只要數據傳輸全過程未完成,線程即不能退出。完成端口把接收和回傳數據兩個步驟分解到多個線程中單獨完成,因此每一個線程在系統中持續的時間變短,同時在線的線程數量大幅減少。其二,完成端口對數據處理采用異步模式,數據的接收和發送由系統進行,WSARecv,WSASend 函數調用后立即返回。系統處理數據結束后再發消息通知。因此可以同時響應多個連接的請求 。本文探討了進一步優化完成端口I/O管理的幾種方法。
1 完成端口建立過程
建立基于完成端口的網絡服務程序的過程是:(1)創建完成端口對象,調用函數 CreateIoCompletionPort(__in HANDLE FileHandle,__in_opt HANDLE ExistingCompletionPort,__in ULONG_PTR CompletionKey,__in DWORD NumberOfConcurrentThreads);該函數返回完成端口句柄。函數只需設定最后一個參數NumberOfConcurrentThreads的值,指定在完成端口上同時運行的工作線程數量。設為0則表示工作線程數與系統CPU數一樣多。(2)建立接收用戶連接的主線程,在主線程里,創建連接套接字,并把套接字和已經建立的完成端口綁定,該步驟仍然使用CreateIoCompletionPort函數完成,第一個參數就是綁定的套接字,第二個參數是完成端口句柄,第三個參數是與套接字關聯的句柄,通常是一個指針,指向關聯對象,關聯對象可以存儲與套接字有聯系的數據。套接字上可以開始調用WSARecv函數投遞接收數據請求。(3)創建工作線程。工作線程中調用GetQueuedCompletionStatus 函數從系統通知隊列中取出數據接收完成的重疊I/O對象。在工作線程里讀取I/O對象關聯的數據緩沖區,處理數據完畢后,根據需要,可以調用WSASend 函數回傳響應數據,或者調用WSARecv函數投遞下一個接收請求。如果數據處理業務邏輯比較復雜或耗時很長,也可以單獨置于其他線程中完成,完成后再通知工作線程做下一步處理。
2 完成端口性能優化
2.1 連接池技術(Socket Pool)
Windows系統下SOCKET的創建需要消耗很多資源,耗費相當的cpu時間,對于多連接應用,大量SOCKET的創建會使服務器對客戶端的響應延遲。因此我們希望開始時就預先建立好多個SOCKET對象,無需等到客戶連接上來時再創建。另一方面,客戶對服務器的連接狀態變化非常頻繁,典型的如web服務器,SOCKET頻繁的創建和銷毀,降低服務器的性能。我們希望連接斷開的SOCKET,不再簡單地銷毀掉,而是放入一個池中,在需要的時候重用這個SOCKET,減少頻繁創建銷毀SOCKET對象而帶來的性能損失, winsock2庫提供了一個新的AcceptEx函數來取代過去的accept函數:
BOOL AcceptEx(
__in SOCKET sListenSocket,
__in SOCKET sAcceptSocket,
__in PVOID lpOutputBuffer,
__in DWORD dwReceiveDataLength,
__in DWORD dwLocalAddressLength,
__in DWORD dwRemoteAddressLength,
__out LPDWORD lpdwBytesReceived,
__in LPOVERLAPPED lpOverlapped
);
AcceptEx功能與accept類似,用于接受連接請求,第一個參數含義與accept一樣,為監聽SOCKET。我們知道,accept接受連接后,再創建一個SOCKET,作為函數返回值。而AcceptEx 要求提前用 WSASocket函數創建SOCKET ,傳遞給第二個參數sAcceptSocket,AcceptEx函數并不阻塞等待客戶連接到sAcceptSocket,而是立即返回。因此我們可以在一個循環里多次創建SOCKET,并多次調用AcceptEx函數,預先建立好多個SOCKET以等待客戶的連接,由于第一個參數監聽sListenSocket綁定到完成端口,AcceptEx類似于WSARecv非阻塞的重疊調用。最后一個參數lpOverlapped指定重疊數據結構,一般將sAcceptSocket包含在這個結構中,當連接完成時,完成包由完成端口置入通知隊列,再由工作線程處理已經真正建立連接的sAcceptSocket。
當連接斷開時,不采用closesocket函數關閉連接,回收資源,而是采用DisconnectEx函數:BOOL DisconnectEx( __in SOCKET hSocket, __in LPOVERLAPPED lpOverlapped, __in DWORD dwFlags, __in DWORD reserved);
該函數回收而不是關閉SOCKET,第二個參數必須取TF_REUSE_SOCKET。之后重新綁定回收的套接字hSocekt到完成端口,然后再次調用AcceptEx 函數,將其放入連接池。
2.2 對象池技術(Object Pool)
重疊I/O模型,每建立一個套接字連接,都要在堆區創建與之關聯的重疊I/O對象,作為WSASend或WSARec函數的參數。而當連接關閉時,I/O對象隨之被銷毀,對象反復創建銷毀導致堆區內存反復分配、釋放,會使系統中出現大量的內存碎片,降低內存的利用效率。應用對象池技術可以解決內存碎片的問題。構建一個對象容器,需要時從對象池中取出一個空閑對象,用完后并不釋放,而是放到對象容器中以供下一次再利用。省卻了內存分配、釋放過程的系統開銷;放回對象池的對象在內存中的位置并沒有變化,僅僅是內容被重置,因而不會產生內存碎片。可用對象池模版類來實現,根據具體需求再實例化模版類。下面給出示例代碼:
template
{
deque
deque
TCritic crticobj; //臨界區對象
public:
T* GetObj () {
T* pObj;
crticobj.Critic(); //進入保護區
if(free_set.size()>0 ) {
pObj = free_set.front();
free_set.pop_front();
}
else{
pObj = new T;
obj_set.push_back(pObj);
}
crticobj.UnCritic ();//離開保護區
return pObj;
}
void FreeObj ( T* pObj) {
((T*) pObj)->Reset(); //重置對象
crticobj.Critic();
free_set.push_back(pObj); //放回空閑對象容器
crticobj.UnCritic ();
}
};
模版類ObjPool 使用stl deque容器類保存空閑對象指針。deque類型的容器能快速在數組頭部彈出元素和尾部添加元素。類的成員函數GetObj ()負責提供可用對象,如果空閑對象集合free_set中有可用對象,則先從free_set頭部取出一個空閑對象指針。再將該指針從free_set中彈出。如果free_set為空,則創建一個新的可用對象,并把對象指針保存到總對象集obj_set中。對free_set的讀取操作需要在各線程間同步。不能有兩個線程同時訪問free_set。讀取操作要設置臨界區加以保護:crticobj為一個臨界區對象,crticobj.Critic()進入保護區,crticobj.UnCritic ()離開保護區。成員函數Reset()重置對象,由于不同類型的對象重置方法不同,當模版類ObjPool實例化時再具體定義Reset()函數。
2.3 減少WSARecv調用的系統資源消耗。
由于WSARecv函數的異步特性,調用后立即返回,可以服務于大量的連接請求。但每一次WSARecv調用,系統都會為之分配接收數據的緩沖區,即使只接收一個字節,系統也會分配最小單元為4k的內存,且在數據接收未完成時,分配的緩沖區將被系統鎖定。如果WSARecv調用過多,將有大量非分頁內存被鎖定,一旦達到系統鎖定內存值的上限。 WSARecv就會返回“WSAENOBUFS”的錯誤。 所以,當系統尚未真正收到數據而處于等待狀態時,只請求一個0字節的緩沖區,內存鎖定值為零,無論投遞多少請求都不會出現系統資源耗盡的問題。當系統收到數據,完成端口收到一個零字節的完成包。 相當于數據到來時的“通知”。此時再調用WSARecv函數投遞非0字節緩沖區接收數據。但是,當客戶連接斷開時,也會收到0字節的數據。區分這兩種情況的方法是,判斷完成包數據緩沖區的大小,如果是零,則表明是零字節投遞的結果,否則,是客戶斷開連接套接字關閉的結果。下面給(下轉第81頁)(上接第36頁)出示例代碼:
GetQueuedCompletionStatus( IOCP_handle,&dwRecv…) ;
if( dwRec ==0) //收到零字節數據
{
if( PerIO->buffer.len == 0){ //零字節WSARecv調用
//再次投遞緩沖區大小為buffersize的接收請求
IOCPobj->IOCP_Recv(PerIO, buffersize,NULL );
}
else IOCPobj ->IOCP_Error(PerIO); //客戶連接斷開了,處理斷開錯誤。
}
3 結語
本文分析了完成端口多線程模型并發處理運行機制,提出了幾種提高完成端口運行效率,減少系統開銷的方法,這些方法已經成功運用在多個網絡服務系統的開發中。
注釋
① 王艷平.Windows網絡與通信程序設計[M].人民郵電出版社,2009.