1. 程式人生 > >高並發網絡編程之epoll詳解

高並發網絡編程之epoll詳解

分享圖片 file 解決 雙鏈表 開始 數據結構實現 list 函數 實現機制

select、poll和epoll的區別

在linux沒有實現epoll事件驅動機制之前,我們一般選擇用select或者poll等IO多路復用的方法來實現並發服務程序。在大數據、高並發、集群等一些名詞唱的火熱之年代,select和poll的用武之地越來越有限了,風頭已經被epoll占盡。

select()和poll() IO多路復用模型

select的缺點:

  • 單個進程能夠監視的文件描述符的數量存在最大限制,通常是1024,當然可以更改數量,但由於select采用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;
  • 內核/用戶空間內存拷貝問題,select需要復制大量的句柄數據結構,產生巨大的開銷
  • select返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件;
  • select的觸發方式是水平觸發,應用程序如果沒有完成對一個已經就緒的文件描述符進行IO,那麽之後再次select調用還是會將這些文件描述符通知進程。

相比於select模型,poll使用鏈表保存文件描述符,因此沒有了監視文件數量的限制,但其他三個缺點依然存在。

拿select模型為例,假設我們的服務器需要支持100萬的並發連接,則在_FD_SETSIZE為1024的情況下,則我們至少需要開辟1k個進程才能實現100萬的並發連接。除了進程間上下文切換的時間消耗外,從內核/用戶空間大量的無腦內存拷貝、數組輪詢等,是系統難以承受的。因此,基於select模型的服務器程序,要達到10萬級別的並發訪問,是一個很難完成的任務。

epoll IO多路復用模型實現機制

由於epoll的實現機制與select/poll機制完全不同,上面所說的select的缺點在epoll上不復存在。

設想一下如下場景:有100萬個客戶端同時與一個服務器進程保持著TCP連接。而每一時刻,通常只有幾百上千個TCP連接是活躍的。如何實現這樣的高並發?

在select/poll時代,服務器進程每次都把這100萬個連接告訴操作系統(從用戶態復制句柄數據結構到內核態),讓操作系統內核去查詢這些套接字上是否有事件發生,輪詢完後,再將句柄數據復制到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的並發連接。

epoll的設計和實現select完全不同。epoll通過在linux內核中申請一個簡易的文件系統(文件系統一般用什麽數據結構實現?B+樹)。把原先的select/poll調用分成了3個部分:

1)調用epoll_create()建立一個epoll對象(在epoll文件系統中為這個句柄對象分配資源)

2)調用epoll_ctl向epoll對象中添加這100萬個連接的套接字

3)調用epoll_wait收集發生的事件的連接

如此一來,要實現上面說的場景,只需要在進程啟動時建立一個epoll對象,然後在需要的時候向這個epoll對象中添加或者刪除連接。同時,epoll_wait的效率也非常高,因為調用epoll_wait時,並沒有一股腦的向操作系統復制這100萬個連接的句柄數據,內核也不需要去遍歷全部的連接。

上面的3個部分非常清晰,首先要調用epoll_create創建一個epoll對象。然後使用epoll_ctl可以操作上面建立的epoll對象,例如,將剛建立的socket加入到epoll中讓其監控,或者把epoll正在監控的某個socket句柄移出epoll,不再監控它等等。

epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。

從上面的調用方式就可以看到epoll比select/poll的優越之處:因為後者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味著需要將用戶態的socket列表copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因為內核已經在epoll_ctl中拿到了要監控的句柄列表

所以,實際上在你調用epoll_create後,內核就已經在內核態開始準備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構裏塞入新的socket句柄。

在內核裏,一切皆文件。所以,epoll向內核註冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統裏創建一個file結點。當然這個file不是普通文件,它只服務於epoll。

epoll在被內核初始化時(操作系統啟動),同時會開辟出epoll自己的內核高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache裏,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然後在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閑的已分配好的對象。

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用kmem_cache_create(slab分配器)分配內存用來存放struct epitem和struct eppoll_entry。

當向系統中添加一個fd時,就創建一個epitem結構體,這是內核管理epoll的基本數據結構:

struct epitem {

struct rb_node rbn; //用於主結構管理的紅黑樹

struct list_head rdllink; //事件就緒隊列

struct epitem *next; //用於主結構體中的鏈表

struct epoll_filefd ffd; //這個結構體對應的被監聽的文件描述符信息

int nwait; //poll操作中事件的個數

struct list_head pwqlist; //雙向鏈表,保存著被監視文件的等待隊列,功能類似於select/poll中的poll_table

struct eventpoll *ep; //該項屬於哪個主結構體(多個epitm從屬於一個eventpoll)

struct list_head fllink; //雙向鏈表,用來鏈接被監視的文件描述符對應的struct file。因為file裏有f_ep_link,用來保存所有監視這個文件的epoll節點

struct epoll_event event; //註冊的感興趣的事件,也就是用戶空間的epoll_event

}

而每個epoll fd(epfd)對應的主要數據結構為:

struct eventpoll {

spin_lock_t lock; //對本數據結構的訪問

struct mutex mtx; //防止使用時被刪除

wait_queue_head_t wq; //sys_epoll_wait() 使用的等待隊列

wait_queue_head_t poll_wait; //file->poll()使用的等待隊列

struct list_head rdllist; //事件滿足條件的鏈表 /*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/

struct rb_root rbr; //用於管理所有fd的紅黑樹(樹根) /*紅黑樹的根節點,這顆樹中存儲著所有添加到epoll中的需要監控的事件*/

struct epitem *ovflist; //將事件到達的fd進行鏈接起來發送至用戶空間

}

struct eventpoll在epoll_create時創建

這樣說來,內核中維護了一棵紅黑樹,大致的結構如下:

技術分享圖片

當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶

技術分享圖片

epoll數據結構示意圖

參考:http://www.cricode.com/3499.html

高並發網絡編程之epoll詳解