Linux epoll模型詳解

一、epoll簡介

epoll是當前在Linux下開發大規模併發網絡程序的熱門選擇,epoll在Linux2.6內核中正式引入,和select相似,都是IO多路複用(IO multiplexing)技術。

按照man手冊的說法,epoll是為處理大批量句柄而做了改進的poll。

更多c/c++ Linux服務器高階知識請後臺私信【架構】獲取

知識點有C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等。

Linux epoll模型詳解

Linux下有以下幾個經典的服務器模型:

1、PPC模型和TPC模型

PPC(Process Per Connection)模型和TPC(Thread Per Connection)模型的設計思想類似,就是給每一個到來的連接都分配一個獨立的進程或者線程來服務。對於這兩種模型,其需要耗費較大的時間和空間資源。當管理連接數較多時,進程或線程的切換開銷較大。因此,這類模型能接受的最大連接數都不會高,一般都在幾百個左右。

2、select模型

對於select模型,其主要有以下幾個特點:

最大併發數限制:由於一個進程所打開的fd(文件描述符)是有限制的,由FD_SETSIZE設置,默認值是1024/2048,因此,select模型的最大併發數就被限制了。

效率問題:每次進行select調用都會線性掃描全部的fd集合。這樣,效率就會呈現線性下降。

內核/用戶空間內存拷貝問題:select在解決將fd消息傳遞給用戶空間時採用了內存拷貝的方式。這樣,其處理效率不高。

3、poll模型

對於poll模型,其雖然解決了select最大併發數的限制,但依然沒有解決掉select的效率問題和內存拷貝問題。

4、epoll模型

對比於其他模型,epoll做了如下改進:

支持一個進程打開較大數目的文件描述符(fd)

select模型對一個進程所打開的文件描述符是有一定限制的,其由FD_SETSIZE設置,默認為1024/2048。這對於那些需要支持上萬連接數目的高併發服務器來說顯然太少了,這個時候,可以選擇兩種方案:一是可以選擇修改FD_SETSIZE宏然後重新編譯內核,不過這樣做也會帶來網絡效率的下降;二是可以選擇多進程的解決方案(傳統的Apache方案),不過雖然Linux中創建線程的代價比較小,但仍然是不可忽視的,加上進程間數據同步遠不及線程間同步的高效,所以也不是一種完美的方案。 但是,epoll則沒有對描述符數目的限制,它所支持的文件描述符上限是整個系統最大可以打開的文件數目,例如,在1GB內存的機器上,這個限制大概為10萬左右。

IO效率不會隨文件描述符(fd)的增加而線性下降

傳統的select/poll的一個致命弱點就是當你擁有一個很大的socket集合時,不過任一時間只有部分socket是活躍的,select/poll每次調用都會線性掃描整個socket集合,這將導致IO處理效率呈現線性下降。

但是,epoll不存在這個問題,它只會對活躍的socket進行操作,這是因為在內核實現中,epoll是根據每個fd上面的callback函數實現的。因此,只有活躍的socket才會主動去調用callback函數,其他idle狀態socket則不會。在這一點上,epoll實現了一個偽AIO,其內部推動力在內核。

在一些benchmark中,如果所有的socket基本上都是活躍的,如高速LAN環境,epoll並不比select/poll效率高,相反,過多使用epoll_ctl,其效率反而還有稍微下降。但是,一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。

使用mmap加速內核與用戶空間的消息傳遞

無論是select,poll還是epoll,它們都需要內核把fd消息通知給用戶空間。因此,如何避免不必要的內存拷貝就很重要了。對於該問題,epoll通過內核與用戶空間mmap同一塊內存來實現。

內核微調

這一點其實不算epoll的優點了,而是整個Linux平臺的優點,Linux賦予開發者微調內核的能力。比如,內核TCP/IP協議棧使用內存池管理sk_buff結構,那麼,可以在運行期間動態調整這個內存池大小(skb_head_pool)來提高性能,該參數可以通過使用echo xxxx > /proc/sys/net/core/hot_list_length來完成。再如,可以嘗試使用最新的NAPI網卡驅動架構來處理數據包數量巨大但數據包本身很小的特殊場景。

二、epoll API

epoll只有epoll_create、epoll_ctl和epoll_wait這三個系統調用。其定義如下:

<code>#include <sys/epoll.h>
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);
/<code>

1、epoll_create

<code>#include <sys/epoll.h>
int epoll_create(int size);
/<code>

可以調用epoll_create方法創建一個epoll的句柄。

需要注意的是,當創建好epoll句柄後,它就會佔用一個fd值。在使用完epoll後,必須調用close函數進行關閉,否則可能導致fd被耗盡。

2、epoll_ctl

<code>#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/<code>

epoll的事件註冊函數,它不同於select是在監聽事件時告訴內核要監聽什麼類型的事件,而是通過epoll_ctl註冊要監聽的事件類型。

第一個參數epfd:epoll_create函數的返回值。

第二個參數events:表示動作類型。有三個宏來表示:加入 修改 刪除

  • EPOLL_CTL_ADD:註冊新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
  • EPOLL_CTL_DEL:從epfd中刪除一個fd。

第三個參數fd:需要監聽的fd。

第四個參數event:告訴內核需要監聽什麼事件。

struct epoll_event結構如下所示:

// 保存觸發事件的某個文件描述符相關的數據

<code>typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

