码迷,mamicode.com
首页 > 其他好文 > 详细

多路复用——epoll

时间:2016-08-01 23:03:43      阅读:178      评论:0      收藏:0      [点我收藏+]

标签:linux   epoll   lt   原理   et   优缺点   

1、基本知识

  epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。


2、epoll函数

  epoll操作过程需要三个接口,分别如下:

#include <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);

(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的事件注册函数,它不同与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队列里

(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表示已超时。


3、工作模式

  epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

  ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

  ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。


4、原理

1)调用epoll_create时,做了以下事情:

    内核帮我们在epoll文件系统里建了个file结点;

    在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket;

    建立一个list链表,用于存储准备就绪的事件。

2)调用epoll_ctl时,做了以下事情:

     把socket放到epoll文件系统里file对象对应的红黑树上;

     给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。

3)调用epoll_wait时,做了以下事情:

      观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。

总结如下:

         一颗红黑树,一张准备就绪句柄链表,少量的内核cache,解决了大并发下的socket处理问题。

         执行epoll_create时,创建了红黑树和就绪链表; 

          执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向

          准备就绪链表中插入数据; 

           执行epoll_wait时立刻返回准备就绪链表里的数据即可。

  两种模式的实现:

           当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait检查这些socket,如果是LT模式,并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表。所以,LT模式的句柄,只要它上面还有事件,epoll_wait每次都会返回。


5、epoll的优点

    1. 本身没有最大并发连接的限制,仅受系统中进程能打开的最大文件数目限制;

    2. 效率提升:只有活跃的socket才会主动的去调用callback函数;

    3. 省去不必要的内存拷贝:epoll通过内核与用户空间mmap同一块内存实现。

当然,以上的优缺点仅仅是特定场景下的情况:高并发,且任一时间只有少数socket是活跃的。如果在并发量低,socket都比较活跃的情况下,select就不见得比epoll慢了(就像我们常常说快排比插入排序快,但是在特定情况下这并不成立)。


6、epoll的缺点

1. 相对select来说, epoll的跨平台性不够用 只能工作在linux下, 而select可以在windows linux apple上使用, 还有手机端android iOS之类的都可以. android虽然是linux的 内核 但早期版本同样不支持epoll

 2. 相对select来说 还是用起来还是复杂了一些, 不过和IOCP比起来 增加了一点点的复杂度却基本上达到了IOCP的并发量和性能, 而复杂度远远小于IOCP.

3. 相对IOCP来说 对多核/多线程的支持不够好, 性能也因此在性能要求比较苛刻的情况下不如IOCP.


7、epoll实现的TCP server

