epoll机制浅析.docx
《epoll机制浅析.docx》由会员分享,可在线阅读,更多相关《epoll机制浅析.docx(10页珍藏版)》请在冰点文库上搜索。
epoll机制浅析
epoll机制浅析
在linux的网络编程中,很长的时间都在使用select来做事件触发。
在linux新的内核中,有了一种替换它的机制,就是epoll。
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。
因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
并且,在头文件有这样的声明:
#define__FD_SETSIZE1024
表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。
1基本函数
epoll的接口一共就三个函数:
intepoll_create(intsize);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。
这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。
需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:
注册新的fd到epfd中;
EPOLL_CTL_MOD:
修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:
从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,structepoll_event结构如下:
typedefunionepoll_data{
void*ptr;
intfd;
__uint32_tu32;
__uint64_tu64;
}epoll_data_t;
structepoll_event{
__uint32_tevents;/*Epollevents*/
epoll_data_tdata;/*Userdatavariable*/
};
events可以是以下几个宏的集合:
EPOLLIN:
表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:
表示对应的文件描述符可以写;
EPOLLPRI:
表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:
表示对应的文件描述符发生错误;
EPOLLHUP:
表示对应的文件描述符被挂断;
EPOLLET:
将EPOLL设为边缘触发(EdgeTriggered)模式,这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:
只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);
等待事件的产生,类似于select()调用。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。
该函数返回需要处理的事件数目,如返回0表示已超时。
2工作模式
EPOLL有两种模型:
LevelTriggered(LT)水平触发
EdgeTriggered(ET)边沿触发
水平触发是缺省的工作方式,并且同时支持block和no-blocksocket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。
如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错可能性要小一点。
传统的select/poll都是这种模型的代表.
边沿触发是高速工作方式,只支持no-blocksocket。
在这种模式下,当描述符从”未就绪”变为”就绪”时,内核通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK错误)。
但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(onlyonce)。
比如有这样一个例子:
1.我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2.这个时候从管道的另一端被写入了2KB的数据
3.调用epoll_wait
(2),并且它会返回RFD,说明它已经准备好读取操作
4.然后我们读取了1KB的数据
5.调用epoll_wait
(2)......
LT工作模式
以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll
(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。
因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。
调用者可以设定EPOLLONESHOT标志,在epoll_wait
(2)收到事件后,epoll会与事件关联的文件句柄从epoll描述符中禁止掉。
因此当EPOLLONESHOT设定后,使用带有EPOLL_CTL_MOD标志的epoll_ctl
(2)处理文件句柄就成为调用者必须做的事情。
ET工作模式
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait
(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。
只有在监视的文件句柄上发生了某个事件的时候ET工作模式才会汇报事件。
因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。
在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。
因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用epoll_wait
(2)完成后,是否挂起是不确定的。
epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
最好以下面的方式调用ET模式的epoll接口:
i基于非阻塞文件句柄
ii只有当read
(2)或者write
(2)返回EAGAIN时才需要挂起,等待。
但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
3使用实例
那么究竟如何来使用epoll呢?
其实,通过在包含一个头文件#include以及几个简单的API将可以大大的提高你的网络服务器的支持人数。
首先通过create_epoll(intmaxfds)来创建一个epoll的句柄,其中maxfds是你epoll所支持的最大句柄数。
这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。
在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
之后在网络主循环里面,每一帧的调用epoll_wait(intepfd,epoll_eventevents,intmaxevents,inttimeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。
几乎所有的epoll程序都使用下面的框架:
for(;;)
{
nfds=epoll_wait(epfd,events,20,500);
for(i=0;i{
if(listenfd==events[i].data.fd)//有新的连接
{
connfd=accept(listenfd,
(sockaddr*)&clientaddr,
&clilen);
=connfd;
=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}
elseif(events[i].events&EPOLLIN)
{
n=read(sockfd,line,MAXLINE))<0
ev.data.ptr=md;//md为自定义类型,添加数据
=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
elseif(events[i].events&EPOLLOUT)
{
structmyepoll_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
{
//其他的处理
}
}
}
下面给出一个完整的服务器端例子:
#include
#include
#include
#include
#include
#include
#include
#include
#defineMAXLINE5
#defineOPEN_MAX100
#defineLISTENQ20
#defineSERV_PORT5000
#defineINFTIM1000
voidsetnonblocking(intsock)
{
intopts;
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);
}
}
intmain(intargc,char*argv[])
{
inti,maxi,listenfd,connfd,sockfd,epfd,nfds,portnumber;
ssize_tn;
charline[MAXLINE];
socklen_tclilen;
if(2==argc)
{
if((portnumber=atoi(argv[1]))<0)
{
fprintf(stderr,"Usage:
%sportnumber\a\n",argv[0]);
return1;
}
}
else
{
fprintf(stderr,"Usage:
%sportnumber\a\n",argv[0]);
return1;
}
//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
structepoll_eventev,events[20];
//生成用于处理accept的epoll专用的文件描述符
epfd=epoll_create(256);
structsockaddr_inclientaddr;
structsockaddr_inserveraddr;
listenfd=socket(AF_INET,SOCK_STREAM,0);
//把socket设置为非阻塞方式
//setnonblocking(listenfd);
//设置与要处理的事件相关的文件描述符
=listenfd;
//设置要处理的事件类型
=EPOLLIN|EPOLLET;
=EPOLLIN;
//注册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);
bind(listenfd,(sockaddr*)&serveraddr,sizeof(serveraddr));
listen(listenfd,LISTENQ);
maxi=0;
for(;;)
{
//等待epoll事件的发生
nfds=epoll_wait(epfd,events,20,500);
//处理所发生的所有事件
for(i=0;i{
if(listenfd==events[i].data.fd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
{
connfd=
accept(listenfd,(sockaddr*)&clientaddr,&clilen);
if(connfd<0)
{
perror("connfd<0");
exit
(1);
}
//setnonblocking(connfd);
char*str=inet_ntoa(clientaddr.sin_addr);
printf("accaptaconnectionfrom%s\n",str);
//设置用于读操作的文件描述符
=connfd;
//设置用于注测的读操作事件
=EPOLLIN|EPOLLET;
=EPOLLIN;
//注册ev
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}
elseif(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。
{
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
printf("readlineerror\n");
}
elseif(n==0)
{
close(sockfd);
events[i].data.fd=-1;
}
line[n]='\0';
prinf("read%s\n",line);
//设置用于写操作的文件描述符
=sockfd;
//设置用于注册的写操作事件
=EPOLLOUT|EPOLLET;
//修改sockfd上要处理的事件为EPOLLOUT
//epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
elseif(events[i].events&EPOLLOUT)//如果有数据发送
{
sockfd=events[i].data.fd;
write(sockfd,line,n);
//设置用于读操作的文件描述符
=sockfd;
//设置用于注册的读操作事件
=EPOLLIN|EPOLLET;
//修改sockfd上要处理的事件为EPOLIN
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
}
}
return0;
}
客户端直接连接到这个服务器就好了。
。
来自: