Linux epoll 筆記(高併發事件處理機制)

wikihtml

Epoll優勢;node

Epoll工做流程;linux

Epoll實現機制:編程

  epollevent;緩存

Epoll源碼分析;服務器

Epoll接口:網絡

  epoll_create;多線程

  epoll_ctl;架構

  epoll_close;併發

Epoll工做方式:

  LT(level-triggered);

  ET(edge-triggered);

Epoll應用模式;

 

Epoll優勢:

<1>支持一個進程打開大數目的socket描述符(FD)

 select一個進程所打開的FD是有必定限制的,由FD_SETSIZE設置,默認值是2048。能夠選擇修改這個宏而後從新編譯內核,不過資料也同時指出這樣會帶來網絡效率的降低,二是能夠選擇多進程的解決方案(傳統的 Apache方案),不過雖然linux上面建立進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,因此也不是一種完美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大能夠打開文件的數目,這個數字通常遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左 右,具體數目能夠cat /proc/sys/fs/file-max察看,通常來講這個數目和系統內存關係很大。

 <2>IO效率不隨FD數目增長而線性降低

epoll只會對"活躍"的socket進行操 做---這是由於在內核實現中epoll是根據每一個fd上面的callback函數實現的。那麼,只有"活躍"的socket纔會主動的去調用 callback函數,其餘idle狀態socket則不會,在這點上,epoll實現了一個"僞"AIO,由於這時候推進力在os內核。在一些 benchmark中,若是全部的socket基本上都是活躍的---好比一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,若是過多使用epoll_ctl,效率相比還有稍微的降低。可是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。同時對於監聽的fd不少,可是活躍的fd不多的狀況下epoll相比select也有很高的效率。

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

不管是select,poll仍是epoll都須要內核把FD消息通知給用戶空間,如何避免沒必要要的內存拷貝就很重要,在這點上,epoll是經過內核於用戶空間mmap同一塊內存實現的。

 <4>內核微調

這一點其實不算 epoll 的優勢了,而是整個linux平臺的優勢。也許你能夠懷疑linux平臺,可是你沒法迴避linux平臺賦予你微調內核的能力。好比,內核TCP/IP協 議棧使用內存池管理sk_buff結構,那麼能夠在運行期動態調整這個內存pool(skb_head_pool)的大小--- 經過echo XXXX>/proc/sys/net/core/hot_list_length完成。再好比listen函數的第2個參數(TCP完成3次握手 的數據包隊列長度),也能夠根據你平臺內存大小動態調整。更甚至在一個數據包面數目巨大但同時每一個數據包自己大小卻很小的特殊系統上嘗試最新的NAPI網 卡驅動架構。

 <5>與select相比,不復用監聽的文件描述集合來傳遞結果

這樣不須要每次等待前對文件描述符集合從新賦值。

 

Epoll工做流程:

Epoll實現機制:

epoll fd有一個私有的struct eventpoll,它記錄哪個fd註冊到了epfd上。eventpoll 一樣有一個等待隊列,記錄全部等待的線程。還有一個預備好的fd列表,這些fd能夠進行讀或寫。相關內核實現代碼fs/eventpoll.c,判斷是否tcp有激活事件嗎:net/ipv4/tcp.c:tcp_poll函數;    

struct eventpoll {

    /* Protect the access to this structure */

    spinlock_t lock;

 

    /*

    * This mutex is used to ensure that files are not removed

    * while epoll is using them. This is held during the event

    * collection loop, the file cleanup path, the epoll file exit

    * code and the ctl operations.

    */

    struct mutex mtx;

 

    /* Wait queue used by sys_epoll_wait() */

    wait_queue_head_t wq;

 

    /* Wait queue used by file->poll() */

    wait_queue_head_t poll_wait;

 

    /* List of ready file descriptors */

    struct list_head rdllist;//調用epoll_wait的時候,readylist中的epitem出列,將觸發的事件拷貝到用戶空間.以後判斷epitem是否需要從新添加回readylist.

 

    /* RB tree root used to store monitored fd structs */