代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <string.h>
static void my_read(int epfd,int fd,char *buf,int len)
{
  int rs=1;
  while(rs)
  {
    ssize_t size=recv(fd,buf,len,0);
    if(size<0)
    {
      if(errno=EAGAIN)
      {
        break;
      }
      else
      {
        perror("recv");
        return 9;
      }
    }
    else if(size==0)
    {
      //表示对端的sock已经正常关闭
      printf("client close ...\n");
      struct epoll_event ev;
      epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
      close(fd);
      break;
    }
    else{
      buf[size-1]=‘\0‘;
      printf("client # %s\n",buf);
      if(size==len)
      {
        rs=1;
      }
      else
        rs=0;
    }
  }
  
}
static void my_write(int fd,char *buf,int len) 
{
  int ws=1;
  while(ws)
  {
    ssize_t size=send(fd,buf,len,0);
    if(size<0)
    {
      //缓冲区已经满了,延时重试
      if(errno==EAGAIN)
      {
        usleep(1000);
        continue;
      }
      if(errno==EINTR)
      {
        return -1;
      }
    }
    if(size==len)
    {
      continue;
    }
    len-=size;
    buf+=size;
  }
}
static void set_no_block(int fd)
{
  //先得到之前的状态,在之前状态上在加入新状态
  int before=fcntl(fd,F_GETFL);
  fcntl(fd,F_SETFL,before|O_NONBLOCK);
}
static int startup(const char *ip,int port)
{
  int sock=socket(AF_INET,SOCK_STREAM,0);
  if(sock<0)
  {
    perror("sock");
    return 2;
  }
  int opt=1;
  setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
  struct sockaddr_in local;
  local.sin_family=AF_INET;
  local.sin_port=htons(port);
  local.sin_addr.s_addr=inet_addr(ip);
  if(bind(sock,(struct sockaddr *)&local,sizeof(local))<0)
  {
    perror("bind");
    return 3;
  }
  if(listen(sock,5)<0)
  {
    perror("listen");
    return 4;
  }
  return sock;
}
static void usage(const char *proc)
{
  printf("%s [ip] [port]",proc);
}
int main(int argc,char *argv[])
{
  if(argc!=3)
  {
    usage(argv[0]);
    return 1;
  }
  int listen_sock=startup(argv[1],atoi(argv[2]));
  int epfd=epoll_create(128);
  if(epfd<0)
  {
    perror("epoll_create");
    return 5;
  }
  struct epoll_event ev;
  ev.events=EPOLLIN;
  ev.data.fd=listen_sock;
  if(epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev)<0)
  {
    perror("epoll_ctl");
    return 6;
  }
  struct epoll_event evs[128];
  int len=sizeof(evs)/sizeof(evs[0]);
  int ready=0;
  int timeout= -1;
  while(1)
  {
    switch(ready=epoll_wait(epfd,evs,len,timeout))
    {
      case 0:
        printf("timeout..\n");
        break;
      case -1:
        perror("epoll_wait");
        return 7;
        break;
      default:
        {
          int i=0;
          for(i;i<ready;i++)
          {
            //LISTEN  SOCKET
            int fd=evs[i].data.fd;
            if(i==0&&fd==listen_sock&&evs[i].events&EPOLLIN)
            {
              struct sockaddr_in peer;
              socklen_t len=sizeof(peer);
              int new_sock=accept(listen_sock,(struct sockaddr*)&peer,&len);
              if(new_sock<0)
              {
                perror("accept");
                return 8;
              }
              else
              {
                //在epoll_wait之后改变其文件状态,为非阻塞,ET工作
                set_no_block(new_sock);
                printf("get new socket:ip %s:port %d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
                ev.events=EPOLLIN|EPOLLET;
                ev.data.fd=new_sock;
                if(epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev)<0)
                {
                  perror("epoll_ctl");
                  return 9;
                }
              }
	          }	
            else
            {
              //read
                if(evs[i].events&EPOLLIN)
                {
                  char buf[1024];
                  //my_read(fd,buf,len);
                  ssize_t _s=recv(fd,buf,sizeof(buf)-1,0);
                  if(_s>0)
                  {
                    buf[_s-1]=‘\0‘;
                    printf("client # %s\n",buf);
                    //read finish change to write
                    ev.data.fd=fd;
                    ev.events=EPOLLOUT;
                    epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
                  }
                  else if(_s==0)
                  {
                    printf("client close ....\n");
                    epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
                    close(fd);
                  }
                  else
                  {
                    perror("recv");
                    return 10;
                  }
                }
                //write
                else if(evs[i].events&EPOLLOUT)
                {
                    char *msg="HTTP/1.1 200 OK\r\n\r\n<html><h1>hello momo</h1></html>\r\n";
                    //my_send(fd,msg,sizeof(msg)-1);
                    send(fd,msg,strlen(msg),0);
                    epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
                    close(fd);
                }
                else
	              {
                    continue;
                }
                  
              }
            }
        }
        break;
    }
  }
  return 0;
  
}


本文出自 “momo就是辣么萌” 博客,请务必保留此出处http://momo462.blog.51cto.com/10138434/1833204

多路复用——epoll

标签:linux   epoll   lt   原理   et   优缺点   

原文地址:http://momo462.blog.51cto.com/10138434/1833204

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!