nginx學習之epoll
首先說一下傳統的I/O多路複用select和poll,對比一下和epoll之間的區別:
舉個例子:假如有100萬用戶同時與一個進程保持TCP連接,而每一時刻只有幾十或者幾百個tcp連接是活躍的(即能接收到TCP包),那麼在每一時刻進程只需要處理這100萬連接中的有一小部分。
select和poll這樣處理的:在某一時刻,進程收集所有的連接,其實這100萬連接中大部分是沒有事件發生的。因此,如果每次收集事件時,都把這100萬連接的套接字傳給操作系統(這首先就是用戶態內存到內核內存的大量複製),而由操作系統內核尋找這些鏈接上沒有處理的事件,將會是巨大的浪費。
更多c/c++ Linux服務器高階知識、電子書籍、視頻等等請後臺私信【架構】獲取
知識點有C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等。
而epoll是這樣做的:epoll把select和poll分為了兩個部分,
1、調用epoll_creat建立一個epoll對象。
2、調用epoll_ctl向epoll對象中添加這100萬個連接的套接字。
3、調用epoll_wait收集發生事件的連接。=》重點是在這裡,調用epoll_wait收集所有發生的事件的連接,並將事件放在一個鏈表中,這樣只需到該鏈表中尋找發生連接的事件,而不用遍歷100萬連接!這樣在實際收集事件時,epoll_wait效率會很高。
三個系統調用函數都是用C進行封裝,在《深入理解Nginx》P310中由函數詳細說明,下面簡單介紹一下。
<code>**int
**epoll_create
(**
int
** size); **int
**epoll_ctl
(**
int
** epfd, **int
** op, **int
** fd, **struct** epoll_event *event); **int
**epoll_wait
(**
int
** epfd, **struct** epoll_event *events,**int
** maxevents, **int
** timeout); /<code>
首先要調用epoll_create建立一個epoll對象。參數size是內核保證能夠正確處理的最大句柄數,多於這個最大數時內核可不保證效果。
epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。
epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。
那麼epoll是如何實現以上想法的呢?
當某一個進程調用epoll_creat方法時,linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員的使用與epoll的使用方式密切相關。
<code>**struct
**eventpoll
{ **struct
**rb_root
rbr; **struct
**list_head
rdllist; } /<code>
epoll為何如此高效:
當我們調用epoll_ctl往裡塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,並有效的將發生事件的句柄給我們用戶。這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統裡建了個file結點,在內核cache裡建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裡有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。
而且,通常情況下即使我們要監控百萬計的句柄,大多一次也只返回很少量的準備就緒句柄而已,所以,epoll_wait僅需要從內核態copy少量的句柄到用戶態而已,如何能不高效?!
那麼,這個準備就緒list鏈表是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統裡file對象對應的紅黑樹上之外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裡。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裡了。
如此,一顆紅黑樹,一張準備就緒句柄鏈表,少量的內核cache,就幫我們解決了大併發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹幹上,然後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時立刻返回準備就緒鏈表裡的數據即可。
最後看看epoll獨有的兩種模式LT和ET。無論是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以後調用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。
這件事怎麼做到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時我們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,然後清空準備就緒list鏈表,最後,epoll_wait幹了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。
epoll的優點:
1.支持一個進程打開大數目的socket描述符(FD)
<code>select
最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置,默認值是2048
。對於那些需要支持的上萬連接數目的IM服務器來說顯然太少了。這時候你一是可以選擇修改這個宏然後重新編譯內核,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多進程的解決方案(傳統的 Apache方案),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。不過epoll
則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048
,舉個例子,在1GB內存的機器上大約是10
萬左右,具體數目可以cat /<code>
/proc/sys/fs/file-max察看,一般來說這個數目和系統內存關係很大。
2.IO效率不隨FD數目增加而線性下降
<code>傳統的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 /<code>
connections模擬WAN環境,epoll的效率就遠在select/poll之上了。
3.使用mmap加速內核與用戶空間的消息傳遞
<code>這點實際上涉及到epoll的具體實現了。無論是select
,poll還是epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的內存拷貝就很重要,在這點上,epoll是通過內核於用戶空間mmap同一塊內存實現的。而如果你想我一樣從2.5
內核就關注epoll的話,一定不會忘記手工 mmap這一步的。 /<code>
4.內核微調
這一點其實不算epoll的優點了,而是整個linux平臺的優點。也許你可以懷疑linux平臺,但是你無法迴避linux平臺賦予你微調內核的能力。比如,內核TCP/IP協議棧使用內存池管理sk_buff結構,那麼可以在運行時期動態調整這個內存pool(skb_head_pool)的大小--- 通過echo XXXX>
/proc/sys/net/core/hot_list_length完成。再比如listen函數的第2個參數(TCP完成3次握手的數據包隊列長度),也可以根據你平臺內存大小動態調整。更甚至在一個數據包面數目巨大但同時每個數據包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。