高併發網絡編程之epoll詳解

select、poll和epoll的區別

在linux沒有實現epoll事件驅動機制以前,咱們通常選擇用select或者poll等IO多路複用的方法來實現併發服務程序。在大數據、高併發、集羣等一些名詞唱的火熱之年代,select和poll的用武之地愈來愈有限了,風頭已經被epoll佔盡。html

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

select的缺點:node

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

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

拿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文件系統中爲這個句柄對象分配資源)socket

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時建立

 

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

 clip_image002

當調用epoll_wait檢查是否有事件發生時,只須要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素便可。若是rdlist不爲空,則把發生的事件複製到用戶態,同時將事件數量返回給用戶

epoll數據結構示意圖

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

相關文章
相關標籤/搜索