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