epoll可是目前在linux下開發大規模并發網絡程式的熱門人選,epoll 在linux2.6核心中正式引入,和select相似,其實都i/o多路複用技術而已,并沒有什麼神秘的。
其實在linux下設計并發網絡程式,向來不缺少方法,比如典型的apache模型(process per connection,簡稱ppc),tpc(thread perconnection)模型,以及select模型和poll模型,那為何還要再引入epoll這個東東呢?那還是有得說說的…
如果不擺出來其他模型的缺點,怎麼能對比出epoll的優點呢。
這兩種模型思想類似,就是讓每一個到來的連接配接一邊自己做事去,别再來煩我。隻是ppc是為它開了一個程序,而tpc開了一個線程。可是别煩我是有代價的,它要時間和空間啊,連接配接多了之後,那麼多的程序/線程切換,這開銷就上來了;
是以這類模型能接受的最大連接配接數都不會高,一般在幾百個左右。
多程序多線程的模型龐大而且繁瑣,是以我們出現了select模型
1
2
3
4
5
6
7
8
select系統調用是用來讓我們的程式監視多個檔案句柄(file descrīptor)的狀态變化的。通過select()系統調用來監視多個檔案描述符的數組,當select()傳回後,該數組中就緒的檔案描述符便會被核心修改标志位,使得程序可以獲得這些檔案描述符進而進行後續的讀寫操作。
select系統調用是用來讓我們的程式監視多個檔案描述符的狀态變化的。程式會停在select這裡等待,直到被監視的檔案描述符有某一個或多個發生了狀态改變。
select()的機制中提供一fd_set的資料結構,實際上是一long類型的數組,每一個數組元素都能與一打開的檔案句柄建立聯系,建立聯系的工作由程式員完成,當調用select()時,由核心根據io狀态修改fd_set的内容,由此來通知執行了select()的程序哪些socket或檔案可讀可寫。
當某些描述符可以讀寫之後,select傳回資料(沒有資料讀寫時,select也會傳回,因為select是同步)時就掃描一遍描述符fd_set來查詢那些有資料請求的描述符,并進行處理。時間複雜度為o(n)
是以性能比那些阻塞的多程序或者多線程模型性能提高不少,但是仍然不夠。因為select有很多限制
linux 下 fd_set 是個 1024 位的位圖,每個位代表一個 fd 的值,傳回後需要掃描位圖,這也是效率低的原因。性能問題且不提,正确性問題則更值得重視。 因為這是一個 1024 位的位圖,是以當程序内的 fd 值 >= 1024 時,就會越界,可能會造成崩潰。對于伺服器程式,fd >= 1024 很容易達到,隻要連接配接數 + 打開的檔案數足夠大即可發生。
1 2 3 4 1 2 3 4
效率問題,select每次調用都會線性掃描全部的fd集合,這樣效率就會呈現線性下降,把fd_setsize改大的後果就是,大家都慢慢來,什麼?都逾時了??!!
核心/使用者空間 記憶體拷貝問題,如何讓核心把fd消息通知給使用者空間呢?在這個問題上select采取了記憶體拷貝方法。
poll的實作和select非常相似,隻是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多。
他通過注冊一堆事件組,當有事件請求時傳回,然後仍然需要輪詢一遍pollfd才能知道查找到對應的檔案描述符,資料也需要在核心空間和使用者空間來回拷貝。時間複雜度為o(n)
是以他隻解決了select的問題1,但是問題2,3仍然得不帶解決。