    struct rb_root rbr;//紅黑樹的根,一個fd被添加到epoll中以後(EPOLL_ADD),內核會爲它生成一個對應的epitem結構對象.epitem被添加到rbr中。該結構保存了epoll監視的文件描述符。

 

    /*

    * This is a single linked list that chains all the "struct epitem" that

    * happened while transferring ready events to userspace w/out

    * holding ->lock.

    */

    struct epitem *ovflist;

 

    /* The user that created the eventpoll descriptor */

    struct user_struct *user;

};

 

 

epitem從新添加到readylist必須知足下列條件:

1) epitem上有用戶關注的事件觸發.

2) epitem被設置爲水平觸發模式(若是一個epitem被設置爲邊界觸發則這個epitem不會被從新添加到readylist

 

注意,若是epitem被設置爲EPOLLONESHOT模式,則當這個epitem上的事件拷貝到用戶空間以後,會將

這個epitem上的關注事件清空(只是關注事件被清空,並無從epoll中刪除,要刪除必須對那個描述符調用

EPOLL_DEL),也就是說即便這個epitem上有觸發事件,可是由於沒有用戶關注的事件因此不會被從新添加到

readylist.

 

epitem被添加到readylist中的各類狀況(當一個epitem被添加到readylist若是有線程阻塞在epoll_wait,

個線程會被喚醒):

1)對一個fd調用EPOLL_ADD,若是這個fd上有用戶關注的激活事件,則這個fd會被添加到readylist.

 2)對一個fd調用EPOLL_MOD改變關注的事件,若是新增長了一個關注事件且對應的fd上有相應的事件激活,

則這個fd會被添加到readylist.

 3)當一個fd上有事件觸發時(例如一個socket上有外來的數據)會調用ep_poll_callback(eventpoll::ep_ptable_queue_proc),

若是觸發的事件是用戶關注的事件,則這個fd會被添加到readylist.

 

瞭解了epoll的執行過程以後,能夠回答一個在使用邊界觸發時常見的疑問.在一個fd被設置爲邊界觸發的狀況下,

調用read/write,如何正確的判斷那個fd已經沒有數據可讀/再也不可寫.epoll文檔中的建議是直到觸發EAGAIN

錯誤.而實際上只要你請求字節數小於read/write的返回值就能夠肯定那個fd上已經沒有數據可讀/再也不可寫.

最後用一個epollfd監聽另外一個epollfd也是合法的,epoll經過調用eventpoll::ep_eventpoll_poll來判斷一個

epollfd上是否有觸發的事件(只能是讀事件).

 

Epoll源碼分析:

涉及linux模塊的編寫;

<<Epoll源碼分析.doc>>

Epoll module:

static int __init eventpoll_init(void){

//模塊初始化函數

}

eventpoll_init函數源碼

static int __init eventpoll_init(void)

{

int error;

 

init_MUTEX(&epsem);

 

/* Initialize the structure used to perform safe poll wait head wake ups */

ep_poll_safewake_init(&psw);

 

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

 

/*

 * Register the virtual file system that will be the source of inodes

 

 * for the eventpoll files

 */

/*註冊了一個新的文件系統,叫"eventpollfs"(在eventpoll_fs_type結構裏),而後掛載此文件系統*/

error = register_filesystem(&eventpoll_fs_type);

if (error)

goto epanic;

 

/* Mount the above commented virtual file system */

eventpoll_mnt = kern_mount(&eventpoll_fs_type);

error = PTR_ERR(eventpoll_mnt);

if (IS_ERR(eventpoll_mnt))

goto epanic;

 

DNPRINTK(3, (KERN_INFO "[%p] eventpoll: successfully initialized.\n",

current));

return 0;

 

epanic:

panic("eventpoll_init() failed\n");

}

epoll是個module,因此先看看module的入口eventpoll_init。這個module在初始化時註冊了一個新的文件系統,叫"eventpollfs"(在eventpoll_fs_type結構裏),而後掛載此文件系統。另外建立兩個內核cache(在內核編程中,若是須要頻繁分配小塊內存,應該建立kmem_cahe來作「內存池」),分別用於存放struct epitemeppoll_entry

 

 

