論epoll的實現

論epoll的實現

  1. 上一篇博客 論select的實現 裏面已經說了爲何 select 比較慢。poll 的實現和 select 相似,只是少了最大 fd 限制,若是有興趣能夠本身去看代碼。我這裏來簡單來過一下 epoll 的實現。

1) 一次添加

select / poll 爲了實現簡單,不對已有的 fd 進行管理。每次須要傳入最大的輪詢 fd, 而後每一個監聽 fd 掛到設備一次致使性能不佳。epoll 對於監聽的 fd,經過 epoll_ctl 來把對應的 fd 添加到紅黑樹,實現快速的查詢和添加,刪除。node

1.1) epoll 實例建立

使用 epoll 以前會使用 epoll_create 建立一個 epoll 實例,它其實是一個文件, 只是存在於內存中的文件。下面實現來自 linux-2.6.24 的 fs/eventpoll.c:linux

asmlinkage long sys_epoll_create(int size) {...
    struct eventpoll *ep;...// 建立 eventpoll 實例if (size <= 0 || (error = ep_alloc(&ep)) != 0)
        goto error_return;...// 爲 epoll 文件添加文件操做函數
    error = anon_inode_getfd(&fd, &inode, &file, "[eventpoll]",&eventpoll_fops, ep);}

epoll_create 的參數 size 是老版本的實現,使用的是 hash 表, size 應該是用來算 bucket 數目,後面由於使用紅黑樹,這個參數再也不使用, 能夠忽略。windows

1.2) 添加 fd

asmlinkage long sys_epoll_ctl(int epfd, int op, int fd,
                  struct epoll_event __user *event) {...// 先查找 fd 是否已經存在
    epi = ep_find(ep, tfile, fd);

    error = -EINVAL;
    switch (op) {// 若是是添加,就插入到 eventpoll 實例的紅黑樹
    case EPOLL_CTL_ADD:if (!epi) {
            epds.events |= POLLERR | POLLHUP;// 添加監聽的 fd 到epoll
            error = ep_insert(ep, &epds, tfile, fd);} else
            error = -EEXIST;break;...}}

接着調用 ep_insert 是添加 fd 到紅黑樹以及把進程的回調函數添加文件句柄的監聽隊列,當有事件到來時,會喚醒進程。網絡

static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
             struct file *tfile, int fd){...// 建立 epitem 並設置回調函數 ep_ptable_queue_procinit_poll_funcptr(&epq.pt, ep_ptable_queue_proc);...// 這裏會回調 ep_ptable_queue_proc, 並查詢 fd 可讀寫狀態
    revents = tfile->f_op->poll(tfile, &epq.pt);...// epitem 添加到 eventpoll 的紅黑樹ep_rbtree_insert(ep, epi);...}

ep_ptable_queue_proc 這個回調函數, 除了把進程添加文件句柄的監聽列表,並註冊回調函數爲 ep_poll_callback。 這個函數會查詢 fd 的讀寫狀態, 若是當前文件句柄能夠讀寫,就把當前的 fd 添加到就緒隊列。後續查詢是否有 fd 能夠讀寫,只要只要拷貝這個就緒列表,不用查詢。咱們下面會來看看 epoll_wait 的實現。數據結構

2) 快速查詢

epoll 之因此快,除了沒有屢次重複掛載事件以外,在有讀寫事件到來的實現,也是很高效。沒有像 select/poll 那樣, 須要輪詢全部的fd, 才能知道哪些 fd 有事件須要處理。併發

epoll 有一個專門的鏈表用來存放哪些 fd 有事件到來,用戶空間須要查詢哪些 fd 有讀寫等待處理,只須要拷貝這個鏈表便可。socket

咱們使用系統調用 epoll_wait, 會到內核調用 sys_epoll_wait, 這個函數的主要實現就是調用了 ep_poll:函數

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout){...
    res = 0;if (list_empty(&ep->rdllist)) {// 若是沒有事件, 不斷等待讀寫事件到來// 直到超時,或者有讀寫事件for (;;) {/* * We don't want to sleep if the ep_poll_callback() sends us * a wakeup in between. That's why we set the task state * to TASK_INTERRUPTIBLE before doing the checks. */// 當前進程設置爲可中斷set_current_state(TASK_INTERRUPTIBLE);// 有鏈接就緒if (!list_empty(&ep->rdllist) || !jtimeout)break;if (signal_pending(current)) {
                res = -EINTR;break;}}}//若是 rdllist 不爲空, 說明有事件到來。
    eavail = !list_empty(&ep->rdllist);spin_unlock_irqrestore(&ep->lock, flags);// 拷貝到用戶空間if (!res && eavail &&!(res = ep_send_events(ep, events, maxevents)) && jtimeout)
        goto retry;return res;}

咱們能夠看到,epoll 在實現 epoll_wait的時候,並不會去查詢 fd 的可讀寫狀態。 而是等待 fd 有讀寫到來時, 經過回調函數把有事件到來的 fd 主動拷貝到 rdllist。post

另一個有一個細節點就是, 當用戶在拷貝事件到用戶空間時,恰好有事件到來,那麼這些讀寫事件會不會正好丟了。答案是固然不會,epoll 準備了另一個鏈表,叫 overflow list, 當檢查正在拷貝時,會把這些 fd 臨時放到這個鏈表,下次再拷貝到 rdllist.性能

3) 總結

select/poll 還有比較坑的是,每次查詢到 fd 讀寫事件結果以後,須要把全部 fd 對應的結果的 bitmap 拷貝到用戶空間。 好比監聽 100w 的 fd, 只有一個 fd 有讀寫事件, 卻要拷貝 100w fd的結果 bitmap。

對比來看,select/poll 實現極爲簡單,但並不適合用來維護大量的鏈接。





開發高性能網絡程序時,windows開發者們言必稱iocp,linux開發者們則言必稱epoll。你們都明白epoll是一種IO多路複用技 術,能夠很是高效的處理數以百萬計的socket句柄,比起之前的select和poll效率高大發了。咱們用起epoll來都感受挺爽,確實快,那麼, 它到底爲何能夠高速處理這麼多併發鏈接呢?


先簡單回顧下如何使用C庫封裝的3個epoll系統調用吧。

  1. int epoll_create(int size);  
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
  3. int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);  

使用起來很清晰,首先要調用epoll_create創建一個epoll對象。參數size是內核保證可以正確處理的最大句柄數,多於這個最大數時內核可不保證效果。

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的內存對象,每次使用時都是使用空閒的已分配好的對象。

  1. static int __init eventpoll_init(void)  
  2. {  
  3.     ... ...  
  4.   
  5.     /* Allocates slab cache used to allocate "struct epitem" items */  
  6.     epi_cache = kmem_cache_create("eventpoll_epi"sizeof(struct epitem),  
  7.             0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,  
  8.             NULL, NULL);  
  9.   
  10.     /* Allocates slab cache used to allocate "struct eppoll_entry" */  
  11.     pwq_cache = kmem_cache_create("eventpoll_pwq",  
  12.             sizeof(struct eppoll_entry), 0,  
  13.             EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);  
  14.   
  15.  ... ...  


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返回的。




相關文章
相關標籤/搜索