事件处理机制之epoll

0.序
1.为什么要采用epoll事件处理机制呢?也就是Epoll的优点。
2.epoll事件处理机制有两种触发方式:ET和LT。有何区别?
3.epoll相关函数
   1)epoll_create函数
   2)epoll_ctl函数
   3)epoll_wait函数
4.程序
5.小结
6.参考文章

0.序
     写了一篇文章是关于事件处理机制之epoll的,但是有点过于冗余,这次将里面的核心内容精简下来。我一直都认为,写博客一方面是为了让他人有一个更快捷的学习方式,更重要的一方面是可以让你对所学知识有一个更加深刻的认识。

1.为什么要采用epoll事件处理机制呢?也就是Epoll的优点。
     一句话,并发性高。因为select和poll事件处理机制都是采用轮询IO的方式。采用轮询IO是最耗时的操作之一。
     可以参考文章 epoll为什么这么快 
     <1>支持一个进程打开大数目的socket描述符(FD)
     select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
     <2>IO效率不随FD数目增加而线性下降
     传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行 操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
     <3>使用mmap加速内核与用户空间的消息传递。
     这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的
     <4>内核微调
     这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。 比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小 --- 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网 卡驱动架构。

     
2.epoll事件处理机制有两种触发方式:ET和LT。有何区别?
     ET:Edge Trigger;LT:Level Trigger。
     作为一名通信专业的学生,当然不能把所学的通信方面的知识忘记了呀,那咱就用信号方面的东西讲讲水平触发和边缘触发。



3.epoll相关函数
1)epoll_create函数
NAME
       epoll_create, epoll_create1 - open an epoll file descriptor

SYNOPSIS
       #include

       int epoll_create(int size);
       int epoll_create1(int flags);
DESCRIPTION
       epoll_create() creates an epoll "instance", requesting the kernel to
       allocate an event backing store dimensioned  for  size  descriptors.
       The  size  is  not  the maximum size of the backing store but just a
       hint to the kernel  about  how  to  dimension  internal  structures.
       (Nowadays, size is ignored; see NOTES below.)

       epoll_create()  returns a file descriptor referring to the new epoll
       instance.  This file descriptor is used for all the subsequent calls
       to  the epoll interface.  When no longer required, the file descrip‐
       tor returned by epoll_create() should be closed by  using  close。
epoll_create()创建一个epoll句柄,内核会分配一个空间用来存放你想关注的文件描述符上是否发生以及发生了什么事件。
epoll_create()返回一个代指新epoll句柄的fd。这个fd作为epoll的接口,会在接下来的所有epoll调用中使用这个fd。

2)epoll_ctl函数
NAME
       epoll_ctl - control interface for an epoll descriptor

SYNOPSIS
       #include

       int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

DESCRIPTION
       This  system  call  performs  control  operations on the epoll instance  referred to by the file descriptor epfd. 

1)int op
 It requests that  the  operation op be performed for the target file descriptor fd.
epoll_ctl执行在epfd代表的epoll句柄上的控制操作。操作op将会在目标文件描述符fd上进行操作。
       Valid values for the op argument are :

       EPOLL_CTL_ADD
              Register  the  target  file  descriptor fd on the epoll instance
              referred to by the file descriptor epfd and associate the  event
              event with the internal file linked to fd.
EPOLL_CTL_ADD:将目标文件描述符fd注册到epoll句柄上,并使事件类型event与文件描述符fd相关联。
       EPOLL_CTL_MOD
              Change  the event event associated with the target file descrip‐
              tor fd.
EPOLL_CTL_MOD:改变目标文件描述符fd的事件类型。
       EPOLL_CTL_DEL
              Remove (deregister) the target file descriptor fd from the epoll
              instance  referred  to by epfd.  The event is ignored and can be
              NULL (but see BUGS below).
EPOLL_CTL_DEL:从epoll句柄epfd中删除目标文件描述符fd。fd对应的事件将会被忽略。
2) struct epoll_event *event
The event argument describes the object linked to the  file  descriptor fd. 
与文件描述符fd相对应的事件类型event,其结构体如下:
 The struct epoll_event is defined as :

           typedef union epoll_data {
               void        *ptr;
               int          fd;
               __uint32_t   u32;
               __uint64_t   u64;
           } epoll_data_t;

           struct epoll_event {
                __uint32_t   events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };

        The  events  member is a bit set composed using the following available event types:
    
