linux下epoll實現機制
原作者:陶輝
鏈接:http://blog.csdn.net/russell_tao/article/details/7160071
先簡單回顧下如何使用C庫封裝的select系統調用吧
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select的使用方法是這樣的:
返回的活躍連接 ==select(全部待監控的連接)
對高並發而言,全部待監控連接是數以十萬計的,返回的只是數百個活躍連接,而select的每次調用都需要傳入全部待監控連接給內核,這是非常低效的。
再來看epoll模型的API:
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的使用方法是這樣的:
新建的epoll描述符==epoll_create()
epoll_ctrl(epoll描述符,添加或者刪除所有待監控的連接)
返回的活躍連接 ==epoll_wait( epoll描述符 )
這么做的好處主要是:分清了頻繁調用和不頻繁調用的操作。例如,epoll_ctrl是不太頻繁調用的,而epoll_wait是非常頻繁調用的。這時,epoll_wait卻幾乎沒有入參,這比select的效率高出一大截,而且,它也不會隨着並發連接的增加使得入參越發多起來,導致內核執行效率下降。
epoll的工作方式:首先要調用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少量的句柄到用戶態而已)。這樣,epoll_wait的執行效率能不高嗎?
那么,這個准備就緒list鏈表是怎么維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統里file對象對應的紅黑樹上之外,還會給內核中斷處理程序注冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到准備就緒list鏈表里。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中后就來把socket插入到准備就緒鏈表里了。
如此,一顆紅黑樹,一張准備就緒句柄鏈表,少量的內核cache,就幫我們解決了大並發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內核注冊回調函數,用於當中斷事件來臨時向准備就緒鏈表中插入數據。執行epoll_wait時立刻返回准備就緒鏈表里的數據即可。
最后看看epoll獨有的兩種模式LT(水平觸發)和ET(邊緣觸發)。無論是LT和ET模式,都適用於以上所說的流程。
這件事怎么做到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的准備就緒list鏈表,這時我們調用epoll_wait,會把准備就緒的socket拷貝到用戶態內存,然后清空准備就緒list鏈表,最后,epoll_wait干了件事,就是檢查這些socket,如果是LT模式,並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的准備就緒鏈表了。所以,LT模式的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。
例如,我們需要監控一個連接的寫緩沖區是否空閑,滿足“可寫”時我們就可以從用戶態將響應調用write發送給客戶端 。但是,或者連接可寫時,我們的“響應”內容還在磁盤上呢,此時若是磁盤讀取還未完成呢?肯定不能使線程阻塞的,那么就不發送響應了。但是,下一次epoll_wait時可能又把這個連接返回給你了,你還得檢查下是否要處理。可能,我們的程序有另一個模塊專門處理磁盤IO,它會在磁盤IO完成時再發送響應。那么,每次epoll_wait都返回這個“可寫”的、卻無法立刻處理的連接,是否符合用戶預期呢?