把其他模型逐個批判了一下,再來看看epoll的改進之處吧,其實把select的缺點反過來那就是epoll的優點了。
epoll沒有最大并發連接配接的限制,上限是最大可以打開檔案的數目,這個數字一般遠大于2048, 一般來說這個數目和系統記憶體關系很大,具體數目可以cat /proc/sys/fs/file-max察看。
效率提升,epoll最大的優點就在于它隻管你“活躍”的連接配接,而跟連接配接總數無關,是以在實際的網絡環境中,epoll的效率就會遠遠高于select和poll。
記憶體拷貝,epoll在這點上使用了“共享記憶體”,這個記憶體拷貝也省略了。
epoll既然是對select和poll的改進,就避免上述的三個缺點。那epoll都是怎麼解決的呢?
在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都隻提供了一個函數——select或者poll函數。
而epoll提供了三個函數,<code>epoll_create</code>,<code>epoll_ctl</code>和<code>epoll_wait</code>,
epoll_create是建立一個epoll句柄;
epoll_ctl是注冊要監聽的事件類型;
epoll_wait則是等待事件的産生。
對于第一個缺點并發數目限制
epoll沒有這個限制,它所支援的fd上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1gb記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。
select 最不能忍受的是一個程序所打開的fd是有一定限制的,由fd_setsize設定,預設值是2048。對于那些需要支援的上萬連接配接數目的im伺服器來說顯 然太少了。這時候你一是可以選擇修改這個宏然後重新編譯核心,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多程序的解決方案(傳統的 apache方案),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上線程間同步的高效,是以也不是一種完 美的方案。不過 epoll則沒有這個限制,它所支援的fd上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1gb記憶體的機器上大約是10萬左 右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。
對于第二個缺點輪詢描述符的線性複雜度
epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待隊列中,而隻在epoll_ctl時把current挂一遍(這一遍必不可少)并為每個fd指定一個回調函數,當裝置就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒連結清單)。epoll_wait的工作實際上就是在這個就緒連結清單中檢視有沒有就緒的f
傳統的select/poll另一個緻命弱點就是當你擁有一個很大的socket集合,不過由于網絡延時,任一時間隻有部分的socket是”活躍”的, 但是select/poll每次調用都會線性掃描全部的集合,導緻效率呈現線性下降。但是epoll不存在這個問題,它隻會對”活躍”的socket進行 操作—這是因為在核心實作中epoll是根據每個fd上面的callback函數實作的。那麼,隻有”活躍”的socket才會主動的去調用 callback函數,其他idle狀态socket則不會,在這點上,epoll實作了一個”僞”aio,因為這時候推動力在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的—比如一個高速lan環境,epoll并不比select/poll有什麼效率,相 反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模拟wan環境,epoll的效率就遠在select/poll之上了。
對于第三缺點資料在核心空間和使用者空間的拷貝
epoll的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定epoll_ctl_add),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中隻會拷貝一次。
這點實際上涉及到epoll的具體實作了。無論是select,poll還是epoll都需要核心把fd消息通知給使用者空間,如何避免不必要的記憶體拷貝就 很重要,在這點上,epoll是通過核心于使用者空間mmap同一塊記憶體實作的。而如果你想我一樣從2.5核心就關注epoll的話,一定不會忘記手工 mmap這一步的。
(1)select,poll實作需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒連結清單,期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,調用回調函數,把就緒fd放入就緒連結清單中,并喚醒在epoll_wait中進入睡眠的程序。雖然都要睡眠和交替,但是select和poll在“醒着”的時候要周遊整個fd集合,而epoll在“醒着”的時候隻要判斷一下就緒連結清單是否為空就行了,這節省了大量的cpu時間。這就是回調機制帶來的性能提升。
(2)select,poll每次調用都要把fd集合從使用者态往核心态拷貝一次,并且要把current往裝置等待隊列中挂一次,而epoll隻要一次拷貝,而且把current往等待隊列上挂也隻挂一次(在epoll_wait的開始,注意這裡的等待隊列并不是裝置等待隊列,隻是一個epoll内部定義的等待隊列)。這也能節省不少的開銷。
令人高興的是,2.6核心的epoll比其2.5開發版本的/dev/epoll簡潔了許多,是以,大部分情況下,強大的東西往往是簡單的。唯一有點麻煩 是epoll有2種工作方式:lt和et。
lt(level triggered)是預設的工作方式,并且同時支援block和no-block socket.在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行io操作。如果你不作任何操作,核心還是會繼續通知你 的,是以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表.
et (edge-triggered)是高速工作方式,隻支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,并且不會再為那個檔案描述 符發送更多的就緒通知,直到你做了某些操作導緻那個檔案描述符不再為就緒狀态了(比如,你在發送,接收或者接收請求,或者發送接收的資料少于一定量時導緻 了一個ewouldblock 錯誤)。但是請注意,如果一直不對這個fd作io操作(進而導緻它再次變成未就緒),核心不會發送更多的通知(only once),不過在tcp協定中,et模式的加速效用仍需要更多的benchmark确認。
leader/follower模式線程 pool實作,以及和epoll的配合。
epoll的高效和其資料結構的設計是密不可分的,這個下面就會提到。
前面提到epoll速度快和其資料結構密不可分,其關鍵資料結構就是:
9
10
11
12
13
14
15
16
17
18
19
可見epoll_data是一個union結構體,借助于它應用程式可以儲存很多類型的資訊:fd、指針等等。有了它,應用程式就可以直接定位目标了。
首先回憶一下select模型,當有i/o事件到來時,select通知應用程式有事件到了快去處理,而應用程式必須輪詢所有的fd集合,測試每個fd是否有事件發生,并處理事件;代碼像下面這樣:
首先回憶一下select模型,當有i/o事件到來時,select通知應用程式有事件到了快去處理,而應用程式必須輪詢所有的fd集合,測試每個fd是否有事件發生,并處理事件;
代碼像下面這樣:
epoll不僅會告訴應用程式有i/0事件到來,還會告訴應用程式相關的資訊,這些資訊是應用程式填充的,是以根據這些資訊應用程式就能直接定位到事件,而不必周遊整個fd集合。
首先通過<code>create_epoll(int maxfds)</code>來建立一個epoll的句柄,其中maxfds為你epoll所支援的最大句柄數。這個函數會傳回一個新的epoll句柄,之後的所有操作 将通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個建立出來的epoll句柄。之後在你的網絡主循環裡面,每一幀的調用<code>epoll_wait(int epfd, epoll_event events, int max events, int timeout)</code>來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的文法為:
其中kdpfd為用epoll_create建立之後的句柄,events是一個 epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裡面将儲存所有的讀寫事件。 max_events是目前需要監聽的所有socket句柄數。最後一個timeout是 epoll_wait的逾時,為0的時候表示馬上傳回,為-1的時候表示一直等下去,直到有事件範圍,為任意正整數的時候表示等這麼長的時間,如果一直沒 有事件,則範圍。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環 的效率。
既然epoll相比select這麼好,那麼用起來如何呢?會不會很繁瑣啊…先看看下面的三個函數吧,就知道epoll的易用了。
生成一個epoll專用的檔案描述符,其實是申請一個核心空間,用來存放你想關注的socket fd上是否發生以及發生了什麼事件。size就是你在這個epoll fd上能關注的最大socket fd數,大小自定,隻要記憶體足夠。
控制某個epoll檔案描述符上的事件:注冊、修改、删除。其中參數epfd是epoll_create()建立epoll專用的檔案描述符。相對于select模型中的fd_set和fd_clr宏。
等待i/o事件的發生,傳回發生事件數;
功能類似與select函數
參數說明:
參數
描述
epfd
由epoll_create() 生成的epoll專用的檔案描述符
epoll_event
用于回傳代處理事件的數組
maxevents
每次能處理的事件數
timeout
等待i/o事件發生的逾時值
轉載:http://blog.csdn.net/gatieme/article/details/50979090