常用的事件类型: 
EPOLLIN :表示对应的文件描述符可以读; 
EPOLLOUT:表示对应的文件描述符可以写; 
EPOLLPRI:表示对应的文件描述符有紧急的数据可读 
EPOLLERR:表示对应的文件描述符发生错误; 
EPOLLHUP:表示对应的文件描述符被挂断; 
EPOLLET:表示对应的文件描述符有事件发生;   
3)举例说明其使用方式
struct epoll_event ev; 
//设置与要处理的事件相关的文件描述符 
ev.data.fd=listenfd; 
//设置要处理的事件类型 
ev.events=EPOLLIN|EPOLLET; 
//注册epoll事件 
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); 
3)epoll_wait函数
  epoll_wait,  epoll_pwait  -  wait  for an I/O event on an epoll file  descriptor
作用:等待在epoll 文件描述符上的一个I/O事件。

SYNOPSIS
       #include

       int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

DESCRIPTION
       The epoll_wait() system call waits for events on the epoll instance referred to by the file descriptor epfd.  The memory area pointed to by events will contain the events that will be available for the caller.  Up to maxevents are returned by epoll_wait().  The maxevents argument must be greater than zero.
       epoll_wait系统调用等待epoll句柄中events事件。由events指向的内存区域包含了对于调用者而言可选的事件,即如果注册在epoll句柄上的fd的事件发生,那么就会将发生的fd以及事件类型放入events数组中。当到了最大事件maxevents时,epoll_wait会返回。maxevents必须大于0.
       The call waits for a maximum time of timeout milliseconds.  Specifying a timeout of -1 makes epoll_wait() wait  indefinitely,  while  specifying  a  timeout    equal to zero makes epoll_wait() to return immediately even if no events are available (return code equal to zero).
       该系统调用epoll_wait等待timeout ms。如果timeout=-1,则是无限等待;如果timeout=0,即使没有事件可供选择,epoll_wait也会立即返回。
The struct epoll_event is defined as :

           typedef union epoll_data {
               void    *ptr;
               int      fd;
               uint32_t u32;
               uint64_t u64;
           } epoll_data_t;

           struct epoll_event {
               uint32_t     events;    /* Epoll events */
               epoll_data_t data;      /* User data variable */
           };

       The  data of each returned structure will contain the same data the user set with an epoll_ctl(2) (EPOLL_CTL_ADD,EPOLL_CTL_MOD) while the events member will  contain the returned event bit field.
注意:epoll_wait会将注册在epfd上的已经发生事件的fd的事件类型清空,所以如果下一个循环还要关注这个fd的话,就必须通过epoll_ctl(epfd,EPOLL_CTL_MOD,xx,xxxx)来重新设置fd的事件类型。这时不用EPOLL_CTL_ADD,因为fd没有被清空,只是事件类型被清空。
4.程序
/*
** 这是服务器端程序
**本程序来自于 http://www.cppblog.com/converse/archive/2008/04/29/48482.html
**  本程序为采用  Epoll 事件的简单程序实现。我添加了#define ET 1 如果#define ET 0 ,那么epoll采用LT触发,如果#define ET 1,那么采用ET触发
** 采用LT触发时, 只要还有数据留在 buffer   ,server 就会继续得到通知
*/
#define  ET 0
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define  MAXLINE 5
#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  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 专用的文件描述符
    epfd=epoll_create(256);
     struct  sockaddr_in clientaddr;
     struct  sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
     // socket  设置为非阻塞方式
#if  ET
    setnonblocking(listenfd);
#endif
     // 设置与要处理的事件相关的文件描述符
    ev.data.fd=listenfd;
     // 设置要处理的事件类型
#if  ET
      ev.events=EPOLLIN|EPOLLET;
#else
      ev.events=EPOLLIN;
