linux中epoll模型的总结

一、前言

    epoll是Linux下的一种IO多路复用技术。简单的说就是可以实现对多个文件描述符的管理和操作,比如说同时监听多个套接字。epoll的功能跟select很像,但是又能够解决select在大规模并发网络应用场景下效率低下的问题,绝对是大型网络程序(Http服务器)的利器。

二、epoll的特点

    为了凸显epoll的特点,先来讲一下select的缺点:
    1、效率问题。这是select最大的问题,也是由select的机制决定的。select的实现机制是,轮询所有集合中的套接字,如果有套接字就绪就返回。这样的话select的效率和套接字的数量是成反比的,所以随着套接字的增加select的效率也会不断下降。
    2、最大并发数限制。受限于最大文件描述符数量的限制,select的最大并发数是2048.
    3、内核和用户空间之间的内存拷贝问题。select需要通过内核和用户空间之间的内存拷贝来实现fd消息的通知。
    下面就来讲一下epoll的优点:
    1、效率不随套接字数量变化。不同于select,epoll采用的是中断方式,某个socket就绪后会直接调用回调函数,这就避免了对全体套接字的扫描。这点从epoll的使用方式就能看出来。epoll只需要执行一次 epoll_ctl函数来注册要监听的文件描述符,以后便可以直接调用 epoll_wait函数来等待描述符可用。而select每次调用select函数都要将要监听的文件描述符作为参数传递进去,再由select传递给内核。
    2、最大并发数无限制。epoll中的并发数不受打开文件描述符数量限制,只受系统打开文件数量上限的限制,而这个上限通常是很大的(至少几万)。
    3、内核和用户空间之间不需要内存拷贝。epoll采用的是mmap(内存映射)技术,因此不需要内存拷贝,提高了效率。

三、epoll的两种模式

    要使用epoll就要先知道epoll的两种工作模式:LT(level trigger,水平触发)和ET(edge trigger,边缘触发)。
    1、LT模式
    此模式是epoll的默认使用模式。LT模式下每次调用epoll-wait时只要有套接字就绪,epoll-wait就会返回通知用户态程序。也就是说,每次的epoll-wait调用都是平等的,所以称之为水平触发。
    2、ET模式
    ET模式下调用epoll-wait函数时,每当一个套接字就绪并触发一次事件后,这个事件就会被删除,以后再次调用epoll-wait函数时就不会再因为相同的事件而返回,即使这个事件并没有得到处理。也就是说ET模式下epoll能够获悉套接字的状态,只有状态变化的时候(例如,从不可读到可读),epoll才会通知用户态。
    看了上面的定义,估计大部分人还是难以理解这两种模式的区别。下面通过一个简单的例子说明:
    假设我们往epoll里边注册了一个套接字,并采用ET模式,然后调用epoll-wait监听这个套接字是否可读。之后这个套接字收到了2KB的数据,这时候epoll-wait会返回通知用户态程序读取这个套接字。之后用户态程序只读取了1KB的数据,这个套接字中还有1KB的数据没有读,便再次调用了epoll-wait函数。然后epoll-wait函数就阻塞并等待下一个事件了,也就是说前边少读的那1KB数据将永远不会被读取。而如果我们使用的是LT模式,那么第二次调用epoll-wait函数的时候函数仍然会返回通知程序读取套接字剩下的1KB数据,这样那1KB的数据就能得到读取了。
    看了上面的描述,似乎ET模式相比LT会多很多问题,那么epoll为什么还要设计ET模式?凡事存在即合理,ET模式能够存在必然也是有它的用处的。ET模式还有另一个名字:高速模式。ET模式在某些情况下可以提高程序的效率相比LT模式。下面举一个简单的例子:
    一个程序创建了一个用来发送数据的套接字sock1,此后程序需要发送数据的时候就会利用epoll监听sock1,如果sock1可写,程序就通过sock1发送要发送的数据。这时候如果我们用的是LT模式,那么当这个sock1的时候,epoll会通知用户写入数据,之后用户写入了自己要发送的数据,但是写入的数据并没有用完发送缓存。之后用户再次调用epoll函数监听其他事件(注意此时用户程序不再需要通过sock1发送数据),epoll仍会因为sock1套接字可写而返回。也就是此后只要调用epoll-wait函数,函数必然会返回直到用户把发送缓存写满为止。但是要知道的是一般情况下,发送缓存是用不完的,因此除非程序把发送套接字移除监听列表,否则程序就会无休止的收到发送套接字的可写通知,这显然会大大降低程序的效率。
    而如果我们使用LT模式,上边的问题就可以得到解决。因为这种模式下套接字可写的事件只会通知一次,即使用户没有用完发送缓存。但是监管ET模式有上边说到的应用场景,但是使用的时候仍然要格外小心。程序必须能够在下次epoll事件之前把上次的epoll事件处理干净。具体的方法是:
    当套接字可读的时候,多次读取套接字直到套接字没有数据可读;
    当一个tcp连接到达后,多次调用accept接受到达连接直到没有未接受的连接。