// 感興趣的事件和被觸發的事件
struct epoll_event {

__uint32_t events; // Epoll events
epoll_data_t data; // User data variable
};
/<code>

如上所示,對於Epoll Events,其可以是以下幾個宏的集合:

EPOLLIN:表示對應的文件描述符可讀(包括對端Socket); EPOLLOUT:表示對應的文件描述符可寫; EPOLLPRI:表示對應的文件描述符有緊急數據可讀(帶外數據); EPOLLERR:表示對應的文件描述符發生錯誤; EPOLLHUP:表示對應的文件描述符被掛斷; EPOLLET:將EPOLL設為邊緣觸發(Edge Triggered),這是相對於水平觸發(Level Triggered)而言的。 EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket,需要再次

3、epoll_wait

<code>#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/<code>

收集在epoll監控的事件中已經發生的事件。參數events是分配好的epoll_event結構體數組,epoll將會把發生的事件賦值到events數組中(events不可以是空指針,內核只負責把數據賦值到這個event數組中,不會去幫助我們在用戶態分配內存)。maxevents告訴內核這個events數組有多大,這個maxevents的值不能大於創建epoll_create時的size。參數timeout是超時時間(毫秒)。如果函數調用成功,則返回對應IO上已準備好的文件描述符數目,如果返回0則表示已經超時。

三、epoll工作模式

  1. LT模式(Level Triggered,水平觸發)

該模式是epoll的缺省工作模式,其同時支持阻塞和非阻塞socket。內核會告訴開發者一個文件描述符是否就緒,如果開發者不採取任何操作,內核仍會一直通知。

  1. ET模式(Edge Triggered,邊緣觸發)

該模式是一種高速處理模式,當且僅當狀態發生變化時才會獲得通知。在該模式下,其假定開發者在接收到一次通知後,會完整地處理該事件,因此內核將不再通知這一事件。注意,緩衝區中還有未處理的數據不能說是狀態變化,因此,在ET模式下,開發者如果只讀取了一部分數據,其將再也得不到通知了。正確的做法是,開發者自己確認讀完了所有的字節(一直調用read/write直到出錯EAGAGIN為止)。

Nginx默認採用的就是ET(邊緣觸發)。

四、epoll高效性探討

epoll的高效性主要體現在以下三個方面:

(1)select/poll每次調用都要傳遞所要監控的所有fd給select/poll系統調用,這意味著每次調用select/poll時都要將fd列表從用戶空間拷貝到內核,當fd數目很多時,這會造成性能低效。對於epoll_wait,每次調用epoll_wait時,其不需要將fd列表傳遞給內核,epoll_ctl不需要每次都拷貝所有的fd列表,只需要進行增量式操作。因此,在調用epoll_create函數之後,內核已經在內核開始準備數據結構用於存放需要監控的fd了。其後,每次epoll_ctl只是對這個數據結構進行簡單的維護操作即可。

(2)內核使用slab機制,為epoll提供了快速的數據結構。在內核裡,一切都是文件。因此,epoll向內核註冊了一個文件系統,用於存儲所有被監控的fd。當調用epoll_create時,就會在這個虛擬的epoll文件系統中創建一個file節點。epoll在被內核初始化時,同時會分配出epoll自己的內核告訴cache區,用於存放每個我們希望監控的fd。這些fd會以紅黑樹的形式保存在內核cache裡,以支持快速查找、插入和刪除。這個內核高速cache,就是建立連續的物理內存頁,然後在之上建立slab層,簡單的說,就是物理上分配好想要的size的內存對象,每次使用時都使用空閒的已分配好的對象。

(3)當調用epoll_ctl往epfd註冊百萬個fd時,epoll_wait仍然能夠快速返回,並有效地將發生的事件fd返回給用戶。原因在於,當我們調用epoll_create時,內核除了幫我們在epoll文件系統新建file節點,同時在內核cache創建紅黑樹用於存儲以後由epoll_ctl傳入的fd外,還會再建立一個list鏈表,用於存儲準備就緒的事件。當調用epoll_wait時,僅僅觀察這個list鏈表中有無數據即可。如果list鏈表中有數據,則返回這個鏈表中的所有元素;如果list鏈表中沒有數據,則sleep然後等到timeout超時返回。所以,epoll_wait非常高效,而且,通常情況下,即使我們需要監控百萬計的fd,但大多數情況下,一次也只返回少量準備就緒的fd而已。因此,每次調用epoll_wait,其僅需要從內核態複製少量的fd到用戶空間而已。那麼,這個準備就緒的list鏈表是怎麼維護的呢?過程如下:當我們執行epoll_ctl時,除了把fd放入到epoll文件系統裡file對象對應的紅黑樹之外,還會給內核中斷處理程序註冊一個回調函數,其告訴內核,如果這個fd的中斷到了,就把它放到準備就緒的list鏈表中。

如此,一棵紅黑樹、一張準備就緒的fd鏈表以及少量的內核cache,就幫我們解決了高併發下fd的處理問題。

總結一下:

執行epoll_create時,創建了紅黑樹和就緒list鏈表;epoll虛擬文件 cache內存 執行epoll_ctl時,如果增加fd,則檢查在紅黑樹中是否存在,存在則立即返回,不存在則添加到紅黑樹中,然後向內核註冊回調函數,用於當中斷事件到來時向準備就緒的list鏈表中插入數據。 執行epoll_wait時立即返回準備就緒鏈表裡的數據即可。



分享到:


相關文章: