epoll是Linux高效網絡的基礎,比如event poll(例如nodejs),是使用libev,而libev的底層就是epoll(只不過不同的平台可能用epoll,可能用kqueue)。
epoll能夠高效支持百萬級別的句柄監聽。
epoll高效,是因為內部用了一個紅黑樹記錄添加的socket,用了一個雙向鏈表接收內核觸發的事件。是系統級別的支持的:
當某一進程調用epoll_create方法時,Linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式密切相關。
eventpoll結構體如下所示:
struct eventpoll{ .... /*紅黑樹的根節點,這顆樹中存儲着所有添加到epoll中的需要監控的事件*/ struct rb_root rbr; /*雙鏈表中則存放着將要通過epoll_wait返回給用戶的滿足條件的事件*/ struct list_head rdlist; .... };
參考這篇文章:http://blog.csdn.net/u010657219/article/details/44061629
每一個epoll對象都有一個獨立的eventpoll結構體,用於存放通過epoll_ctl方法向epoll對象中添加進來的事件。
這些事件都會掛載在紅黑樹中,如此,重復添加的事件就可以通過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n為樹的高度)。
而所有添加到epoll中的事件都會與設備(網卡)驅動程序建立回調關系,也就是說,當相應的事件發生時會調用這個回調方法。這個回調方法在內核中叫ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中。
在epoll中,對於每一個事件,都會建立一個epitem結構體,如下所示:
struct epitem{ struct rb_node rbn;//紅黑樹節點 struct list_head rdllink;//雙向鏈表節點 struct epoll_filefd ffd; //事件句柄信息 struct eventpoll *ep; //指向其所屬的eventpoll對象 struct epoll_event event; //期待發生的事件類型 }
下面圖的左上角文字寫錯了,應該是雙向鏈表的每個節點都是基於epitem結構中的rdllink成員。
上面這一句更具體的解釋是(為什么能支持百萬句柄):
1. 不用重復傳遞。我們調用epoll_wait時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因為內核已經在epoll_ctl中拿到了要監控的句柄列表。
2. 在內核里,一切皆文件。所以,epoll向內核注冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統里創建一個file結點。當然這個file不是普通文件,它只服務於epoll。
epoll在被內核初始化時(操作系統啟動),同時會開辟出epoll自己的內核高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache里,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然后在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閑的已分配好的對象。
3. 極其高效的原因:
這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統里建了個file結點,在內核cache里建了個紅黑樹用於存儲以后epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲准備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表里有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到后即使鏈表沒數據也返回。所以,epoll_wait非常高效。
這個准備就緒list鏈表是怎么維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統里file對象對應的紅黑樹上之外,還會給內核中斷處理程序注冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到准備就緒list鏈表里。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中后就來把socket插入到准備就緒鏈表里了。(注:好好理解這句話!)
從上面這句可以看出,epoll的基礎就是回調呀!
如此,一顆紅黑樹,一張准備就緒句柄鏈表,少量的內核cache,就幫我們解決了大並發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內核注冊回調函數,用於當中斷事件來臨時向准備就緒鏈表中插入數據。執行epoll_wait時立刻返回准備就緒鏈表里的數據即可。
最后看看epoll獨有的兩種模式LT和ET。無論是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以后調用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。
關於LT,ET,有一端描述,LT和ET都是電子里面的術語,ET是邊緣觸發,LT是水平觸發,一個表示只有在變化的邊際觸發,一個表示在某個階段都會觸發。
參考了這篇文章:https://zhuanlan.zhihu.com/p/20315482
關於LT和ET的實際代碼實驗,可以看這里:http://www.cnblogs.com/charlesblc/p/5521086.html
關於ET和EPOLL_ONESHOT,可以看這里:http://www.cnblogs.com/charlesblc/p/5538363.html
關於epoll統一事件源代碼,可以看這里:http://www.cnblogs.com/charlesblc/p/5554785.html
LT, ET這件事怎么做到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的准備就緒list鏈表,這時我們調用epoll_wait,會把准備就緒的socket拷貝到用戶態內存,然后清空准備就緒list鏈表,最后,epoll_wait干了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的准備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回這個句柄。(從上面這段,可以看出,LT還有個回放的過程,低效了)
更詳細的內容,可以看下面兩篇文章:
http://blog.csdn.net/russell_tao/article/details/7160071
http://www.open-open.com/lib/view/open1410403215664.html