四、epoll的使用

    前边讲了那么多epoll的特点,下面终于到了真枪实战的环节。epoll的使用很简单,你只需要掌握3个函数即可。
    1、 int epoll_create(int size);      
     创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好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 */  
};  
    events可以是以下几个宏的集合: 
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); 
EPOLLOUT:表示对应的文件描述符可以写; 
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); 
EPOLLERR:表示对应的文件描述符发生错误; 
EPOLLHUP:表示对应的文件描述符被挂断; 
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 
    data是个集合,里边可以存放fd或者指向用户自定义数据结构的指针。当所监听的fd的事件发生后,events结构体会通过epoll-wait函数重新返回,并且里边的内容和调用epoll_ctl注册时填写的内容是相同的。我们可以通过这个集合自己定义事件结构体,里边存放事件所属的fd,回掉函数等信息,这样当事件发生的时候就可以方便地对事件进行处理了。不过需要注意的是,如果此处data保存的是指针,那么指针指向的内存空间在epoll-wait调用的时候一定要有效。我们可以通过定义全局静态变量来保证data指向的内存是有效的,应该避免让data指向动态分配的内存空间,因为这块空间可能随时被销毁。


     3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);  
    等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,内核会把epoll_ctl注册时填写的struct epoll_event内容复制给events参数所指向的内存。maxevents告之内核这个events最大可以是多大,这个maxevents的值不能大于创建epoll_create()时的size,这个参数主要是为了避免epoll-wait传出来的event数量过大以至于超出了events指针所指向的内存空间从而造成溢出。参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。 注意epoll事件的到来是不受epoll-wait是否调用的影响的,也就是说即使一个套接字变得可读这个事件发生的时候epoll-wait没有被执行,那么等到以后epoll-wait被执行的时候让然会获取这个事件。

五、epoll实例

#include <sys/socket.h>
#include <sys/epoll.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <fcntl.h> 
#include <unistd.h>   
#include <errno.h> 
#include <stdio.h>
#include <stdlib.h> 
#include <string.h> 
#include <iostream> 

using namespace std; 

#define MAX_EVENTS 10

int epoll_fd;
struct my_events{
 int fd;
 char text[64];
};

struct my_events g_events[MAX_EVENTS];
static int ind=1;

void eventAdd(int epfd,int sock,int events,void *cp)
{
 struct epoll_event epv = {0, {0}}; 
 epv.data.ptr=cp;
 epv.events=events;
 if(epoll_ctl(epfd, EPOLL_CTL_ADD,sock , &epv) < 0){
         printf("Event Add failed[fd=%d], evnets[%d]\n", sock, events);    
  exit(-1);
 }
     else    
         printf("Event Add OK[fd=%d], evnets[%0X]\n", sock, events);
}

void eventDel(int epfd,int sock,int events,void *cp)
{
 struct epoll_event epv = {0, {0}};
     epv.data.ptr = cp;    
     int res=epoll_ctl(epfd, EPOLL_CTL_DEL, sock, &epv);
 printf("event del. res:%d\n",res);
}

int acceptConn(int epfd,int fd)
{
 int afd;
 printf("index:%d\n",ind);
 struct sockaddr_in sin;    
     socklen_t len = sizeof(struct sockaddr_in); 
 afd=accept(fd, (struct sockaddr*)&sin, &len);
 if(afd<0){printf("accept error!\n");exit(-1);}
 struct my_events *ev=&g_events[ind];
 ev->fd=afd;
 strcpy(ev->text,"date come!");
 eventAdd(epfd,afd,EPOLLIN,(void *)ev);//wrong
 printf("new conn[%s:%d]\n", inet_ntoa(sin.sin_addr),ntohs(sin.sin_port));
 ind++;
}

void recvDate(int fd)
{
 int len;
 char buf[64];
 len=recv(fd, buf, 64, 0);
 buf[len]=0;
 if(len>0){
  printf("%d msg recved from sock %d.\nmsg:%s\n",len,fd,buf);
  if(send(fd,buf,len,0)<0){printf("send error!\n");}
 }else if(len==0){
  printf("sock %d closed!\n",fd);
  close(fd);
 }else{
  printf("recv error!errno:%s\n",strerror(errno));
 }
}

void fdListenInit(int epfd,short port)
{
 int listenFd = socket(AF_INET, SOCK_STREAM, 0);
 if(listenFd<0){printf("socket error!\n");exit(-1);}
 struct my_events *ev=&g_events[MAX_EVENTS];
 ev->fd=listenFd;
 sockaddr_in sin;    
 bzero(&sin, sizeof(sin));    
 sin.sin_family = AF_INET;    
 sin.sin_addr.s_addr = INADDR_ANY;    
  sin.sin_port = htons(port);    
 if(bind(listenFd, (const sockaddr*)&sin, sizeof(sin))==-1){
  printf("bind error. errno:%s\n",strerror(errno));
 }
 listen(listenFd, 5);
 strcpy(ev->text,"new conn!");
 eventAdd(epfd,listenFd,EPOLLIN,(void *)ev);
 //eventAdd(epfd,listenFd,EPOLLIN|EPOLLET,(void *)ev);
 printf("start listen %d\n",listenFd);
}

int main()
{
 epoll_fd = epoll_create(MAX_EVENTS);
 if(epoll_fd<0){printf("create epoll error!\n");exit(-1);}
 fdListenInit(epoll_fd,8888);
 struct epoll_event events[MAX_EVENTS];
 int fds,i;
 ind=1;
 //sleep(10);
 printf("main while\n");
 int j=10;
 while(1)
 {
  fds=epoll_wait(epoll_fd,events,MAX_EVENTS,1000000);
  if(fds<0){printf("epoll wait error!\n");exit(-1);}
  for(i=0;i<fds;i++)
  {
   if(events[i].events==EPOLLIN){
    struct my_events *ev=(struct my_events *)events[i].data.ptr;
    printf("event EPOLLIN:%s\n",ev->text);
    if(!strcmp(ev->text,"new conn!")){
     int sk;
    	sk=acceptConn(epoll_fd,ev->fd);
    }else if(!strcmp(ev->text,"date come!")){
     recvDate(ev->fd);
    }else{
     printf("unknown ev:%s\n",ev->text);
    }
   }else{
    printf("unknown event:%d\n",events[i].events);
   }
  }
 }
}




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值