Epoll详解

Epoll详解

1. epoll简介

     epoll是为了处理大批量句柄而作了改进的poll。目前被认为是linux2.6下性能最好的多路I/O就绪通知方法。
     epoll的系统调用有:epoll_create, epoll_ctl, epoll_wait三个。 

     1. int epoll_create(int size);

创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好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:注册新的fdepfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd

第三个参数是需要监听的fd

第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

  1. //保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)  
  2.   
  3. typedef union epoll_data {  
  4.     void *ptr;  
  5.     int fd;  
  6.     __uint32_t u32;  
  7.     __uint64_t u64;  
  8. } epoll_data_t;  
  9.  //感兴趣的事件和被触发的事件  
  10. struct epoll_event {  
  11.     __uint32_t events; /* Epoll events */  
  12.     epoll_data_t data; /* User data variable */  
  13. };  

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

收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。

2. epoll触发方式

  1. epoll事件处理机制有两种触发方式:ET和LT
    
     ET:Edge Trigger ; LT:Level Trigger
     下面以图来说明:
    

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

3. epoll编程框架

那么究竟如何来使用epoll呢?其实非常简单。

通过在包含一个头文件#include <sys/epoll.h> 以及几个简单的API将可以大大的提高你的网络服务器的支持人数。

首先通过create_epoll(int maxfds)来创建一个epoll的句柄。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。

之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件返回,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。

epoll_wait返回之后应该是一个循环,遍历所有的事件。

 

几乎所有的epoll程序都使用下面的框架:

  1. epollfd = epoll_create(...)                     //创建epoll描述符  
  2. listenfd = socket(...)                          //创建用于端口监听的socket  
  3. bind(listenfd ...)                              //绑定  
  4. listen(listenfd ...)                            //开始在端口监听  
  5. epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd...)  //将listenfd加入epoll描述符监听  
  6. for(;;)                                         //无限循环  
  7. {  
  8.     n = epoll_wait(...)                         //等待epoll描述符的事件发生  
  9.     for(0 ~ n)                                  //可能有多个读写事件发生,遍历所有事件  
  10.     {  
  11.         if(events[i].data.fd == listenfd)       //通过发生事件的socket描述符确认有新链接,  
  12.         {                                       //而非已经打开的socket的读写事件  
  13.             connfd = accpet(...)                //accept新连接为connfd  
  14.             epoll_ctl(...)                      //connfd加入epoll监听,像上面listenfd一样  
  15.         }  
  16.         else if(events[i].events & EPOLLIN)     //发生事件的socket不是listenfd而是connfd  
  17.         {                                       //且事件集里有EPOLLIN表明有数据要读取  
  18.             n = read(...)                       //读取数据  
  19.             if(n == 0)                          //读到0字节,需要关闭socket  
  20.                 close(connfd)                   //close会自动将connfd从epoll监听中删除  
  21.         }                                       //无需调用epoll_ctl(..EPOLL_CTL_DEL..)  
  22.         else if(...)                            //其他各种事件处理依次处理  
  23.         ...    }                                              
  24. }   

4. epoll为什么这么快?

以一个生活中的例子来解释.

假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但是不知道你具体住在哪里,于是你们约好了在A号楼门口见面.

如果你使用的阻塞IO模型来处理这个问题,那么你就只能一直守候在A号楼门口等待朋友的到来,在这段时间里你不能做别的事情,不难知道,这种方式的效率是低下的.

现在时代变化了,开始使用多路复用IO模型来处理这个问题.你告诉你的朋友来了A号楼找楼管大妈,让她告诉你该怎么走.这里的楼管大妈扮演的就是多路复用IO的角色.

进一步解释select和epoll模型的差异.

select版大妈做的是如下的事情:比如同学甲的朋友来了,select版大妈比较笨,她带着朋友挨个房间进行查询谁是同学甲,你等的朋友来了,于是在实际的代码中,select版大妈做的是以下的事情:

int n = select(&readset,NULL,NULL,100);

for (int i = 0; n > 0; ++i)
{
   if (FD_ISSET(fdarray[i], &readset))
   {
      do_something(fdarray[i]);
      --n;
   }
}

epoll版大妈就比较先进了,她记下了同学甲的信息,比如说他的房间号,那么等同学甲的朋友到来时,只需要告诉该朋友同学甲在哪个房间即可,不用自己亲自带着人满大楼的找人了.于是epoll版大妈做的事情可以用如下的代码表示:
n=epoll_wait(epfd,events,20,500);
    
for(i=0;i<n;++i)
{
    do_something(events[n]);
}

在epoll中,关键的数据结构epoll_event定义如下:
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 */
        }; 
可以看到,epoll_data是一个union结构体,它就是epoll版大妈用于保存同学信息的结构体,它可以保存很多类型的信息:fd,指针,等等.有了这个结构体,epoll大妈可以不用吹灰之力就可以定位到同学甲.

