欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

epoll使用详解

程序员文章站 2022-03-24 12:44:47
[TOC] epoll介绍 epoll的行为与poll(2)相似,监视多个有IO事件的文件描述符。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epol ......

目录


epoll介绍

epoll的行为与poll(2)相似,监视多个有io事件的文件描述符。epoll除了提供select/poll那种io事件的水平触发(level triggered)外,还提供了边缘触发(edge triggered),这就使得用户空间程序有可能缓存io状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

epoll_create(2) 创建一个新的epoll实例,并返回一个引用该实例的文件描述符
epoll_ctl(2) 创建epoll实例后,注册对感兴趣的文件描述符。当前注册在epoll实例上的文件描述符集被称为epoll集合。
epoll_wait(2) 等待i/o事件,如果当前没有事件可用,则阻塞调用线程。

水平触发边沿触发
epoll事件分布接口既可以表现为边缘触发(et),也可以表现为水平触发(lt)。这两种机制的区别
可以这样描述。假设有这种情况发生:

  1. 表示管道(rfd)的读侧的文件描述符在epoll实例上注册。
  2. 管道写入器在管道的写入端写入2 kb的数据。
  3. 调用epoll_wait(2)将返回rfd作为就绪文件描述符。
  4. 管道读取器从rfd读取1kb的数据。
  5. epoll_wait(2)调用完成。

如果使用边缘触发标志将rfd文件描述符注册到epoll接口,那么第五步的epoll_wait(2)的调用可能会挂起,尽管文件输入缓冲区仍然有1kb数据可读;同时,远程对等端可能正在期望基于它已发送的数据的应答。这样做的原因是,只有在被监视文件描述符上发生更改时,边缘触发模式才交付事件。因此,在步骤5中,调用者可能会以等待那些仍在输入缓冲区中的数据的状态下结束。
在上面的例子中,将生成rfd上的一个事件,因为在2中完成了写入,而在3中使用了该事件。由于在4中完成的读操作不会消耗整个缓冲区数据,所以在步骤5中完成的对epoll_wait(2)的调用可能会无限期阻塞。

使用epollet标志的应用程序应该使用非阻塞文件描述符,以避免在处理多个文件描述符时出现有阻塞的读写饥饿任务。建议使用epoll作为边沿触发(epollet)接口的方式如下:
i、 具有非阻塞文件描述符
ii、只有在read(2)或write(2)返回eagain后才等待事件。
相反,当epollet作为水平触发接口使用时(默认情况下,没有指定epollet), epoll只是一个更快的poll(2),并且可以在使用后者的任何地方使用,因为它具有相同的语义。

epoll的优点:

1、支持一个进程打开大数目的socket描述符(fd)

select能打开的文件描述符有一定的限制,fd_setsize设置,默认值是2048,有两种解决方法,1、修改它的值,然后重新编译内核。2、使用多进程加入要并发20w个客户,那么就要开100进程;epoll则没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1gb内存的机器上大约是2万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2、io效率不随fd数目增加而线性下降

select/poll采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;内核 / 用户空间内存拷贝问题,select/poll需要复制大量的句柄数据结构,产生巨大的开销;select/poll返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。

3、支持边缘触发模式

select/poll的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行io操作,那么之后每次select/poll调用还是会将这些文件描述符通知进程。

4、使用mmap加速内核与用户空间的消息传递。

select/poll和epoll都需要内核把fd消息通知给用户空间,如何避免不必要的内存拷贝很重要,在这点上,select/poll需要复制整个fd数组,产生巨大的开销;而epoll是通过内核于用户空间mmap同一块内存实现的。

epoll的系统调用

epoll_create

int epoll_create(int size);
int epoll_create1(int flags);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的,更推荐使用epoll_crete1(0)来替代,flags可以设置epoll_cloexec标志

epoll_ctl

#include <sys/epoll.h>

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

该系统调用对文件描述符epfd引用的epoll(7)实例执行控制操作。它请求对目标文件描述符fd执行操作op。

epfd : epoll_create创建的文件描述符.

op :参数的有效参数为:
epoll_ctl_add
在文件描述符epfd引用的epoll实例上注册目标文件描述符fd。
epoll_ctl_mod
修改已注册描述符fd关联的事件。
epoll_ctl_del
从epfd引用的epoll实例中删除(取消注册)目标文件描述符fd。该事件将被忽略,并且可以是null

fd :待监听的fd

epoll_event : 描述链接到文件描述符fd的对象,它的定义如下

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 */
           };

events成员是由以下可用事件类型的零个或多个组合在一起组成的位掩码:

epollin :关联的文件描述符可以读(包括对端socket正常关闭);

