即墨专业医院网站制作公司,新媒体营销案例分析,北京装修公司排名前十名,浙江省财务开发公司官网Epoll基本介绍在linux的网络编程中#xff0c;很长的时间都在使用select来做事件触发。在linux新的内核中#xff0c;有了一种替换它的机制#xff0c;就是epoll。相比于 select#xff0c;epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select… Epoll基本介绍在linux的网络编程中很长的时间都在使用select来做事件触发。在linux新的内核中有了一种替换它的机制就是epoll。相比于 selectepoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中它是采用轮询来处理的轮询的fd 数目越多自然耗时越多。并且在linux/posix_types.h头文件有这样的声明#define __FD_SETSIZE 1024表示select最多同时监听1024个fd当然可以通过修改头文件再重编译内核来扩大这个数目但这似乎并不治本。所以先来看看二者的区别select的特点select 选择句柄的时候是遍历所有句柄也就是说句柄有事件响应时select需要遍历所有句柄才能获取到哪些句柄有事件通知因此效率是非常低。但是如果连接很少的情况下 select和epoll的LT触发模式相比 性能上差别不大。这里要多说一句select支持的句柄数是有限制的 同时只支持1024个这个是句柄集合限制的如果超过这个限制很可能导致溢出而且非常不容易发现问题 TAF就出现过这个问题 调试了n天才发现当然可以通过修改linux的socket内核调整这个参数。epoll的特点epoll对于句柄事件的选择不是遍历的是事件响应的就是句柄上事件来就马上选择出来不需要遍历整个句柄链表因此效率非常高内核将句柄用红黑树保存的。对于epoll而言还有ET和LT的区别LT表示水平触发ET表示边缘触发两者在性能以及代码实现上差别也是非常大的。 epoll函数epoll的接口非常简单一共就三个函数1.int epoll_create( int size ); //int close(int epfd);创建一个epoll的句柄size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数给出最大监听的fd1的值。需要注意的是当创建好epoll句柄后它就是会占用一个fd值在linux下如果查看/proc/进程id/fd/是能够看到这个fd的所以在使用完epoll后必须调用close()关闭否则可能导致fd被耗尽。2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event );epoll的事件注册函数它不同与select()是在监听事件时告诉内核要监听什么类型的事件而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值第二个参数表示动作用三个宏来表示EPOLL_CTL_ADD注册新的fd到epfd中EPOLL_CTL_MOD修改已经注册的fd的监听事件EPOLL_CTL_DEL从epfd中删除一个fd第三个参数是需要监听的fd第四个参数是告诉内核需要监听什么事struct epoll_event结构如下struct epoll_event
{__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
}
typedef union epoll_data {void *ptr;int fd;__uint32_t u32;__uint64_t u64;
} epoll_data_t;
events可以是以下几个宏的集合EPOLLIN 表示对应的文件描述符可以读包括对端SOCKET正常关闭EPOLLOUT表示对应的文件描述符可以写EPOLLPRI表示对应的文件描述符有紧急的数据可读这里应该表示有带外数据到来EPOLLERR表示对应的文件描述符发生错误EPOLLHUP表示对应的文件描述符被挂断EPOLLET 将EPOLL设为边缘触发(Edge Triggered)模式这是相对于水平触发(Level Triggered)来说的。只有在使用epoll ET(Edge Trigger)模式的时候才需要关注数据是否读取完毕了。使用select或者epoll的LT模式其实根本不用关注数据是否读完了select/epoll检测到有数据可读去读就OK了EPOLLONESHOT只监听一次事件当监听完这次事件之后如果还需要继续监听这个socket的话需要再次把这个socket加入到EPOLL队列里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表示已超时。 epoll_wait运行的原理是 等侍注册在epfd上的socket fd的事件的发生如果发生则将发生的sokct fd和事件类型放入到events数组中。 并且将注册在epfd上的socket fd的事件类型给清空所以如果下一个循环你还要关注这个socket fd的话则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空只是事件类型清空。这一步非常重要。 水平出发和边缘触发LT水平触发效率会低于ET触发尤其在大并发大流量的情况下。但是LT对代码编写要求比较低不容易出现问题。LT模式服务编写上的表现是只要有数据没有被获取内核就不断通知你因此不用担心事件丢失的情况。ET边缘触发效率非常高在并发大流量的情况下会比LT少很多epoll的系统调用因此效率高。但是对编程要求高需要细致的处理每个请求否则容易发生丢失事件的情况。 再详细一点LT水平触发 是缺省的工作方式并且同时支持block和no-block socket.在这种做法中内核告诉你一个文件描述符是否就绪了然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作内核还是会继续通知你 的所以这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。 效率会低于ET触发尤其在大并发大流量的情况下。但是LT对代码编写要求比较低不容易出现问题。LT模式服务编写上的表现是只要有数据没有被获取内核就不断通知你因此不用担心事件丢失的情况。ET边缘触发 是高速工作方式只支持no-block socket。在这种模式下当描述符从未就绪变为就绪时内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪并且不会再为那个文件描述 符发送更多的就绪通知直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如你在发送接收或者接收请求或者发送接收的数据少于一定量时导致 了一个EWOULDBLOCK 错误。但是请注意如果一直不对这个fd作IO操作(从而导致它再次变成未就绪)内核不会发送更多的通知(only once)。 效率非常高在并发大流量的情况下会比LT少很多epoll的系统调用因此效率高。但是对编程要求高需要细致的处理每个请求否则容易发生丢失事件的情况。 假如有这样一个例子1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2)并且它会返回RFD说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式如果我们在第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()返回的读到的数据长度小于请求的数据长度时就可以确定此时缓冲中已没有数据了也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的以LT方式调用epoll接口的时候它就相当于一个速度比较快的poll(2)并且无论后面的数据是否被使用因此他们具有同样的职能。因为即使使用ET模式的epoll在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式并且同时支持block和no-block socket.在这种做法中内核告诉你一个文件描述符是否就绪了然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作内核还是会继续通知你的所以这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表ET(edge-triggered) 是高速工作方式只支持no-block socket。在这种模式下当描述符从未就绪变为就绪时内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪并且不会再为那个文件描述符发送更多的就绪通知直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如你在发送接收或者接收请求或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误。但是请注意如果一直不对这个fd作IO操作(从而导致它再次变成未就绪)内核不会发送更多的通知(only once),不过在TCP协议中ET模式的加速效用仍需要更多的benchmark确认这句话不理解。在 许多测试中我们会看到如果没有大量的idle -connection或者dead-connectionepoll的效率并不会比select/poll高很多但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接)就会发现epoll的效率大大高于select/poll。未测试另外当使用epoll的ET模型来工作时当产生了一个EPOLLIN事件后读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小那么很有可能是缓冲区还有数据未读完也意味着该次事件还没有处理完所以还需要再次读取while(rs)
{
buflen recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen 0)
{// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno EAGAIN)break;elsereturn;}else if(buflen 0){// 这里表示对端的socket已正常关闭.}if(buflen sizeof(buf)rs 1; // 需要再次读取elsers 0;
}
还有假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回返回- 1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
{
ssize_t tmp;
size_t total buflen;
const char *p buffer;
while(1)
{tmp send(sockfd, p, total, 0);if(tmp 0){// 当send收到信号时,可以继续写,但这里返回-1.if(errno EINTR)return -1;// 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满,// 在这里做延时后再重试.if(errno EAGAIN){usleep(1000);continue;}return -1;}if((size_t)tmp total)return buflen;total - tmp;p tmp;
}
return tmp;
}
单个epoll并不能解决所有问题特别是你的每个操作都比较费时的时候因为epoll是串行处理的。 所以你有还是必要建立线程池来发挥更大的效能。例子 #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
#define MAXLINE 10
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555
#define INFTIM 1000
void setnonblocking(int sock)
{
int opts;
optsfcntl(sock,F_GETFL);
if(opts0)
{
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 i, maxi, listenfd, connfd, sockfd,epfd,nfds;
ssize_t n;
char line[MAXLINE];
socklen_t clilen;
//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
struct epoll_event ev,events[20];
//生成用于处理accept的epoll专用的文件描述符
epfdepoll_create(256);
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
listenfd socket(AF_INET, SOCK_STREAM, 0);
//把socket设置为非阻塞方式
setnonblocking(listenfd);
//设置与要处理的事件相关的文件描述符
ev.data.fdlistenfd;
//设置要处理的事件类型
ev.eventsEPOLLIN|EPOLLET;
//注册epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,ev);
bzero(serveraddr, sizeof(serveraddr));
serveraddr.sin_family AF_INET;
char *local_addr200.200.200.204;
inet_aton(local_addr,(serveraddr.sin_addr));//htons(SERV_PORT);
serveraddr.sin_porthtons(SERV_PORT);
bind(listenfd,(sockaddr *)serveraddr, sizeof(serveraddr));
listen(listenfd, LISTENQ);
maxi 0;
for ( ; ; ) {
//等待epoll事件的发生
nfdsepoll_wait(epfd,events,20,500);
//处理所发生的所有事件
for(i0;infds;i)
{
if(events[i].data.fdlistenfd)
{
connfd accept(listenfd,(sockaddr *)clientaddr, clilen);
if(connfd0){
perror(connfd0);
exit(1);
}
setnonblocking(connfd);
char *str inet_ntoa(clientaddr.sin_addr);
std::coutconnect from _u115 ?trstd::endl;
//设置用于读操作的文件描述符
ev.data.fdconnfd;
//设置用于注测的读操作事件
ev.eventsEPOLLIN|EPOLLET;
//注册ev
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,ev);
}
else if(events[i].eventsEPOLLIN)
{
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::coutreadline errorstd::endl;
} else if (n 0) {
close(sockfd);
events[i].data.fd -1;
}
//设置用于写操作的文件描述符
ev.data.fdsockfd;
//设置用于注测的写操作事件
ev.eventsEPOLLOUT|EPOLLET;
//修改sockfd上要处理的事件为EPOLLOUT
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,ev);
}
else if(events[i].eventsEPOLLOUT)
{
sockfd events[i].data.fd;
write(sockfd, line, n);
//设置用于读操作的文件描述符
ev.data.fdsockfd;
//设置用于注测的读操作事件
ev.eventsEPOLLIN|EPOLLET;
//修改sockfd上要处理的事件为EPOLIN
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,ev);
}
}
}
}
对epoll的操作就这么简单总共不过4个APIepoll_create, epoll_ctl, epoll_wait和close。