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


wiki

Epoll優點;

Epoll工作流程;

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM