天天看點

【轉】 Linux下epoll是如何實作高效處理百萬句柄的

作者:陶輝

原文位址:http://blog.csdn.net/russell_tao/article/details/7160071

開發高性能網絡程式時,windows開發者們言必稱iocp,linux開發者們則言必稱epoll。大家都明白epoll是一種IO多路複用技術,可以非常高效的處理數以百萬計的socket句柄,比起以前的select和poll效率高大發了。我們用起epoll來都感覺挺爽,确實快,那麼,它到底為什麼可以高速處理這麼多并發連接配接呢?

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

[cpp] view plain copy

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);  

使用起來很清晰,首先要調用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的記憶體對象,每次使用時都是使用空閑的已配置設定好的對象。

static int __init eventpoll_init(void)  

{  

    ... ...  

    /* Allocates slab cache used to allocate "struct epitem" items */  

    epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),  

            0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,  

            NULL, NULL);  

    /* Allocates slab cache used to allocate "struct eppoll_entry" */  

    pwq_cache = kmem_cache_create("eventpoll_pwq",  

            sizeof(struct eppoll_entry), 0,  

            EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);  

 ... ...  

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

                            作者:陶輝

                            原文位址:http://blog.csdn.net/russell_tao/article/details/7160071

繼續閱讀