别小看了这些效率的提高,在一个大规模并发的服务器中,轮询IO是最耗时间的操作之一.再回到那个例子中,如果每到来一个朋友楼管大妈都要全楼的查询同学,那么处理的效率必然就低下了,过不久楼底就有不少的人了.

对比最早给出的阻塞IO的处理模型, 可以看到采用了多路复用IO之后, 程序可以自由的进行自己除了IO操作之外的工作, 只有到IO状态发生变化的时候由多路复用IO进行通知, 然后再采取相应的操作, 而不用一直阻塞等待IO状态发生变化了.

从上面的分析也可以看出,epoll比select的提高实际上是一个用空间换时间思想的具体应用.

5. epoll测试例子程序

  1.  #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <unistd.h>  
  4. #include <errno.h>  
  5. #include <sys/socket.h>  
  6. #include <netdb.h>  
  7. #include <fcntl.h>  
  8. #include <sys/epoll.h>  
  9. #include <string.h>  
  10.   
  11. #define MAXEVENTS 64  
  12.   
  13. //函数:  
  14. //功能:创建和绑定一个TCP socket  
  15. //参数:端口  
  16. //返回值:创建的socket  
  17. static int  
  18. create_and_bind (char *port)  
  19. {  
  20.   struct addrinfo hints;  
  21.   struct addrinfo *result, *rp;  
  22.   int s, sfd;  
  23.   
  24.   memset (&hints, 0, sizeof (struct addrinfo));  
  25.   hints.ai_family = AF_UNSPEC;     /* Return IPv4 and IPv6 choices */  
  26.   hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */  
  27.   hints.ai_flags = AI_PASSIVE;     /* All interfaces */  
  28.   
  29.   s = getaddrinfo (NULL, port, &hints, &result);  
  30.   if (s != 0)  
  31.     {  
  32.       fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));  
  33.       return -1;  
  34.     }  
  35.   
  36.   for (rp = result; rp != NULL; rp = rp->ai_next)  
  37.     {  
  38.       sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);  
  39.       if (sfd == -1)  
  40.         continue;  
  41.   
  42.       s = bind (sfd, rp->ai_addr, rp->ai_addrlen);  
  43.       if (s == 0)  
  44.         {  
  45.           /* We managed to bind successfully! */  
  46.           break;  
  47.         }  
  48.   
  49.       close (sfd);  
  50.     }  
  51.   
  52.   if (rp == NULL)  
  53.     {  
  54.       fprintf (stderr, "Could not bind\n");  
  55.       return -1;  
  56.     }  
  57.   
  58.   freeaddrinfo (result);  
  59.   
  60.   return sfd;  
  61. }  
  62.   
  63.   
  64. //函数  
  65. //功能:设置socket为非阻塞的  
  66. static int  
  67. make_socket_non_blocking (int sfd)  
  68. {  
  69.   int flags, s;  
  70.   
  71.   //得到文件状态标志  
  72.   flags = fcntl (sfd, F_GETFL, 0);  
  73.   if (flags == -1)  
  74.     {  
  75.       perror ("fcntl");  
  76.       return -1;  
  77.     }  
  78.   
  79.   //设置文件状态标志  
  80.   flags |= O_NONBLOCK;  
  81.   s = fcntl (sfd, F_SETFL, flags);  
  82.   if (s == -1)  
  83.     {  
  84.       perror ("fcntl");  
  85.       return -1;  
  86.     }  
  87.   
  88.   return 0;  
  89. }  
  90.   
  91. //端口由参数argv[1]指定  
  92. int  
  93. main (int argc, char *argv[])  
  94. {  
  95.   int sfd, s;  
  96.   int efd;  
  97.   struct epoll_event event;  
  98.   struct epoll_event *events;  
  99.   
  100.   if (argc != 2)  
  101.     {  
  102.       fprintf (stderr, "Usage: %s [port]\n", argv[0]);  
  103.       exit (EXIT_FAILURE);  
  104.     }  
  105.   
  106.   sfd = create_and_bind (argv[1]);  
  107.   if (sfd == -1)  
  108.     abort ();  
  109.   
  110.   s = make_socket_non_blocking (sfd);  
  111.   if (s == -1)  
  112.     abort ();  
  113.   
  114.   s = listen (sfd, SOMAXCONN);  
  115.   if (s == -1)  
  116.     {  
  117.       perror ("listen");  
  118.       abort ();  
  119.     }  
  120.   
  121.   //除了参数size被忽略外,此函数和epoll_create完全相同  
  122.   efd = epoll_create1 (0);  
  123.   if (efd == -1)  
  124.     {  
  125.       perror ("epoll_create");  
  126.       abort ();  
  127.     }  
  128.   
  129.   event.data.fd = sfd;  
  130.   event.events = EPOLLIN | EPOLLET;//读入,边缘触发方式  
  131.   s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);  
  132.   if (s == -1)  
  133.     {  
  134.       perror ("epoll_ctl");  
  135.       abort ();  
  136.     }  
  137.   
  138.   /* Buffer where events are returned */  
  139.   events = calloc (MAXEVENTS, sizeof event);  
  140.   
  141.   /* The event loop */  
  142.   while (1)  
  143.     {  
  144.       int n, i;  
  145.   
  146.       n = epoll_wait (efd, events, MAXEVENTS, -1);  
  147.       for (i = 0; i < n; i++)  
  148.         {  
  149.           if ((events[i].events & EPOLLERR) ||  
  150.               (events[i].events & EPOLLHUP) ||  
  151.               (!(events[i].events & EPOLLIN)))  
  152.             {  
  153.               /* An error has occured on this fd, or the socket is not 
  154.                  ready for reading (why were we notified then?) */  
  155.               fprintf (stderr, "epoll error\n");  
  156.               close (events[i].data.fd);  
  157.               continue;  
  158.             }  
  159.   
  160.           else if (sfd == events[i].data.fd)  
  161.             {  
  162.               /* We have a notification on the listening socket, which 
  163.                  means one or more incoming connections. */  
  164.               while (1)  
  165.                 {  
  166.                   struct sockaddr in_addr;  
  167.                   socklen_t in_len;  
  168.                   int infd;  
  169.                   char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];  
  170.   
  171.                   in_len = sizeof in_addr;  
  172.                   infd = accept (sfd, &in_addr, &in_len);  
  173.                   if (infd == -1)  
  174.                     {  
  175.                       if ((errno == EAGAIN) ||  
  176.                           (errno == EWOULDBLOCK))  
  177.                         {  
  178.                           /* We have processed all incoming 
  179.                              connections. */  
  180.                           break;  
  181.                         }  
  182.                       else  
  183.                         {  
  184.                           perror ("accept");  
  185.                           break;  
  186.                         }  
  187.                     }  
  188.   
  189.                                   //将地址转化为主机名或者服务名  
  190.                   s = getnameinfo (&in_addr, in_len,  
  191.                                    hbuf, sizeof hbuf,  
  192.                                    sbuf, sizeof sbuf,  
  193.                                    NI_NUMERICHOST | NI_NUMERICSERV);//flag参数:以数字名返回  
  194.                                   //主机地址和服务地址  
  195.   
  196.                   if (s == 0)  
  197.                     {  
  198.                       printf("Accepted connection on descriptor %d "  
  199.                              "(host=%s, port=%s)\n", infd, hbuf, sbuf);  
  200.                     }  
  201.   
  202.                   /* Make the incoming socket non-blocking and add it to the 
  203.                      list of fds to monitor. */  
  204.                   s = make_socket_non_blocking (infd);  
  205.                   if (s == -1)  
  206.                     abort ();  
  207.   
  208.                   event.data.fd = infd;  
  209.                   event.events = EPOLLIN | EPOLLET;  
  210.                   s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);  
  211.                   if (s == -1)  
  212.                     {  
  213.                       perror ("epoll_ctl");  
  214.                       abort ();  
  215.                     }  
  216.                 }  
  217.               continue;  
  218.             }  
  219.           else  
  220.             {  
  221.               /* We have data on the fd waiting to be read. Read and 
  222.                  display it. We must read whatever data is available 
  223.                  completely, as we are running in edge-triggered mode 
  224.                  and won't get a notification again for the same 
  225.                  data. */  
  226.               int done = 0;  
  227.   
  228.               while (1)  
  229.                 {  
  230.                   ssize_t count;  
  231.                   char buf[512];  
  232.   
  233.                   count = read (events[i].data.fd, buf, sizeof(buf));  
  234.                   if (count == -1)  
  235.                     {  
  236.                       /* If errno == EAGAIN, that means we have read all 
  237.                          data. So go back to the main loop. */  
  238.                       if (errno != EAGAIN)  
  239.                         {  
  240.                           perror ("read");  
  241.                           done = 1;  
  242.                         }  
  243.                       break;  
  244.                     }  
  245.                   else if (count == 0)  
  246.                     {  
  247.                       /* End of file. The remote has closed the 
  248.                          connection. */  
  249.                       done = 1;  
  250.                       break;  
  251.                     }  
  252.   
  253.                   /* Write the buffer to standard output */  
  254.                   s = write (1, buf, count);  
  255.                   if (s == -1)  
  256.                     {  
  257.                       perror ("write");  
  258.                       abort ();  
  259.                     }  
  260.                 }  
  261.   
  262.               if (done)  
  263.                 {  
  264.                   printf ("Closed connection on descriptor %d\n",  
  265.                           events[i].data.fd);  
  266.   
  267.                   /* Closing the descriptor will make epoll remove it 
  268.                      from the set of descriptors which are monitored. */  
  269.                   close (events[i].data.fd);  
  270.                 }  
  271.             }  
  272.         }  
  273.     }  
  274.   
  275.   free (events);  
  276.   
  277.   close (sfd);  
  278.   
  279.   return EXIT_SUCCESS;  
  280. }  

运行方式:

在一个终端运行此程序:epoll.out PORT

另一个终端:telnet  127.0.0.1 PORT

运行结果:


参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值