Linux epoll 笔记(高并发事件处理机制)
wiki:
Epoll優(yōu)點;
Epoll工作流程;
Epoll實現(xiàn)機制:
epollevent;
Epoll源碼分析;
Epoll接口:
epoll_create;
epoll_ctl;
epoll_close;
Epoll工作方式:
LT(level-triggered);
ET(edge-triggered);
Epoll應(yīng)用模式;
?
Epoll優(yōu)點:
<1>支持一個進程打開大數(shù)目的socket描述符(FD)
?select一個進程所打開的FD是有一定限制的,由FD_SETSIZE設(shè)置,默認值是2048。可以選擇修改這個宏然后重新編譯內(nèi)核,不過資料也同時指出這樣會帶來網(wǎng)絡(luò)效率的下降,二是可以選擇多進程的解決方案(傳統(tǒng)的 Apache方案),不過雖然linux上面創(chuàng)建進程的代價比較小,但仍舊是不可忽視的,加上進程間數(shù)據(jù)同步遠比不上線程間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠大于2048,舉個例子,在1GB內(nèi)存的機器上大約是10萬左 右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
?<2>IO效率不隨FD數(shù)目增加而線性下降
epoll只會對"活躍"的socket進行操 作---這是因為在內(nèi)核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的。那么,只有"活躍"的socket才會主動的去調(diào)用 callback函數(shù),其他idle狀態(tài)socket則不會,在這點上,epoll實現(xiàn)了一個"偽"AIO,因為這時候推動力在os內(nèi)核。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環(huán)境,epoll并不比select/poll有什么效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環(huán)境,epoll的效率就遠在select/poll之上了。同時對于監(jiān)聽的fd很多,但是活躍的fd很少的情況下epoll相比select也有很高的效率。
?<3>使用mmap加速內(nèi)核與用戶空間的消息傳遞。
無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點上,epoll是通過內(nèi)核于用戶空間mmap同一塊內(nèi)存實現(xiàn)的。
?<4>內(nèi)核微調(diào)
這一點其實不算 epoll 的優(yōu)點了,而是整個linux平臺的優(yōu)點。也許你可以懷疑linux平臺,但是你無法回避linux平臺賦予你微調(diào)內(nèi)核的能力。比如,內(nèi)核TCP/IP協(xié) 議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu),那么可以在運行時期動態(tài)調(diào)整這個內(nèi)存pool(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個參數(shù)(TCP完成3次握手 的數(shù)據(jù)包隊列長度),也可以根據(jù)你平臺內(nèi)存大小動態(tài)調(diào)整。更甚至在一個數(shù)據(jù)包面數(shù)目巨大但同時每個數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng) 卡驅(qū)動架構(gòu)。
?<5>與select相比,不復(fù)用監(jiān)聽的文件描述集合來傳遞結(jié)果
這樣不需要每次等待前對文件描述符集合重新賦值。
?
Epoll工作流程:
Epoll實現(xiàn)機制:
epoll fd有一個私有的struct eventpoll,它記錄哪一個fd注冊到了epfd上。eventpoll 同樣有一個等待隊列,記錄所有等待的線程。還有一個預(yù)備好的fd列表,這些fd可以進行讀或?qū)憽?span lang="zh-cn">相關(guān)內(nèi)核實現(xiàn)代碼fs/eventpoll.c,判斷是否tcp有激活事件嗎:net/ipv4/tcp.c:tcp_poll函數(shù);????
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;//調(diào)用epoll_wait的時候,將readylist中的epitem出列,將觸發(fā)的事件拷貝到用戶空間.之后判斷epitem是否需要重新添加回readylist.
?
??? /* RB tree root used to store monitored fd structs */
??? struct rb_root rbr;//紅黑樹的根,一個fd被添加到epoll中之后(EPOLL_ADD),內(nèi)核會為它生成一個對應(yīng)的epitem結(jié)構(gòu)對象.epitem被添加到rbr中。該結(jié)構(gòu)保存了epoll監(jiān)視的文件描述符。
?
??? /*
??? * 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上有用戶關(guān)注的事件觸發(fā).
2) epitem被設(shè)置為水平觸發(fā)模式(如果一個epitem被設(shè)置為邊界觸發(fā)則這個epitem不會被重新添加到readylist
?
注意,如果epitem被設(shè)置為EPOLLONESHOT模式,則當這個epitem上的事件拷貝到用戶空間之后,會將
這個epitem上的關(guān)注事件清空(只是關(guān)注事件被清空,并沒有從epoll中刪除,要刪除必須對那個描述符調(diào)用
EPOLL_DEL),也就是說即使這個epitem上有觸發(fā)事件,但是因為沒有用戶關(guān)注的事件所以不會被重新添加到
readylist中.
?
epitem被添加到readylist中的各種情況(當一個epitem被添加到readylist如果有線程阻塞在epoll_wait中,那
個線程會被喚醒):
1)對一個fd調(diào)用EPOLL_ADD,如果這個fd上有用戶關(guān)注的激活事件,則這個fd會被添加到readylist.
?2)對一個fd調(diào)用EPOLL_MOD改變關(guān)注的事件,如果新增加了一個關(guān)注事件且對應(yīng)的fd上有相應(yīng)的事件激活,
則這個fd會被添加到readylist.
?3)當一個fd上有事件觸發(fā)時(例如一個socket上有外來的數(shù)據(jù))會調(diào)用ep_poll_callback(見eventpoll::ep_ptable_queue_proc),
如果觸發(fā)的事件是用戶關(guān)注的事件,則這個fd會被添加到readylist中.
?
了解了epoll的執(zhí)行過程之后,可以回答一個在使用邊界觸發(fā)時常見的疑問.在一個fd被設(shè)置為邊界觸發(fā)的情況下,
調(diào)用read/write,如何正確的判斷那個fd已經(jīng)沒有數(shù)據(jù)可讀/不再可寫.epoll文檔中的建議是直到觸發(fā)EAGAIN
錯誤.而實際上只要你請求字節(jié)數(shù)小于read/write的返回值就可以確定那個fd上已經(jīng)沒有數(shù)據(jù)可讀/不再可寫.
最后用一個epollfd監(jiān)聽另一個epollfd也是合法的,epoll通過調(diào)用eventpoll::ep_eventpoll_poll來判斷一個
epollfd上是否有觸發(fā)的事件(只能是讀事件).
?
Epoll源碼分析:
涉及linux模塊的編寫;
<<Epoll源碼分析.doc>>
Epoll module:
static int __init eventpoll_init(void){
//模塊初始化函數(shù)
}
eventpoll_init函數(shù)源碼
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
?*/
/*注冊了一個新的文件系統(tǒng),叫"eventpollfs"(在eventpoll_fs_type結(jié)構(gòu)里),然后掛載此文件系統(tǒng)*/
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在初始化時注冊了一個新的文件系統(tǒng),叫"eventpollfs"(在eventpoll_fs_type結(jié)構(gòu)里),然后掛載此文件系統(tǒng)。另外創(chuàng)建兩個內(nèi)核cache(在內(nèi)核編程中,如果需要頻繁分配小塊內(nèi)存,應(yīng)該創(chuàng)建kmem_cahe來做“內(nèi)存池”),分別用于存放struct epitem和eppoll_entry。
?
?
Epoll的接口:
epoll是Linux內(nèi)核為處理大批句柄而作改進的poll,是Linux下多路復(fù)用IO接口select/poll的增強版本,它能顯著的減少程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率。因為它會復(fù)用文件描述符集合來傳遞結(jié)果而不是迫使開發(fā)者每次等待事件之前都必須重新準備要被偵聽的文件描述符集合,另一個原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。epoll除了提供select\poll那種IO事件的電平觸發(fā)(Level Triggered)外,還提供了邊沿觸發(fā)(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態(tài),減少epoll_wait/epoll_pwait的調(diào)用,提供應(yīng)用程序的效率。
1.工作函數(shù)
1>.int epoll_create(int size);
創(chuàng)建一個epoll的句柄,size用來告訴內(nèi)核這個監(jiān)聽的數(shù)目fd+1,每個epoll都會占用一個fd值,可以在/proc/進程id/fd/查看。記得close()。
2>.int epoll_ctl(int epfd,int op,int fd ,struct epoll_event *event);
epoll的事件注冊函數(shù),epoll的控制函數(shù);
這里先注冊要監(jiān)聽的事件類型。第一個參數(shù)是epoll_create()的返回值,第二個參數(shù)表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數(shù)是需要監(jiān)聽的fd,第四個參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事,struct epoll_event結(jié)構(gòu)如下:
| ? typedef union epoll_data { ??? void *ptr;//數(shù)據(jù)指針 ??? 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涵蓋了調(diào)用epoll_ctl增加或者修改某指定句柄時寫入的信息,epoll_event->event,則包含了返回事件的位域。 |
?
events可以是以下幾個宏的集合:
EPOLLIN :表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉);
EPOLLOUT:表示對應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對應(yīng)的文件描述符發(fā)生錯誤;
EPOLLHUP:表示對應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對于水平觸發(fā)(Level Triggered)來說的。
EPOLLONESHOT:只監(jiān)聽一次事件,當監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個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);
等待事件的產(chǎn)生,類似于select()調(diào)用。參數(shù)events用來從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個events有多大,這個 maxevents的值不能大于創(chuàng)建epoll_create()時的size,參數(shù)timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時。
?
工作方式:
LT/ET:
LT(level triggered):水平觸發(fā),缺省方式,同時支持block和no-block socket,在這種做法中,內(nèi)核告訴我們一個文件描述符是否被就緒了,如果就緒了,你就可以對這個就緒的fd進行IO操作。如果你不作任何操作,內(nèi)核還是會繼續(xù)通知你的,所以,這種模式編程出錯的可能性較小。傳統(tǒng)的select\poll都是這種模型的代表。
?
ET(edge-triggered):邊沿觸發(fā),高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變?yōu)榫途w狀態(tài)時,內(nèi)核通過epoll告訴你。然后它會假設(shè)你知道文件描述符已經(jīng)就緒,并且不會再為那個描述符發(fā)送更多的就緒通知,直到你做了某些操作導(dǎo)致那個文件描述符不再為就緒狀態(tài)了(比如:你在發(fā)送、接受或者接受請求,或者發(fā)送接受的數(shù)據(jù)少于一定量時導(dǎo)致了一個EWOULDBLOCK錯誤)。但是請注意,如果一直不對這個fs做IO操作(從而導(dǎo)致它再次變成未就緒狀態(tài)),內(nèi)核不會發(fā)送更多的通知。
?
應(yīng)用模式:
那么究竟如何來使用epoll呢?其實非常簡單。
通過在包含一個頭文件#include <sys/epoll.h> 以及幾個簡單的API將可以大大的提高你的網(wǎng)絡(luò)服務(wù)器的支持人數(shù)。
?
首先通過create_epoll(int maxfds)來創(chuàng)建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)。這個函數(shù)會返回一個新的epoll句柄,之后的所有操作將通過這個句柄來進行操作。在用完之后,記得用close()來關(guān)閉這個創(chuàng)建出來的epoll句柄。
?
之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網(wǎng)絡(luò)接口,看哪一個可以讀,哪一個可以寫了。基本的語法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數(shù)操作成功之后,epoll_events里面將儲存所有的讀寫事件。max_events是當前需要監(jiān)聽的所有socket句柄數(shù)。最后一個timeout是 epoll_wait的超時,為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件范圍,為任意正整數(shù)的時候表示等這么長的時間,如果一直沒有事件,則范圍。一般如果網(wǎng)絡(luò)主循環(huán)是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環(huán)的效率。
?
epoll_wait范圍之后應(yīng)該是一個循環(huán),遍利所有的事件。
?
幾乎所有的epoll程序都使用下面的框架(尤其是socket):
?
??? for( ; ; )
??? {
??????? nfds = epoll_wait(epfd,events,20,500);
??????? for(i=0;i<nfds;++i)
??????? {
??????????? if(events[i].data.fd==listenfd) //有新的連接;我們可以注冊多個FD,如果內(nèi)核發(fā)現(xiàn)事件,就會載入events,如果有我們要的描述符也就是listenfd,說明某某套接字監(jiān)聽描述符所對應(yīng)的事件發(fā)生了變化。每次最多監(jiān)測20個fd數(shù)。
??????????? {
??????????????? 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的監(jiān)聽隊列中
??????????? }
??????????? else if( events[i].events&EPOLLIN ) //接收到數(shù)據(jù),讀socket,數(shù)據(jù)可讀標志EPOLLIN
??????????? {
??????????????? n = read(sockfd, line, MAXLINE)) < 0??? //讀
??????????????? ev.data.ptr = md;???? //md為自定義類型,添加數(shù)據(jù)
??????????????? ev.events=EPOLLOUT|EPOLLET;
??????????????? epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環(huán)時發(fā)送數(shù)據(jù),異步處理的精髓
??????????? }
??????????? else if(events[i].events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送,寫socket
??????????? {
??????????????? struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;??? //取數(shù)據(jù)
??????????????? sockfd = md->fd;
??????????????? send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );??????? //發(fā)送數(shù)據(jù)
??????????????? ev.data.fd=sockfd;
??????????????? ev.events=EPOLLIN|EPOLLET;
??????????????? epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環(huán)時接收數(shù)據(jù)
??????????? }
??????????? else
??????????? {
??????????????? //其他的處理
??????????? }
??????? }
??? }
1.Linux下多線程epoll編程
來自 <http://blog.csdn.net/susubuhui/article/details/37906287>
2.epoll + 多線程實現(xiàn)并發(fā)網(wǎng)絡(luò)連接處理
來自 <http://www.cnblogs.com/iTsihang/archive/2013/05/23/3095775.html>
3.高并發(fā)的epoll+線程池,業(yè)務(wù)在線程池內(nèi)
來自 <http://blog.chinaunix.net/uid-311680-id-2439722.html>?
轉(zhuǎn)載于:https://www.cnblogs.com/ypwen/p/4725532.html
總結(jié)
以上是生活随笔為你收集整理的Linux epoll 笔记(高并发事件处理机制)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里云linux下web服务器配置
- 下一篇: dubbo报错Data length t