Epoll的接口:

epollLinux內核爲處理大批句柄而做改進的pollLinux下多路複用IO接口select/poll的加強版本,它能顯著的減小程序在大量併發鏈接中只有少許活躍的狀況下的系統CPU利用率。由於它會複用文件描述符集合來傳遞結果而不是迫使開發者每次等待事件以前都必須從新準備要被偵聽的文件描述符集合,另外一個緣由就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就好了。epoll除了提供select\poll那種IO事件的電平觸發(Level Triggered)外,還提供了邊沿觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減小epoll_wait/epoll_pwait的調用,提供應用程序的效率。

1.工做函數

1>.int epoll_create(int size);

建立一個epoll的句柄,size用來告訴內核這個監聽的數目fd+1,每一個epoll都會佔用一個fd值,能夠在/proc/進程id/fd/查看。記得close()

2>.int epoll_ctl(int epfd,int op,int fd ,struct epoll_event *event);

epoll的事件註冊函數,epoll的控制函數;

這裏先註冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動做,用三個宏來表示:

EPOLL_CTL_ADD:註冊新的fdepfd中;

EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;

EPOLL_CTL_DEL:從epfd中刪除一個fd

第三個參數是須要監聽的fd,第四個參數是告訴內核須要監聽什麼事,struct epoll_event結構以下:

 

typedef union epoll_data {

    void *ptr;//數據指針

    int fd;/*descriptor*/

    __uint32_t u32;

    __uint64_t u64;

} epoll_data_t;

 

struct epoll_event {

    __uint32_t events; /* Epoll events type */

    epoll_data_t data; /* User data variable */

};

 

epoll_event->data涵蓋了調用epoll_ctl增長或者修改某指定句柄時寫入的信息,epoll_event->event,則包含了返回事件的位域。

 

events能夠是如下幾個宏的集合:

EPOLLIN :表示對應的文件描述符能夠讀(包括對端SOCKET正常關閉);

EPOLLOUT:表示對應的文件描述符能夠寫;

EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);

EPOLLERR:表示對應的文件描述符發生錯誤;

EPOLLHUP:表示對應的文件描述符被掛斷;

EPOLLET EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來講的。

EPOLLONESHOT:只監聽一次事件,當監聽完此次事件以後,若是還須要繼續監聽這個socket的話,須要再次把這個socket加入到EPOLL隊列裏

 

enum EPOLL_EVENTS

  {

    EPOLLIN = 0x001,

#define EPOLLIN EPOLLIN

    EPOLLPRI = 0x002,

#define EPOLLPRI EPOLLPRI

    EPOLLOUT = 0x004,

#define EPOLLOUT EPOLLOUT

    EPOLLRDNORM = 0x040,

#define EPOLLRDNORM EPOLLRDNORM

    EPOLLRDBAND = 0x080,

#define EPOLLRDBAND EPOLLRDBAND

    EPOLLWRNORM = 0x100,

#define EPOLLWRNORM EPOLLWRNORM

    EPOLLWRBAND = 0x200,

#define EPOLLWRBAND EPOLLWRBAND

    EPOLLMSG = 0x400,

#define EPOLLMSG EPOLLMSG

    EPOLLERR = 0x008,

#define EPOLLERR EPOLLERR

    EPOLLHUP = 0x010,

#define EPOLLHUP EPOLLHUP

    EPOLLRDHUP = 0x2000,

#define EPOLLRDHUP EPOLLRDHUP

    EPOLLWAKEUP = 1u << 29,

#define EPOLLWAKEUP EPOLLWAKEUP

    EPOLLONESHOT = 1u << 30,

#define EPOLLONESHOT EPOLLONESHOT

    EPOLLET = 1u << 31

#define EPOLLET EPOLLET

  };

 

3>. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待事件的產生,相似於select()調用。參數events用來從內核獲得事件的集合maxevents告以內核這個events有多大,這個 maxevents的值不能大於建立epoll_create()時的size,參數timeout是超時時間(毫秒,0會當即返回,-1將不肯定,也有說法說是永久阻塞)。該函數返回須要處理的事件數目,如返回0表示已超時。

 

