WINDOWS完成端口編程 1,、基本概念 2、WINDOWS完成端口的特點 3,、完成端口(Completion Ports )相關數據結構和創(chuàng)建 4,、完成端口線程的工作原理 5、Windows完成端口的實例代碼 Linux的EPoll模型 1,、為什么select落后 2,、內核中提高I/O性能的新方法epoll 3、epoll的優(yōu)點 4,、epoll的工作模式 5,、epoll的使用方法 6、Linux下EPOll編程實例 總結 WINDOWS完成端口編程 摘要:開發(fā)網絡程序從來都不是一件容易的事情,,盡管只需要遵守很少的一些規(guī)則;創(chuàng)建socket,發(fā)起連接,,接受連接,發(fā)送和接受數據。真正的困難在于:讓你的程序可以適應從單單一個連接到幾千個連接乃至于上萬個連接,。利用Windows平臺完成端口進行重疊I/O的技術和Linux在2.6版本的內核中引入的EPOll技術,,可以很方便在在在Windows和Linux平臺上開發(fā)出支持大量連接的網絡服務程序。本文介紹在Windows和Linux平臺上使用的完成端口和EPoll模型開發(fā)的基本原理,,同時給出實際的例子,。本文主要關注C/S結構的服務器端程序,因為一般來說,,開發(fā)一個大容量,,具可擴展性的winsock程序一般就是指服務程序。
1,、基本概念 設備---windows操作系統(tǒng)上允許通信的任何東西,,比如文件、目錄,、串行口,、并行口、郵件槽,、命名管道,、無名管道、套接字,、控制臺,、邏輯磁盤、物理磁盤等,。絕大多數與設備打交道的函數都是CreateFile/ReadFile/WriteFile等,。所以我們不能看到**File函數就只想到文件設備。與設備通信有兩種方式,,同步方式和異步方式,。同步方式下,當調用ReadFile函數時,,函數會等待系統(tǒng)執(zhí)行完所要求的工作,,然后才返回;異步方式下,,ReadFile這類函數會直接返回,,系統(tǒng)自己去完成對設備的操作,然后以某種方式通知完成操作,。 重疊I/O----顧名思義,,當你調用了某個函數(比如ReadFile)就立刻返回做自己的其他動作的時候,同時系統(tǒng)也在對I/0設備進行你要求的操作,,在這段時間內你的程序和系統(tǒng)的內部動作是重疊的,,因此有更好的性能,。所以,重疊I/O是用于異步方式下使用I/O設備的,。 重疊I/O需要使用的一個非常重要的數據結構OVERLAPPED,。
2、WINDOWS完成端口的特點 Win32重疊I/O(Overlapped I/O)機制允許發(fā)起一個操作,,然后在操作完成之后接受到信息,。對于那種需要很長時間才能完成的操作來說,重疊IO機制尤其有用,,因為發(fā)起重疊操作的線程在重疊請求發(fā)出后就可以自由的做別的事情了,。在WinNT和Win2000上,提供的真正的可擴展的I/O模型就是使用完成端口(Completion Port)的重疊I/O.完成端口---是一種WINDOWS內核對象,。完成端口用于異步方式的重疊I/0情況下,,當然重疊I/O不一定非使用完成端口不可,還有設備內核對象,、事件對象,、告警I/0等。但是完成端口內部提供了線程池的管理,,可以避免反復創(chuàng)建線程的開銷,,同時可以根據CPU的個數靈活的決定線程個數,而且可以讓減少線程調度的次數從而提高性能其實類似于WSAAsyncSelect和select函數的機制更容易兼容Unix,,但是難以實現我們想要的“擴展性”,。而且windows的完成端口機制在操作系統(tǒng)內部已經作了優(yōu)化,提供了更高的效率,。所以,我們選擇完成端口開始我們的服務器程序的開發(fā),。 1,、發(fā)起操作不一定完成,系統(tǒng)會在完成的時候通知你,,通過用戶在完成端口上的等待,,處理操作的結果。所以要有檢查完成端口,,取操作結果的線程,。在完成端口上守候的線程系統(tǒng)有優(yōu)化,除非在執(zhí)行的線程阻塞,,不會有新的線程被激活,,以此來減少線程切換造成的性能代價。所以如果程序中沒有太多的阻塞操作,,沒有必要啟動太多的線程,,CPU數量的兩倍,,一般這樣來啟動線程。 2,、操作與相關數據的綁定方式:在提交數據的時候用戶對數據打相應的標記,,記錄操作的類型,在用戶處理操作結果的時候,,通過檢查自己打的標記和系統(tǒng)的操作結果進行相應的處理,。 3、操作返回的方式:一般操作完成后要通知程序進行后續(xù)處理,。但寫操作可以不通知用戶,,此時如果用戶寫操作不能馬上完成,寫操作的相關數據會被暫存到到非交換緩沖區(qū)中,,在操作完成的時候,,系統(tǒng)會自動釋放緩沖區(qū)。此時發(fā)起完寫操作,,使用的內存就可以釋放了,。此時如果占用非交換緩沖太多會使系統(tǒng)停止響應。
3,、完成端口(Completion Ports )相關數據結構和創(chuàng)建 其實可以把完成端口看成系統(tǒng)維護的一個隊列,,操作系統(tǒng)把重疊IO操作完成的事件通知放到該隊列里,由于是暴露 “操作完成”的事件通知,,所以命名為“完成端口”(COmpletion Ports),。一個socket被創(chuàng)建后,可以在任何時刻和一個完成端口聯系起來,。 完成端口相關最重要的是OVERLAPPED數據結構 typedef struct _OVERLAPPED { ULONG_PTR Internal;//被系統(tǒng)內部賦值,,用來表示系統(tǒng)狀態(tài) ULONG_PTR InternalHigh;// 被系統(tǒng)內部賦值,傳輸的字節(jié)數 union { struct { DWORD Offset;//和OffsetHigh合成一個64位的整數,,用來表示從文件頭部的多少字節(jié)開始 DWORD OffsetHigh;//操作,,如果不是對文件I/O來操作,則必須設定為0 }; PVOID Pointer; }; HANDLE hEvent;//如果不使用,,就務必設為0,否則請賦一個有效的Event句柄 } OVERLAPPED, *LPOVERLAPPED;
下面是異步方式使用ReadFile的一個例子 OVERLAPPED Overlapped; Overlapped.Offset=345; Overlapped.OffsetHigh=0; Overlapped.hEvent=0; //假定其他參數都已經被初始化 ReadFile(hFile,buffer,sizeof(buffer),&dwNumBytesRead,&Overlapped); 這樣就完成了異步方式讀文件的操作,,然后ReadFile函數返回,由操作系統(tǒng)做自己的事情,,下面介紹幾個與OVERLAPPED結構相關的函數 等待重疊I/0操作完成的函數 BOOL GetOverlappedResult ( HANDLE hFile, LPOVERLAPPED lpOverlapped,//接受返回的重疊I/0結構 LPDWORD lpcbTransfer,//成功傳輸了多少字節(jié)數 BOOL fWait //TRUE只有當操作完成才返回,,FALSE直接返回,如果操作沒有完成,,通過調//用GetLastError ( )函數會返回ERROR_IO_INCOMPLETE ); 宏HasOverlappedIoCompleted可以幫助我們測試重疊I/0操作是否完成,,該宏對OVERLAPPED結構的Internal成員進行了測試,查看是否等于STATUS_PENDING值,。 一般來說,,一個應用程序可以創(chuàng)建多個工作線程來處理完成端口上的通知事件,。工作線程的數量依賴于程序的具體需要。但是在理想的情況下,,應該對應一個CPU創(chuàng)建一個線程,。因為在完成端口理想模型中,每個線程都可以從系統(tǒng)獲得一個“原子”性的時間片,,輪番運行并檢查完成端口,,線程的切換是額外的開銷。在實際開發(fā)的時候,,還要考慮這些線程是否牽涉到其他堵塞操作的情況,。如果某線程進行堵塞操作,系統(tǒng)則將其掛起,,讓別的線程獲得運行時間,。因此,如果有這樣的情況,,可以多創(chuàng)建幾個線程來盡量利用時間,。 應用完成端口: 創(chuàng)建完成端口:完成端口是一個內核對象,使用時他總是要和至少一個有效的設備句柄進行關聯,,完成端口是一個復雜的內核對象,,創(chuàng)建它的函數是: HANDLE CreateIoCompletionPort( IN HANDLE FileHandle, IN HANDLE ExistingCompletionPort, IN ULONG_PTR CompletionKey, IN DWORD NumberOfConcurrentThreads );
通常創(chuàng)建工作分兩步: 第一步,創(chuàng)建一個新的完成端口內核對象,,可以使用下面的函數: HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads) { return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads); }; 第二步,,將剛創(chuàng)建的完成端口和一個有效的設備句柄關聯起來,可以使用下面的函數: bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey) { HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0); return h==hCompPort; }; 說明 1) CreateIoCompletionPort函數也可以一次性的既創(chuàng)建完成端口對象,,又關聯到一個有效的設備句柄 2) CompletionKey是一個可以自己定義的參數,,我們可以把一個結構的地址賦給它,然后在合適的時候取出來使用,,最好要保證結構里面的內存不是分配在棧上,,除非你有十分的把握內存會保留到你要使用的那一刻。 3) NumberOfConcurrentThreads通常用來指定要允許同時運行的的線程的最大個數,。通常我們指定為0,這樣系統(tǒng)會根據CPU的個數來自動確定,。創(chuàng)建和關聯的動作完成后,,系統(tǒng)會將完成端口關聯的設備句柄、完成鍵作為一條紀錄加入到這個完成端口的設備列表中,。如果你有多個完成端口,,就會有多個對應的設備列表。如果設備句柄被關閉,,則表中自動刪除該紀錄,。
4,、完成端口線程的工作原理 完成端口可以幫助我們管理線程池,但是線程池中的線程需要我們使用_beginthreadex來創(chuàng)建,,憑什么通知完成端口管理我們的新線程呢,?答案在函數GetQueuedCompletionStatus。該函數原型: BOOL GetQueuedCompletionStatus( IN HANDLE CompletionPort, OUT LPDWORD lpNumberOfBytesTransferred, OUT PULONG_PTR lpCompletionKey, OUT LPOVERLAPPED *lpOverlapped, IN DWORD dwMilliseconds ); 這個函數試圖從指定的完成端口的I/0完成隊列中抽取紀錄,。只有當重疊I/O動作完成的時候,,完成隊列中才有紀錄。凡是調用這個函數的線程將被放入到完成端口的等待線程隊列中,,因此完成端口就可以在自己的線程池中幫助我們維護這個線程,。完成端口的I/0完成隊列中存放了當重疊I/0完成的結果---- 一條紀錄,該紀錄擁有四個字段,,前三項就對應GetQueuedCompletionStatus函數的2,、3、4參數,,最后一個字段是錯誤信息dwError,。我們也可以通過調用PostQueudCompletionStatus模擬完成了一個重疊I/0操作。 當I/0完成隊列中出現了紀錄,,完成端口將會檢查等待線程隊列,,該隊列中的線程都是通過調用GetQueuedCompletionStatus函數使自己加入隊列的。等待線程隊列很簡單,,只是保存了這些線程的ID,。完成端口會按照后進先出的原則將一個線程隊列的ID放入到釋放線程列表中,同時該線程將從等待GetQueuedCompletionStatus函數返回的睡眠狀態(tài)中變?yōu)榭烧{度狀態(tài)等待CPU的調度,。所以我們的線程要想成為完成端口管理的線程,,就必須要調用GetQueuedCompletionStatus函數。出于性能的優(yōu)化,,實際上完成端口還維護了一個暫停線程列表,,具體細節(jié)可以參考《Windows高級編程指南》,我們現在知道的知識,,已經足夠了,。 完成端口線程間數據傳遞線程間傳遞數據最常用的辦法是在_beginthreadex函數中將參數傳遞給線程函數,或者使用全局變量,。但是完成端口還有自己的傳遞數據的方法,,答案就在于CompletionKey和OVERLAPPED參數。 CompletionKey被保存在完成端口的設備表中,,是和設備句柄一一對應的,,我們可以將與設備句柄相關的數據保存到CompletionKey中,或者將CompletionKey表示為結構指針,,這樣就可以傳遞更加豐富的內容,。這些內容只能在一開始關聯完成端口和設備句柄的時候做,,因此不能在以后動態(tài)改變。 OVERLAPPED參數是在每次調用ReadFile這樣的支持重疊I/0的函數時傳遞給完成端口的,。我們可以看到,,如果我們不是對文件設備做操作,該結構的成員變量就對我們幾乎毫無作用,。我們需要附加信息,,可以創(chuàng)建自己的結構,然后將OVERLAPPED結構變量作為我們結構變量的第一個成員,,然后傳遞第一個成員變量的地址給ReadFile函數,。因為類型匹配,當然可以通過編譯,。當GetQueuedCompletionStatus函數返回時,,我們可以獲取到第一個成員變量的地址,然后一個簡單的強制轉換,,我們就可以把它當作完整的自定義結構的指針使用,,這樣就可以傳遞很多附加的數據了。太好了,!只有一點要注意,,如果跨線程傳遞,請注意將數據分配到堆上,,并且接收端應該將數據用完后釋放,。我們通常需要將ReadFile這樣的異步函數的所需要的緩沖區(qū)放到我們自定義的結構中,這樣當GetQueuedCompletionStatus被返回時,,我們的自定義結構的緩沖區(qū)變量中就存放了I/0操作的數據,。CompletionKey和OVERLAPPED參數,都可以通過GetQueuedCompletionStatus函數獲得,。 線程的安全退出 很多線程為了不止一次的執(zhí)行異步數據處理,,需要使用如下語句 while (true) { ...... GetQueuedCompletionStatus(...); ...... } 那么如何退出呢,答案就在于上面曾提到的PostQueudCompletionStatus函數,,我們可以用它發(fā)送一個自定義的包含了OVERLAPPED成員變量的結構地址,,里面包含一個狀態(tài)變量,當狀態(tài)變量為退出標志時,,線程就執(zhí)行清除動作然后退出,。
5、Windows完成端口的實例代碼: DWORD WINAPI WorkerThread(LPVOID lpParam) { ULONG_PTR *PerHandleKey; OVERLAPPED *Overlap; OVERLAPPEDPLUS *OverlapPlus, *newolp; DWORD dwBytesXfered; while (1) { ret = GetQueuedCompletionStatus( hIocp, &dwBytesXfered, (PULONG_PTR)&PerHandleKey, &Overlap, INFINITE); if (ret == 0) { // Operation failed continue; } OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol); switch (OverlapPlus->OpCode) { case OP_ACCEPT: // Client socket is contained in OverlapPlus.sclient // Add client to completion port CreateIoCompletionPort( (HANDLE)OverlapPlus->sclient, hIocp, (ULONG_PTR)0, 0); // Need a new OVERLAPPEDPLUS structure // for the newly accepted socket. Perhaps // keep a look aside list of free structures. newolp = AllocateOverlappedPlus(); if (!newolp) { // Error } newolp->s = OverlapPlus->sclient; newolp->OpCode = OP_READ; // This function divpares the data to be sent PrepareSendBuffer(&newolp->wbuf); ret = WSASend( newolp->s, &newolp->wbuf, 1, &newolp->dwBytes, 0, &newolp.ol, NULL); if (ret == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { // Error } } // Put structure in look aside list for later use FreeOverlappedPlus(OverlapPlus); // Signal accept thread to issue another AcceptEx SetEvent(hAcceptThread); break; case OP_READ: // Process the data read // Repost the read if necessary, reusing the same // receive buffer as before memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED)); ret = WSARecv( OverlapPlus->s, &OverlapPlus->wbuf, 1, &OverlapPlus->dwBytes, &OverlapPlus->dwFlags, &OverlapPlus->ol, NULL); if (ret == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) { // Error } } break; case OP_WRITE: // Process the data sent, etc. break; } // switch } // while } // WorkerThread 查看以上代碼,,注意如果Overlapped操作立刻失敗(比如,,返回SOCKET_ERROR或其他非WSA_IO_PENDING的錯誤),,則沒有任何完成通知時間會被放到完成端口隊列里,。反之,則一定有相應的通知時間被放到完成端口隊列,。更完善的關于Winsock的完成端口機制,,可以參考MSDN的Microsoft PlatFormSDK,那里有完成端口的例子,。訪問http://msdn.microsoft.com/library/techart/msdn_servrapp.htm可以獲得更多信息,。 Linux的EPoll模型 Linux 2.6內核中提高網絡I/O性能的新方法-epoll I/O多路復用技術在比較多的TCP網絡服務器中有使用,即比較多的用到select函數,。
1,、為什么select落后 首先,在Linux內核中,,select所用到的FD_SET是有限的,,即內核中有個參數__FD_SETSIZE定義了每個FD_SET的句柄個數,在我用的2.6.15-25-386內核中,,該值是1024,,搜索內核源代碼得到: include/linux/posix_types.h:#define __FD_SETSIZE 1024 也就是說,如果想要同時檢測1025個句柄的可讀狀態(tài)是不可能用select實現的,?;蛘咄瑫r檢測1025個句柄的可寫狀態(tài)也是不可能的。其次,,內核中實現select是用輪詢方法,,即每次檢測都會遍歷所有FD_SET中的句柄,顯然,,select函數執(zhí)行時間與FD_SET中的句柄個數有一個比例關系,,即select要檢測的句柄數越多就會越費時。當然,,在前文中我并沒有提及poll方法,,事實上用select的朋友一定也試過poll,我個人覺得select和poll大同小異,,個人偏好于用select而已,。 2、內核中提高I/O性能的新方法epoll epoll是什么,?按照man手冊的說法:是為處理大批量句柄而作了改進的poll,。要使用epoll只需要這三個系統(tǒng)調用:epoll_create(2), epoll_ctl(2),, epoll_wait(2),。 當然,這不是2.6內核才有的,它是在2.5.44內核中被引進的(epoll(4) is a new API introduced in Linux kernel 2.5.44) Linux2.6內核epoll介紹 先介紹2本書《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》,,以2.4內核講解Linux TCP/IP實現,,相當不錯.作為一個現實世界中的實現,很多時候你必須作很多權衡,,這時候參考一個久經考驗的系統(tǒng)更有實際意義,。舉個例子,linux內核中sk_buff結構為了追求速度和安全,犧牲了部分內存,,所以在發(fā)送TCP包的時候,,無論應用層數據多大,sk_buff最小也有272的字節(jié).其實對于socket應用層程序來說,另外一本書《UNIX Network Programming Volume 1》意義更大一點.2003年的時候,,這本書出了最新的第3版本,,不過主要還是修訂第2版本。其中第6章《I/O Multiplexing》是最重要的,。Stevens給出了網絡IO的基本模型,。在這里最重要的莫過于select模型和Asynchronous I/O模型.從理論上說,AIO似乎是最高效的,,你的IO操作可以立即返回,,然后等待os告訴你IO操作完成。但是一直以來,,如何實現就沒有一個完美的方案,。最著名的windows完成端口實現的AIO,實際上也是內部用線程池實現的罷了,最后的結果是IO有個線程池,,你應用也需要一個線程池...... 很多文檔其實已經指出了這帶來的線程context-switch帶來的代價,。在linux 平臺上,關于網絡AIO一直是改動最多的地方,,2.4的年代就有很多AIO內核patch,最著名的應該算是SGI那個,。但是一直到2.6內核發(fā)布,網絡模塊的AIO一直沒有進入穩(wěn)定內核版本(大部分都是使用用戶線程模擬方法,,在使用了NPTL的linux上面其實和windows的完成端口基本上差不多了),。2.6內核所支持的AIO特指磁盤的AIO---支持io_submit(),io_getevents()以及對Direct IO的支持(就是繞過VFS系統(tǒng)buffer直接寫硬盤,對于流服務器在內存平穩(wěn)性上有相當幫助),。 所以,,剩下的select模型基本上就是我們在linux上面的唯一選擇,其實,,如果加上no-block socket的配置,,可以完成一個"偽"AIO的實現,只不過推動力在于你而不是os而已,。不過傳統(tǒng)的select/poll函數有著一些無法忍受的缺點,,所以改進一直是2.4-2.5開發(fā)版本內核的任務,包括/dev/poll,realtime signal等等,。最終,,Davide Libenzi開發(fā)的epoll進入2.6內核成為正式的解決方案
3、epoll的優(yōu)點 <1>支持一個進程打開大數目的socket描述符(FD) select 最不能忍受的是一個進程所打開的FD是有一定限制的,,由FD_SETSIZE設置,默認值是2048,。對于那些需要支持的上萬連接數目的IM服務器來說顯然太少了,。這時候你一是可以選擇修改這個宏然后重新編譯內核,不過資料也同時指出這樣會帶來網絡效率的下降,,二是可以選擇多進程的解決方案(傳統(tǒng)的Apache方案),,不過雖然linux上面創(chuàng)建進程的代價比較小,但仍舊是不可忽視的,,加上進程間數據同步遠比不上線程間同步的高效,,所以也不是一種完美的方案。不過 epoll則沒有這個限制,,它所支持的FD上限是最大可以打開文件的數目,,這個數字一般遠大于2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統(tǒng)內存關系很大,。 <2>IO效率不隨FD數目增加而線性下降 傳統(tǒng)的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,,不過由于網絡延時,任一時間只有部分的socket是"活躍"的,,但是select/poll每次調用都會線性掃描全部的集合,,導致效率呈現線性下降。但是epoll不存在這個問題,,它只會對"活躍"的socket進行操作---這是因為在內核實現中epoll是根據每個fd上面的callback函數實現的,。那么,只有"活躍"的socket才會主動的去調用 callback函數,,其他idle狀態(tài)socket則不會,,在這點上,epoll實現了一個"偽"AIO,,因為這時候推動力在os內核,。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環(huán)境,,epoll并不比select/poll有什么效率,,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降,。但是一旦使用idle connections模擬WAN環(huán)境,epoll的效率就遠在select/poll之上了,。 <3>使用mmap加速內核與用戶空間的消息傳遞。 這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要內核把FD消息通知給用戶空間,,如何避免不必要的內存拷貝就很重要,,在這點上,epoll是通過內核于用戶空間mmap同一塊內存實現的,。而如果你想我一樣從2.5內核就關注epoll的話,,一定不會忘記手工 mmap這一步的。 <4>內核微調 這一點其實不算epoll的優(yōu)點了,,而是整個linux平臺的優(yōu)點,。也許你可以懷疑linux平臺,但是你無法回避linux平臺賦予你微調內核的能力,。比如,,內核TCP/IP協議棧使用內存池管理sk_buff結構,那么可以在運行時期動態(tài)調整這個內存pool(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成,。再比如listen函數的第2個參數(TCP完成3次握手的數據包隊列長度),,也可以根據你平臺內存大小動態(tài)調整。更甚至在一個數據包面數目巨大但同時每個數據包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網卡驅動架構,。 4,、epoll的工作模式 令人高興的是,2.6內核的epoll比其2.5開發(fā)版本的/dev/epoll簡潔了許多,,所以,,大部分情況下,強大的東西往往是簡單的,。唯一有點麻煩是epoll有2種工作方式:LT和ET,。 LT(level triggered)是缺省的工作方式,并且同時支持block和no-block socket.在這種做法中,,內核告訴你一個文件描述符是否就緒了,,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,,內核還是會繼續(xù)通知你的,,所以,這種模式編程出錯誤可能性要小一點,。傳統(tǒng)的select/poll都是這種模型的代表. ET (edge-triggered)是高速工作方式,,只支持no-block socket。在這種模式下,,當描述符從未就緒變?yōu)榫途w時,,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,,并且不會再為那個文件描述符發(fā)送更多的就緒通知,,直到你做了某些操作導致那個文件描述符不再為就緒狀態(tài)了(比如,,你在發(fā)送,接收或者接收請求,,或者發(fā)送接收的數據少于一定量時導致了一個EWOULDBLOCK 錯誤),。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),,內核不會發(fā)送更多的通知(only once),不過在TCP協議中,,ET模式的加速效用仍需要更多的benchmark確認。 epoll只有epoll_create,epoll_ctl,epoll_wait 3個系統(tǒng)調用,,具體用法請參考http://www./linux-patches/nio-improve.html ,,在http://www./rn/也有一個完整的例子,大家一看就知道如何使用了 Leader/follower模式線程pool實現,,以及和epoll的配合。
5,、 epoll的使用方法 首先通過create_epoll(int maxfds)來創(chuàng)建一個epoll的句柄,,其中maxfds為你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,,之后的所有操作將通過這個句柄來進行操作,。在用完之后,記得用close()來關閉這個創(chuàng)建出來的epoll句柄,。 之后在你的網絡主循環(huán)里面,,每一幀的調用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網絡接口,看哪一個可以讀,,哪一個可以寫了,。基本的語法為: nfds = epoll_wait(kdpfd, events, maxevents, -1); 其中kdpfd為用epoll_create創(chuàng)建之后的句柄,,events是一個epoll_event*的指針,,當epoll_wait這個函數操作成功之后,epoll_events里面將儲存所有的讀寫事件,。max_events是當前需要監(jiān)聽的所有socket句柄數,。最后一個timeout是epoll_wait的超時,為0的時候表示馬上返回,,為-1的時候表示一直等下去,,直到有事件范圍,為任意正整數的時候表示等這么長的時間,,如果一直沒有事件,,則范圍。一般如果網絡主循環(huán)是單獨的線程的話,,可以用-1來等,,這樣可以保證一些效率,,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環(huán)的效率,。 epoll_wait范圍之后應該是一個循環(huán),,遍利所有的事件: for(n = 0; n < nfds; ++n) { if(events[n].data.fd == listener) { //如果是主socket的事件的話,則表示有新連接進入了,,進行新連接的處理,。 client = accept(listener, (struct sockaddr *) &local, &addrlen); if(client < 0){ perror("accept"); continue; } setnonblocking(client); // 將新連接置于非阻塞模式 ev.events = EPOLLIN | EPOLLET; // 并且將新連接也加入EPOLL的監(jiān)聽隊列。 注意,,這里的參數EPOLLIN | EPOLLET并沒有設置對寫socket的監(jiān)聽,,如果有寫操作的話,這個時候epoll是不會返回事件的,,如果要對寫操作也監(jiān)聽的話,,應該是EPOLLIN | EPOLLOUT | EPOLLET ev.data.fd = client; if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) { // 設置好event之后,將這個新的event通過epoll_ctl加入到epoll的監(jiān)聽隊列里面,,這里用EPOLL_CTL_ADD來加一個新的epoll事件,,通過EPOLL_CTL_DEL來減少一個epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監(jiān)聽方式,。 fprintf(stderr, "epoll set insertion error: fd=%d0, client); return -1; } } else // 如果不是主socket的事件的話,,則代表是一個用戶socket的事件,則來處理這個用戶socket的事情,,比如說read(fd,xxx)之類的,,或者一些其他的處理。 do_use_fd(events[n].data.fd); } 對,,epoll的操作就這么簡單,,總共不過4個API:epoll_create, epoll_ctl, epoll_wait和close。 如果您對epoll的效率還不太了解,,請參考我之前關于網絡游戲的網絡編程等相關的文章,。 以前公司的服務器都是使用HTTP連接,,但是這樣的話,,在手機目前的網絡情況下不但顯得速度較慢,,而且不穩(wěn)定,。因此大家一致同意用SOCKET來進行連接,。雖然使用SOCKET之后,,對于用戶的費用可能會增加(由于是用了CMNET而非CMWAP),,但是,,秉著用戶體驗至上的原則,,相信大家還是能夠接受的(希望那些玩家月末收到帳單不后能夠保持克制...),。 這次的服務器設計中,最重要的一個突破,,是使用了EPOLL模型,,雖然對之也是一知半解,,但是既然在各大PC網游中已經經過了如此嚴酷的考驗,相信他不會讓我們失望,,使用后的結果,,確實也是表現相當不錯。在這里,,我還是主要大致介紹一下這個模型的結構,。 6、Linux下EPOll編程實例 EPOLL模型似乎只有一種格式,,所以大家只要參考我下面的代碼,,就能夠對EPOLL有所了解了,代碼的解釋都已經在注釋中:
while (TRUE) { int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT);//等待EPOLL時間的發(fā)生,,相當于監(jiān)聽,,至于相關的端口,需要在初始化EPOLL的時候綁定,。 if (nfds <= 0) continue; m_bOnTimeChecking = FALSE; G_CurTime = time(NULL); for (int i=0; i { try { if (m_events[i].data.fd == m_listen_http_fd)//如果新監(jiān)測到一個HTTP用戶連接到綁定的HTTP端口,,建立新的連接。由于我們新采用了SOCKET連接,,所以基本沒用。 { OnAcceptHttpEpoll (); } else if (m_events[i].data.fd == m_listen_sock_fd)//如果新監(jiān)測到一個SOCKET用戶連接到了綁定的SOCKET端口,,建立新的連接,。 { OnAcceptSockEpoll (); } else if (m_events[i].events & EPOLLIN)//如果是已經連接的用戶,并且收到數據,,那么進行讀入,。 { OnReadEpoll (i); } OnWriteEpoll (i);//查看當前的活動連接是否有需要寫出的數據。 } catch (int) { PRINTF ("CATCH捕獲錯誤\n"); continue; } } m_bOnTimeChecking = TRUE; OnTimer ();//進行一些定時的操作,,主要就是刪除一些短線用戶等,。 } 其實EPOLL的精華,也就是上述的幾段短短的代碼,,看來時代真的不同了,,以前如何接受大量用戶連接的問題,現在卻被如此輕松的搞定,,真是讓人不得不感嘆,,對哪。 總結 Windows完成端口與Linux epoll技術方案是這2個平臺上實現異步IO和設計開發(fā)一個大容量,,具可擴展性的winsock程序指服務程序的很好的選擇,,本文對這2中技術的實現原理和實際的使用方法做了一個詳細的介紹。
|
|