高效epoll原理

来源:互联网 发布:深圳证券交易所软件 编辑:程序博客网 时间:2024/05/20 10:20

epoll简介

epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

epoll常规用法

epoll相关的系统调用有3个:epoll_create, epoll_ctl和epoll_wait。在头文件<sys/epoll.h> 中被声明

 int epoll_create(int size);
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 int epoll_wait(int epfd, struct epoll_event *events,int maxevents,   int timeout);

epoll_create用于建立一个epoll fd。参数size是内核保证能够正确处理的最大文件描述符数目(现在内核使用红黑树组织epoll相关数据结构,不再使用这个参数)。

epoll_ctl可以操作上面建立的epoll fd,例如,将刚建立的socket fd加入到epoll中让其监控,或者把 epoll正在监控的某个socket fd移出epoll,不再监控它等等。

epoll_wait在调用时,在给定的timeout时间内,当在监控的这些文件描述符中的某些文件描述符上有事件发生时,就返回用户态的进程。

两种工作模式  

LT(level triggered,水平触发)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。   
ET (edge-triggered,边缘触发)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。

/* 使用 epoll 写的回射服务器 将从client中接收到的数据再返回给client */  #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 100  #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;      string szTemp("");      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结构体的变量,ev用于注册事件,数组用于回传要处理的事件      struct epoll_event ev, events[20];      //创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大      epfd = epoll_create(256); //生成用于处理accept的epoll专用的文件描述符      struct sockaddr_in clientaddr;      struct sockaddr_in serveraddr;      listenfd = socket(AF_INET, SOCK_STREAM, 0);      //把socket设置为非阻塞方式      //setnonblocking(listenfd);      //设置与要处理的事件相关的文件描述符      ev.data.fd=listenfd;      //设置要处理的事件类型      ev.events=EPOLLIN|EPOLLET;      //注册epoll事件      epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);      bzero(&serveraddr, sizeof(serveraddr)); /*配置Server socket的相关信息 */      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事件的发生          //返回需要处理的事件数目nfds,如返回0表示已超时。          nfds=epoll_wait(epfd,events,20,500);          //处理所发生的所有事件          for(i=0; i < nfds; ++i)          {              //如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。              if(events[i].data.fd == listenfd)              {                  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;                  //设置用于读操作的文件描述符                    ev.data.fd=connfd;                  //设置用于注册的读操作事件                    ev.events=EPOLLIN|EPOLLET;                  //注册ev                  epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); /* 添加 */              }              //如果是已经连接的用户,并且收到数据,那么进行读入。              else if(events[i].events&EPOLLIN)              {                  cout << "EPOLLIN" << endl;                  if ( (sockfd = events[i].data.fd) < 0)                      continue;                  if ( (n = recv(sockfd, line, sizeof(line), 0)) < 0)                   {                        // Connection Reset:你连接的那一端已经断开了,而你却还试着在对方已断开的socketfd上读写数据!                      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;                  }                  szTemp = "";                  szTemp += line;                  szTemp = szTemp.substr(0,szTemp.find('\r')); /* remove the enter key */                  memset(line,0,100); /* clear the buffer */                  //line[n] = '\0';                  cout << "Readin: " << szTemp << endl;                  //设置用于写操作的文件描述符                  ev.data.fd=sockfd;                  //设置用于注册的写操作事件                  ev.events=EPOLLOUT|EPOLLET;                  //修改sockfd上要处理的事件为EPOLLOUT                  epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); /* 修改 */              }              else if(events[i].events&EPOLLOUT) // 如果有数据发送              {                  sockfd = events[i].data.fd;                  szTemp = "Server:" + szTemp + "\n";                  send(sockfd, szTemp.c_str(), szTemp.size(), 0);                  //设置用于读操作的文件描述符                  ev.data.fd=sockfd;                  //设置用于注册的读操作事件                  ev.events=EPOLLIN|EPOLLET;                  //修改sockfd上要处理的事件为EPOLIN                  epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); /* 修改 */              }          } //(over)处理所发生的所有事件      } //(over)等待epoll事件的发生      close(epfd);      return 0;  }

epoll为什么高效(相比select)

select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。

此外,内核使用了slab机制,为epoll提供了快速的数据结构:

epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。

一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题

  1. 执行epoll_create时,创建了红黑树和就绪list链表。
  2. 执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
  3. 执行epoll_wait时立刻返回准备就绪链表里的数据即可。
0 0