工做方式:

LT/ET:

LT(level triggered):水平觸發,缺省方式,同時支持blockno-block socket,在這種作法中,內核告訴咱們一個文件描述符是否被就緒了,若是就緒了,你就能夠對這個就緒的fd進行IO操做。若是你不做任何操做,內核仍是會繼續通知你的,因此,這種模式編程出錯的可能性較小。傳統的select\poll都是這種模型的表明。

 

ET(edge-triggered):邊沿觸發,高速工做方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒狀態時,內核經過epoll告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個描述符發送更多的就緒通知,直到你作了某些操做致使那個文件描述符再也不爲就緒狀態了(好比:你在發送、接受或者接受請求,或者發送接受的數據少於必定量時致使了一個EWOULDBLOCK錯誤)。可是請注意,若是一直不對這個fs作IO操做(從而致使它再次變成未就緒狀態),內核不會發送更多的通知。

 

應用模式:

那麼究竟如何來使用epoll呢?其實很是簡單。

經過在包含一個頭文件#include <sys/epoll.h> 以及幾個簡單的API將能夠大大的提升你的網絡服務器的支持人數。

 

首先經過create_epoll(int maxfds)來建立一個epoll的句柄,其中maxfds爲你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,以後的全部操做將經過這個句柄來進行操做。在用完以後,記得用close()來關閉這個建立出來的epoll句柄。

 

以後在你的網絡主循環裏面,每一幀的調用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢全部的網絡接口,看哪個能夠讀,哪個能夠寫了。基本的語法爲:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd爲用epoll_create建立以後的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操做成功以後,epoll_events裏面將儲存全部的讀寫事件。max_events是當前須要監聽的全部socket句柄數。最後一個timeout epoll_wait的超時,爲0的時候表示立刻返回,爲-1的時候表示一直等下去,直到有事件範圍,爲任意正整數的時候表示等這麼長的時間,若是一直沒有事件,則範圍。通常若是網絡主循環是單獨的線程的話,能夠用-1來等,這樣能夠保證一些效率,若是是和主邏輯在同一個線程的話,則能夠用0來保證主循環的效率。

 

epoll_wait範圍以後應該是一個循環,遍利全部的事件。

 

幾乎全部的epoll程序都使用下面的框架(尤爲是socket)

 

    for( ; ; )

    {

        nfds = epoll_wait(epfd,events,20,500);

        for(i=0;i<nfds;++i)

        {

            if(events[i].data.fd==listenfd) //有新的鏈接;咱們能夠註冊多個FD,若是內核發現事件,就會載入events,若是有咱們要的描述符也就是listenfd,說明某某套接字監聽描述符所對應的事件發生了變化。每次最多監測20fd數。

            {

                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個鏈接

                ev.data.fd=connfd;

                ev.events=EPOLLIN|EPOLLET;//LT

                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監聽隊列中

            }

            else if( events[i].events&EPOLLIN ) //接收到數據,讀socket,數據可讀標誌EPOLLIN

            {

                n = read(sockfd, line, MAXLINE)) < 0    //讀

                ev.data.ptr = md;     //md爲自定義類型,添加數據

                ev.events=EPOLLOUT|EPOLLET;

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環時發送數據,異步處理的精髓

            }

            else if(events[i].events&EPOLLOUT) //有數據待發送,寫socket

            {

                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取數據

                sockfd = md->fd;

                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發送數據

                ev.data.fd=sockfd;

                ev.events=EPOLLIN|EPOLLET;

                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時接收數據

            }

            else

            {

                //其餘的處理

            }

        }

    }

1.Linux下多線程epoll編程

來自 <http://blog.csdn.net/susubuhui/article/details/37906287>

2.epoll + 多線程實現併發網絡鏈接處理

來自 <http://www.cnblogs.com/iTsihang/archive/2013/05/23/3095775.html>

3.高併發的epoll+線程池,業務在線程池內

來自 <http://blog.chinaunix.net/uid-311680-id-2439722.html

相關文章
相關標籤/搜索