在linux 沒(méi)有實(shí)現(xiàn)epoll事件驅(qū)動(dòng)機(jī)制之前凌净,我們一般選擇用select或者poll等IO多路復(fù)用的方法來(lái)實(shí)現(xiàn)并發(fā)服務(wù)程序悲龟。在大數(shù)據(jù)、高并發(fā)冰寻、集群等一些名詞唱得火熱之年代须教,select和poll的用武之地越來(lái)越有限,風(fēng)頭已經(jīng)被epoll占盡斩芭。
本文便來(lái)介紹epoll的實(shí)現(xiàn)機(jī)制轻腺,并附帶講解一下select和poll。通過(guò)對(duì)比其不同的實(shí)現(xiàn)機(jī)制秒旋,真正理解為何epoll能實(shí)現(xiàn)高并發(fā)约计。
select()和poll() IO多路復(fù)用模型
- select的缺點(diǎn):
- 單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,通常是1024迁筛,當(dāng)然可以更改數(shù)量煤蚌,但由于select采用輪詢(xún)的方式掃描文件描述符,文件描述符數(shù)量越多细卧,性能越差尉桩;(在linux內(nèi)核頭文件posix_types.h中,有這樣的定義:#define __FD_SETSIZE 1024)贪庙。相比于select蜘犁,epoll最大的好處在于它不會(huì)隨著監(jiān)聽(tīng)fd數(shù)目的增長(zhǎng)而降低效率。
- 內(nèi)核 / 用戶(hù)空間內(nèi)存拷貝問(wèn)題止邮,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu)这橙,產(chǎn)生巨大的開(kāi)銷(xiāo);
- select返回的是含有整個(gè)句柄的數(shù)組导披,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件屈扎;
- select的觸發(fā)方式是水平觸發(fā),應(yīng)用程序如果沒(méi)有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符進(jìn)行IO操作撩匕,那么之后每次select調(diào)用還是會(huì)將這些文件描述符通知進(jìn)程鹰晨。
相比select模型,poll使用鏈表保存文件描述符止毕,因此沒(méi)有了監(jiān)視文件數(shù)量的限制模蜡,但其他三個(gè)缺點(diǎn)依然存在。
拿select模型為例扁凛,假設(shè)我們的服務(wù)器需要支持100萬(wàn)的并發(fā)連接忍疾,則在__FD_SETSIZE 為1024的情況下,則我們至少需要開(kāi)辟1k個(gè)進(jìn)程才能實(shí)現(xiàn)100萬(wàn)的并發(fā)連接谨朝。除了進(jìn)程間上下文切換的時(shí)間消耗外膝昆,從內(nèi)核/用戶(hù)空間大量的無(wú)腦內(nèi)存拷貝丸边、數(shù)組輪詢(xún)等叠必,是系統(tǒng)難以承受的荚孵。因此,基于select模型的服務(wù)器程序纬朝,要達(dá)到10萬(wàn)級(jí)別的并發(fā)訪(fǎng)問(wèn)收叶,是一個(gè)很難完成的任務(wù)。
因此共苛,該epoll上場(chǎng)了判没。
epoll IO多路復(fù)用模型實(shí)現(xiàn)機(jī)制
由于epoll的實(shí)現(xiàn)機(jī)制與select/poll機(jī)制完全不同,上面所說(shuō)的 select的缺點(diǎn)在epoll上不復(fù)存在隅茎。
設(shè)想一下如下場(chǎng)景:有100萬(wàn)個(gè)客戶(hù)端同時(shí)與一個(gè)服務(wù)器進(jìn)程保持著TCP連接澄峰。而每一時(shí)刻,通常只有幾百上千個(gè)TCP連接是活躍的(事實(shí)上大部分場(chǎng)景都是這種情況)辟犀。如何實(shí)現(xiàn)這樣的高并發(fā)俏竞?
在select/poll時(shí)代,服務(wù)器進(jìn)程每次都把這100萬(wàn)個(gè)連接告訴操作系統(tǒng)(從用戶(hù)態(tài)復(fù)制句柄數(shù)據(jù)結(jié)構(gòu)到內(nèi)核態(tài))堂竟,讓操作系統(tǒng)內(nèi)核去查詢(xún)這些套接字上是否有事件發(fā)生魂毁,輪詢(xún)完后,再將句柄數(shù)據(jù)復(fù)制到用戶(hù)態(tài)出嘹,讓服務(wù)器應(yīng)用程序輪詢(xún)處理已發(fā)生的網(wǎng)絡(luò)事件席楚,這一過(guò)程資源消耗較大,因此税稼,select/poll一般只能處理幾千的并發(fā)連接烦秩。
epoll的設(shè)計(jì)和實(shí)現(xiàn)與select完全不同。epoll通過(guò)在Linux內(nèi)核中申請(qǐng)一個(gè)簡(jiǎn)易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)郎仆?B+樹(shù))只祠。把原先的select/poll調(diào)用分成了3個(gè)部分:
-
1)調(diào)用epoll_create()建立一個(gè)epoll對(duì)象
int epfd = epoll_create(int size);
創(chuàng)建一個(gè)epoll的句柄(epoll專(zhuān)用的文件描述符),size就是你在這個(gè)epoll fd上能關(guān)注的最大socket fd數(shù)丸升。這個(gè)參數(shù)不同于select()中的第一個(gè)參數(shù)铆农,給出最大監(jiān)聽(tīng)的fd+1的值(每次有個(gè)麻煩就是要先比較尋找到這個(gè)最大的fd)。需要注意的是狡耻,當(dāng)創(chuàng)建好epoll句柄后墩剖,它就是會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/夷狰,是能夠看到這個(gè)fd的岭皂,所以在使用完epoll后,必須調(diào)用close()關(guān)閉沼头,否則可能導(dǎo)致fd被耗盡爷绘。
-
2)調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬(wàn)個(gè)連接的套接字
函數(shù)聲明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
該函數(shù)用于控制某個(gè)epoll文件描述符上的事件书劝,可以注冊(cè)事件,修改事件土至,刪除事件购对。它不同與select()是在監(jiān)聽(tīng)事件時(shí)告訴內(nèi)核要監(jiān)聽(tīng)什么類(lèi)型的事件,而是在這里先注冊(cè)要監(jiān)聽(tīng)的事件類(lèi)型陶因。
參數(shù):
epfd:由 epoll_create 生成的epoll專(zhuān)用的文件描述符骡苞;
op:要進(jìn)行的操作例如注冊(cè)事件,可能的取值EPOLL_CTL_ADD 注冊(cè)楷扬、EPOLL_CTL_MOD 修 改解幽、EPOLL_CTL_DEL 刪除
fd:需要監(jiān)聽(tīng)的fd文件描述符。每調(diào)用一次這個(gè)函數(shù)烘苹,只操作一個(gè)fd躲株,所以如果要監(jiān)聽(tīng)很多個(gè)fd,也要循環(huán)這個(gè)函數(shù)添加所有的fd镣衡,這個(gè)與select類(lèi)似霜定。
event:指向epoll_event的指針,告訴內(nèi)核需要監(jiān)聽(tīng)什么事件;
如果調(diào)用成功返回0,不成功返回-1捆探。
- structepoll_event結(jié)構(gòu)如下:
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 */
};
events可以是以下幾個(gè)宏的集合:
EPOLLIN: 觸發(fā)該事件然爆,表示對(duì)應(yīng)的文件描述符上有可讀數(shù)據(jù)。(包括對(duì)端SOCKET正常關(guān)閉)黍图;
EPOLLOUT:觸發(fā)該事件曾雕,表示對(duì)應(yīng)的文件描述符上可以寫(xiě)數(shù)據(jù);
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái))助被;
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤剖张;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET:將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式揩环,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的搔弄。
EPOLLONESHOT:只監(jiān)聽(tīng)一次事件,當(dāng)監(jiān)聽(tīng)完這次事件之后丰滑,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話(huà)顾犹,
需要再次把這個(gè)socket加入到EPOLL隊(duì)列里。
編程實(shí)例:
struct epoll_event ev;
//設(shè)置與要處理的事件相關(guān)的文件描述符
ev.data.fd = listenfd;
//設(shè)置要處理的事件類(lèi)型
ev.events = EPOLLIN|EPOLLET;
//注冊(cè)epoll事件
epoll_ctl( epfd, EPOLL_CTL_ADD, listenfd, &ev);
- 3)調(diào)用epoll_wait收集發(fā)生的事件的連接
如此一來(lái)褒墨,要實(shí)現(xiàn)上面說(shuō)是的場(chǎng)景炫刷,只需要在進(jìn)程啟動(dòng)時(shí)建立一個(gè)epoll對(duì)象,然后在需要的時(shí)候向這個(gè)epoll對(duì)象中添加或者刪除連接郁妈。同時(shí)浑玛,epoll_wait的效率也非常高,因?yàn)檎{(diào)用epoll_wait時(shí)噩咪,并沒(méi)有一股腦的向操作系統(tǒng)復(fù)制這100萬(wàn)個(gè)連接的句柄數(shù)據(jù)顾彰,內(nèi)核也不需要去遍歷全部的連接极阅。
- 函數(shù)原型:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產(chǎn)生,類(lèi)似于select()調(diào)用涨享。參數(shù)events用來(lái)從內(nèi)核得到事件的集合筋搏,maxevents告之內(nèi)核這個(gè)events有多大(數(shù)組成員的個(gè)數(shù)),這個(gè)maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size灰伟,參數(shù)timeout是超時(shí)時(shí)間(毫秒拆又,0會(huì)立即返回,-1將不確定栏账,也有說(shuō)法說(shuō)是永久阻塞)。
該函數(shù)返回需要處理的事件數(shù)目栈源,如返回0表示已超時(shí)挡爵。
返回的事件集合在events數(shù)組中,數(shù)組中實(shí)際存放的成員個(gè)數(shù)是函數(shù)的返回值甚垦。返回0表示已經(jīng)超時(shí)茶鹃。
epoll_wait運(yùn)行的原理是:
等待注冊(cè)在epfd上的socket fd的事件的發(fā)生,如果發(fā)生則將發(fā)生的sokct fd和事件類(lèi)型放入到events數(shù)組中艰亮。
并 且將注冊(cè)在epfd上的socket fd的事件類(lèi)型給清空闭翩,所以如果下一個(gè)循環(huán)你還要關(guān)注這個(gè)socket fd的話(huà),則需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)來(lái)重新設(shè)置socket fd的事件類(lèi)型迄埃。這時(shí)不用EPOLL_CTL_ADD,因?yàn)閟ocket fd并未清空疗韵,只是事件類(lèi)型清空。這一步非常重要侄非。
下面來(lái)看一個(gè)服務(wù)器實(shí)例
關(guān)于ET蕉汪、LT兩種工作模式,可以得出這樣的結(jié)論:
ET模式僅當(dāng)狀態(tài)發(fā)生變化的時(shí)候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說(shuō),如果要采用ET模式,需要一直read/write直到出錯(cuò)為止,很多人反映為什么采用ET模式只接收了一部分?jǐn)?shù)據(jù)就再也得不到通知了,大多因?yàn)檫@樣;而LT模式是只要有數(shù)據(jù)沒(méi)有處理就會(huì)一直通知下去的.那么究竟如何來(lái)使用epoll呢?其實(shí)非常簡(jiǎn)單逞怨。
通過(guò)在包含一個(gè)頭文件#include <sys/epoll.h> 以及幾個(gè)簡(jiǎn)單的API將可以大大的提高你的網(wǎng)絡(luò)服務(wù)器的支持人數(shù)者疤。首先通過(guò)create_epoll(int maxfds)來(lái)創(chuàng)建一個(gè)epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)叠赦。這個(gè)函數(shù)會(huì)返回一個(gè)新的epoll句柄驹马,之后的所有操作將通過(guò)這個(gè)句柄來(lái)進(jìn)行操作。在用完之后除秀,記得用close()來(lái)關(guān)閉這個(gè)創(chuàng)建出來(lái)的epoll句柄糯累。
之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來(lái)查詢(xún)所有的網(wǎng)絡(luò)接口鳞仙,看哪一個(gè)可以讀寇蚊,哪一個(gè)可以寫(xiě)了」骱茫基本的語(yǔ)法為:
nfds = epoll_wait(epfd, events, maxevents, -1);
其中epfd為用epoll_create創(chuàng)建之后的句柄仗岸,events是一個(gè)epoll_event*的指針允耿,當(dāng)epoll_wait這個(gè)函數(shù)操作成功之后,epoll_events里面將儲(chǔ)存所有的讀寫(xiě)事件扒怖。max_events是當(dāng)前需要監(jiān)聽(tīng)的所有socket句柄數(shù)较锡。最后一個(gè)timeout是 epoll_wait的超時(shí),為0的時(shí)候表示馬上返回盗痒,為-1的時(shí)候表示一直等下去蚂蕴,直到有事件范圍,為任意正整數(shù)的時(shí)候表示等這么長(zhǎng)的時(shí)間俯邓,如果一直沒(méi)有事件骡楼,則返回。一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線(xiàn)程的話(huà)稽鞭,可以用-1來(lái)等鸟整,這樣可以保證一些效率,如果是和主邏輯在同一個(gè)線(xiàn)程的話(huà)朦蕴,則可以用0來(lái)保證主循環(huán)的效率篮条。
epoll_wait調(diào)用返回之后應(yīng)該是一個(gè)循環(huán),遍利所有的事件吩抓。
幾乎所有的epoll程序都使用下面的框架:(與select函數(shù)調(diào)用返回后思路一致)
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd) //有新的連接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個(gè)連接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監(jiān)聽(tīng)隊(duì)列中
}
else if( events[i].events&EPOLLIN ) //接收到數(shù)據(jù)涉茧,讀socket
{
n = read(sockfd, line, MAXLINE)) < 0 //讀
ev.data.ptr = md; //md為自定義類(lèi)型,添加數(shù)據(jù)
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標(biāo)識(shí)符疹娶,等待下一個(gè)循環(huán)時(shí)發(fā)送數(shù)據(jù)伴栓,異步處理的精髓
}
else if(events[i].events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送,寫(xiě)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); //修改標(biāo)識(shí)符蚓胸,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)
}
else
{
//其他的處理
}
}
}
下面給出一個(gè)完整的服務(wù)器端例子:
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
using namespace std;
#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000
void setnonblocking(int sock)
{
int opts;
opts=fcntl(sock,F_GETFL);
if(opts<0)
{
perror("fcntl(sock,GETFL)");
exit(1);
}
opts = opts|O_NONBLOCK;
if(fcntl(sock,F_SETFL,opts)<0)
{
perror("fcntl(sock,SETFL,opts)");
exit(1);
}
}
int main(int argc, char* argv[])
{
int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
ssize_t n;
char line[MAXLINE];
socklen_t clilen;
if ( 2 == argc )
{
if( (portnumber = atoi(argv[1])) < 0 )
{
fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
return 1;
}
}
else
{
fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
return 1;
}
//聲明epoll_event結(jié)構(gòu)體的變量,ev用于注冊(cè)事件,數(shù)組用于回傳要處理的事件
struct epoll_event ev,events[20];
//生成用于處理accept的epoll專(zhuān)用的文件描述符
epfd=epoll_create(256);
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//把socket設(shè)置為非阻塞方式
//setnonblocking(listenfd);
//設(shè)置與要處理的事件相關(guān)的文件描述符
ev.data.fd=listenfd;
//設(shè)置要處理的事件類(lèi)型
ev.events=EPOLLIN|EPOLLET;
//ev.events=EPOLLIN;
//注冊(cè)epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
char *local_addr="127.0.0.1";
inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber);
serveraddr.sin_port=htons(portnumber);
bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
listen(listenfd, LISTENQ);
maxi = 0;
for ( ; ; ) {
//等待epoll事件的發(fā)生
nfds=epoll_wait(epfd,events,20,500);
//處理所發(fā)生的所有事件
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd)
{
//如果新監(jiān)測(cè)到一個(gè)SOCKET用戶(hù)連接到了綁定的SOCKET端口挣饥,建立新的連接。
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
if(connfd<0){
perror("connfd<0");
exit(1);
}
//setnonblocking(connfd);
char *str = inet_ntoa(clientaddr.sin_addr);
cout << "accapt a connection from " << str << endl;
//設(shè)置用于讀操作的文件描述符
ev.data.fd=connfd;
//設(shè)置用于注測(cè)的讀操作事件
ev.events=EPOLLIN|EPOLLET;
//ev.events=EPOLLIN;
//注冊(cè)ev
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}
else if(events[i].events&EPOLLIN)
{
//如果是已經(jīng)連接的用戶(hù)沛膳,并且收到數(shù)據(jù)扔枫,那么進(jìn)行讀入。
cout << "EPOLLIN" << endl;
if ( (sockfd = events[i].data.fd) < 0)
continue;
if ( (n = read(sockfd, line, MAXLINE)) < 0) {
if (errno == ECONNRESET) {
close(sockfd);
events[i].data.fd = -1;
} else
std::cout<<"readline error"<<std::endl;
} else if (n == 0) {
close(sockfd);
events[i].data.fd = -1;
}
line[n] = '/0';
cout << "read " << line << endl;
//設(shè)置用于寫(xiě)操作的文件描述符
ev.data.fd=sockfd;
//設(shè)置用于注測(cè)的寫(xiě)操作事件
ev.events=EPOLLOUT|EPOLLET;
//修改sockfd上要處理的事件為EPOLLOUT
//epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
else if(events[i].events&EPOLLOUT) // 如果有數(shù)據(jù)發(fā)送
{
sockfd = events[i].data.fd;
write(sockfd, line, n);
//設(shè)置用于讀操作的文件描述符
ev.data.fd=sockfd;
//設(shè)置用于注測(cè)的讀操作事件
ev.events=EPOLLIN|EPOLLET;
//修改sockfd上要處理的事件為EPOLIN
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
}
}
return 0;
}
客戶(hù)端直接連接到這個(gè)服務(wù)器就好了锹安。短荐。
https://blog.csdn.net/ljx0305/article/details/4065058
https://blog.csdn.net/shenya1314/article/details/73691088
https://blog.csdn.net/yusiguyuan/article/details/15027821