#endif
     // 注册 epoll  事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    memset(&serveraddr,0,  sizeof (serveraddr));
    serveraddr.sin_family = AF_INET;
    // char *local_addr="127.0.0.1";
    // inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port=htons(SERV_PORT);
    bind(listenfd,(  struct  sockaddr *)&serveraddr,  sizeof  (serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    clilen =  sizeof ( struct  sockaddr);
     for  ( ; ; ) {
         // 等待 epoll  事件的发生
        nfds=epoll_wait(epfd,events,20,500);
         // 处理所发生的所有事件     
         for (i=0;i<nfds;++i)
        {
             if(events[i].data.fd==listenfd)
            {
                printf(  "001\n" );
                connfd = accept(listenfd,(  struct  sockaddr *)&clientaddr, &clilen);
                 if (connfd<0){
                    perror(  "connfd<0" );
                    exit(1);
                }
                 //setnonblocking(connfd);
                 char  *str = inet_ntoa(clientaddr.sin_addr);
                printf(  "accapt a connection from %s\n"  ,str);
                 // 设置用于读操作的文件描述符
                ev.data.fd=connfd;
                 // 设置用于注册的读操作事件
#if  ET
                ev.events=EPOLLIN|EPOLLET;
#else
                ev.events=EPOLLIN;
#endif
                 // 注册 ev
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
             else if (events[i].events&EPOLLIN)
            {
            
                printf(  "EPOLLIN\n" );
                 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(  "error\n" );
                }  else  if  (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                line[n] =  '\0' ;
                printf(  "read %s\n" ,line);
                 // 设置用于写操作的文件描述符
                ev.data.fd=sockfd;
                 // 设置用于注册的写操作事件
#if  ET
               ev.events=EPOLLOUT|EPOLLET;
#else           
               ev.events=EPOLLOUT;
#endif     
            // 修改 sockfd  上要处理的事件为  EPOLLOUT
                 //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);/*注意,如果将这一行的注释去掉,那么程序就会将要处理的事件修改为读,那么就会进入002进行处理*/
            }
             else  if  (events[i].events&EPOLLOUT)
            {
                printf(  "002\n" );
  
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                 // 设置用于读操作的文件描述符
                ev.data.fd=sockfd;
                 // 设置用于注测的读操作事件
#if  ET     
               ev.events=EPOLLIN|EPOLLET;
#else               
              ev.events=EPOLLIN;               
#endif
// 修改 sockfd  上要处理的事件为  EPOLIN
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
    }
     return  0;
}
  //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);/*注意,如果将这一行的注释去掉,那么程序就会将要处理的事件修改为读,那么就会进入002进行处理*/其结果如下所示

/*
** 这是客户端端程序文件名: epoll_io_client.perl
** perl参考 http://www.phpchina.com/resource/manual/perl/perl5-1.htm
** 
执行方法: $chmod +x  epoll_io_client.perl 就可以执行了:$./ epoll_io_client.perl
注:你的程序的第一行必须为#!/usr/local/bin/perl(perl所在位置)
*/
#!/usr/bin/perl
use IO::Socket;

my $host = "127.0.0.1";
my $port = 5000;

my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";
my $msg_out = "1234567890";
print $socket $msg_out;
print "now send over, go to sleep\n";
#sleep(5);
print "5 second gone send another line\n";
#print $socket $msg_out;

while (1)
{
    sleep(1);
}

5.小结
单进程的用法
int epfd = epoll_create(int size);
       函数作用:用来创建Epoll事件的句柄。官方解释:它其实是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。
       我对于该函数的作用的理解:就是创建一个用于Epoll事件的句柄,类似文件句柄
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 
       函数作用:用来添加或者修改事件。
       这里面重要的参数是struct epoll_event * event 和int fd。int fd 就是Epoll要处理的fd。 struct epoll_event * event则是设置了Epoll对fd的处理方式。
int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout) 
      函数作用: 该函数用于轮询I/O事件的发生。对于epoll_ctl中设置的I/O fd进行处理。
     参数说明:epfd:已经经过epoll_create创建和epoll_ctl设置。这里面存放着要处理的fd。
                      struct epoll_event * events:这个数组用来存放发生的fd和事件类型。其中的取值都是由epoll_ctl中注册的已经发生的fd和其对应的事件类型。
                      maxevents:每次能处理的事件数
      注意:每次执行完epoll_wait函数都会将int epfd中注册的int fd清除,因此还需要重新设置int fd 和   struct epoll_event *event,并且使用epoll_ctl重新添加或者修改事件。

单个epoll并不能解决所有问题,特别是你的每个操作都比较费时的时候,因为epoll是串行处理的。 所以你还是有必要建立线程池来发挥更大的效能。

线程池的用法:
          本人对线程池不懂,但是可以从这篇文章中看到epoll的用法:一个简短的epoll服务器示例, 监听5000个端口, 使用线程池

从本例子中可以看到:
1)int epfd中不仅仅可以添加一个fd,可以添加多个fd,也就是epoll可以用来处理多个fd。
2)整个流程为 socket-----> bind -----> listen ----->创建epfd = epoll_create() ------------> 将socket产生的fd放入epfd中 epoll_add(epfd,EPOLL_CTL_ADD,fd,&ev)------------------------------->for(;;){  epoll_wait(epfd,events,20,time_value); .....}



6.参考文章
https://www.chenyajun.com/2008/11/14/814 
http://hi.baidu.com/seraphsky/item/a4d7f3b4d9f3f541ba0e129a 
http://www.cppblog.com/converse/archive/2008/10/12/63836.html epoll为什么这么快
http://www.cppblog.com/converse/archive/2008/10/13/63928.aspx epoll相关资料整理
http://hi.baidu.com/jackbillow/item/de083d7cff28593f7044230d epoll的ET和LT模式
http://hi.baidu.com/mgqw864/item/c2464168f75f23166895e696 accept:invalid argument
http://hi.baidu.com/linglux/item/f0101993b391dedb1b49df3f 一个简短的epoll服务器示例, 监听5000个端口, 使用线程池
http://hi.baidu.com/ym012/item/449b0c260d168acaa4275a49 好文章

你可能感兴趣的:(APUE)