epollout:关联的文件描述符可以写;

epollpri:关联的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

epollerr:关联的文件描述符发生错误;

epollhup:关联的文件描述符被挂断;

epollrdhup:流套接字对等关闭连接,或半关闭写。(当使用边缘触发监视时,此标记对于编写简单代码检测对等端是否关闭特别有用。2.6.17引入)

epollet: 将epoll设为边缘触发(edge triggered)模式,这是相对于水平触发(level triggered)来说的。

epolloneshot:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个fd的话,需要再次把这个fd加入到epoll队列里

它们在内核头文件里的定义如下:

 33 
 34 enum epoll_events
 35   {
 36     epollin = 0x001,
 37 #define epollin epollin
 38     epollpri = 0x002,
 39 #define epollpri epollpri
 40     epollout = 0x004,
 41 #define epollout epollout
 42     epollrdnorm = 0x040,
 43 #define epollrdnorm epollrdnorm
 44     epollrdband = 0x080,
 45 #define epollrdband epollrdband
 46     epollwrnorm = 0x100,
 47 #define epollwrnorm epollwrnorm
 48     epollwrband = 0x200,
 49 #define epollwrband epollwrband
 50     epollmsg = 0x400,
 51 #define epollmsg epollmsg
 52     epollerr = 0x008,
 53 #define epollerr epollerr
 54     epollhup = 0x010,
 55 #define epollhup epollhup
 56     epollrdhup = 0x2000,
 57 #define epollrdhup epollrdhup
 58     epollexclusive = 1u << 28,
 59 #define epollexclusive epollexclusive
 60     epollwakeup = 1u << 29,
 61 #define epollwakeup epollwakeup
 62     epolloneshot = 1u << 30,
 63 #define epolloneshot epolloneshot
 64     epollet = 1u << 31
 65 #define epollet epollet
 66   };
 67 
 68 
 69 /* valid opcodes ( "op" parameter ) to issue to epoll_ctl().  */
 70 #define epoll_ctl_add 1 /* add a file descriptor to the interface.  */
 71 #define epoll_ctl_del 2 /* remove a file descriptor from the interface.  */
 72 #define epoll_ctl_mod 3 /* change file descriptor epoll_event structure.  */

epoll_wait

       #include <sys/epoll.h>

       int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
       int epoll_pwait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout,
                      const sigset_t *sigmask);

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

  • 一个文件描述符有事件发生;
  • 信号处理器中断;
  • 超时;

epoll示例程序

此程序简单测试一下三个api,注册标准输出的描述符到epoll,监视标准输出的读事件,触发后回显一遍,quit退出程序.

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/epoll.h>
#include <vector>

typedef std::vector<struct epoll_event> pollfdlist;

int main(int argc ,char **argv)
{

  int fd;
  char buf[1024];
  int i,res,real_read, maxfd;

  if((fd=open("/dev/stdin",o_rdonly|o_nonblock)) < 0)
  {
    fprintf(stderr,"open data1 error:%s",strerror(errno));
    return 1;
  }

  pollfdlist m_pollfds;
  int epfd = epoll_create1(epoll_cloexec);

  struct epoll_event ev;
  ev.events = epollin | epollpri;
  ev.data.fd = fd;

  epoll_ctl(epfd, epoll_ctl_add, fd, &ev);
  
  m_pollfds.resize(1024);

  while(1)
  {
    int ret = epoll_wait(epfd, m_pollfds.data(), m_pollfds.size(), 5000);
    if (ret < 0)
    {
      printf("epoll error : %s\n",strerror(errno));
      return 1;
    }

    if(ret == 0){
      printf("epoll timeout\n");
      continue;
    }

    for (i = 0; i< 1; i++)
    {
      if (m_pollfds[i].events & epollin)
      {
        memset(buf, 0, 1024);
        real_read = read(m_pollfds[i].data.fd, buf, 1024);
        if (real_read < 0)
        {
          if (errno != eagain)
          {
            printf("read eror : %s\n",strerror(errno));
            continue;
          }
        }
        else if (!real_read)
        {
          close(m_pollfds[i].data.fd);
          m_pollfds[i].events = 0;
        }
        else
        {
          if (i == 0)
          {
            buf[real_read] = '\0';
            printf("%s", buf);
            if ((buf[0] == 'q') || (buf[0] == 'q'))
            {
              printf("quit\n");
              return 1;
            }
          }
          else
          {
            buf[real_read] = '\0';
            printf("%s", buf);
          }
        }
      }
    }
  }

  exit(0);
}
./test
hello
hello
hello epoll
hello epoll
epoll timeout
quit
quit
quit