Unix网络编程补充:epoll

        $epoll$ 执行与 $select$ 和 $poll$ 类似的工作:监听多个描述符,当其中存在可用描述符时返回。$epoll$ 支持边缘触发和层级触发两种模式,并且在监听大量描述符的情况下有着很好的性能。
        $epoll$ 的核心是 $epoll$ 对象,这是一个内核数据结构,形式上,可以认为是两个集合:

1. epoll操作

#include <sys/epoll.h>

/* 成功时返回0,出错返回-1 */

int epoll_create(int size);

int epoll_create1(int flags);

        $epoll_-create$ 创建一个 $epoll$ 对象,返回该对象的描述符。从Linux 2.6.8开始,$size$ 参数不再有作用,会被忽略,但是必须大于 $0$ ,否则会设置 $EINVAL$ 错误。$epoll_-create1$ 与 $epoll_-create$ 类似,$flags$ 参数的值可以是 $EPOLL_-CLOEXEC$ ,当设置该标志时,该对象描述符会在执行 $execve$ 调用后自动关闭。

#inlcude <sys/epoll.h>

// 成功返回0,出错返回-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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_data_t data;
};

        $epoll_-ctl$ 从 $epoll$ 描述符中添加、修改或删除事件,$fd$ 指定待监听的描述符。其中 $op$ 可以是:

$op$ 说明
$EPOLL_-CTL_-ADD$ 添加一个事件
$EPOLL_-CTL_-MOD$ 修改一个事件
$EPOLL_-CTL_-DEL$ 删除一个事件,$events$ 参数会被忽略

        $epoll_-event$ 的 $data$ 成员指定该事件关联的对象,会被保存在内核中,并在可用时返回;$events$ 成员是多个标志位的或的值,用于注册进程感兴趣的事件,当其中的条件满足时会唤醒阻塞进程,同时也作为返回值接收事件状态。

$event$ 说明
$EPOLLIN$ 描述符可读
$EPOLLOUT$ 描述符可写
$EPOLLRDHUP$ 流套接字对端关闭连接或者进入写半关闭状态
$EPOLLPRI$ TCP套接字带外数据
$EPOLLERR$ 描述符发生错误,不需要作为入参指定也能触发
$EPOLLHUP$ 描述符挂起,一般是描述符未打开或异常关闭导致的,不需要作为入参指定也能触发
$EPOLLET$ 边缘触发模式 ( 默认为层级触发模式 ),仅作为入参,不会被返回
$EPOLLONESHOT$ 单次通知,完成后该描述符会被移除出监听集合
$EPOLLWAKEUP$ 在非 $EPOLLET$ 和 $EPOLLONESHOT$ 的事件上,并且进程拥有 $CAP_-BLOCK_-SUSPEND$ 的能力,可以确保系统不会在等待该事件或者处理该事件时进入休眠状态。仅作为入参,不会被返回

        当 $epfd$ 或 $fd$ 不可用时,返回 $EBADF$ 错误。当 $fd$ 已存在时,返回 $EEXIST$ 错误。

#include <sys/epoll.h>

/* 成功时返回可用描述符数,出错时返回-1 */

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

int epoll_pwait2(int epfd, struct epoll_event *events,
  int maxevents, const struct timespec *timeout, const sigset_t *sigmask);

        $epoll_-wait$ 等待 $epoll$ 对象上的任意一个或多个描述符变为可用。$events$ 数组用于返回可用描述符,最多返回 $maxevents$ ( 必须大于 $0$ ) 个描述符。$timeout$ 参数指定超时事件,单位为毫秒,如果为 $-1$ ,意味着没有超时时间。文件描述符的状态会作为 $epoll_-event$ 中的 $events$ 成员返回。$epoll_-pwait$ 和 $epoll_-pwait2$ 与 $epoll_-wait$ 类似,其中 $sigmask$ 参数可以指定在等待描述符期间阻塞的信号,类似于以下操作:

sigset_t origmask;

pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = epoll(wait, epfd, &events, maxevents, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);

        如果 $epfd$ 不可用,返回 $EBADF$ 错误;如果 $epfd$ 不是一个 $epoll$ 描述符或者 $maxevents$ 不大于 $0$ ,返回 $EINVAL$ 错误;如果 $events$ 指向不可用区域,返回 $EFAULT$ 错误;如果阻塞过程中被中断,返回 $EINTR$ 错误。

2. 边缘触发和层级触发

        $epoll$ 允许边缘触发 ( $edge-triggered$ ,$ET$ ) 和层级触发 ( $level-triggered$ ,$LT$ ) 两种行为。假设存在以下情形:

  1. 一个管道的读描述符 $rfd$ 被注册进 $epoll$ 对象;
  2. 另一个进程往这个管道写入 $2KB$ 数据;
  3. $epoll_-wait$ 调用返回,当前进程从管道中读入 $1KB$ 数据;
  4. 当前进程再次调用 $epoll_-wait$ 。

        对于这种情况,$epoll$ 存在两种行为:

        在上述例子中,假设使用边缘触发并且对端进程在等待先前数据的响应,那么由于 $epoll_-wait$ 的调用,对端可能会无限等待下去。所以对于这种情况,建议使用非阻塞式I/O,或者多次调用 $read$ 或 $write$ 直到返回 $EAGAIN$ 错误。

#define MAX_EVENTS 10

struct epoll_event, ev, events[MAX_EVENTS];
int listenfd, connfd, nfds, epollfd, val;

/* Code to set up listening socket, 'listenfd',
   (socket(), bind(), listen()) omitted. */

epollfd = epoll_create1(0);
if (epollfd = -1) {
  perror("epoll_create1");
  exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listenfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {
  perror("epoll_ctl: listenfd");
  exit(EXIT_FAILURE);
}

for (;;) {
  nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
  if (nfds == -1) {
    perror("epoll_wait");
    exit(EXIT_FAILURE);
  }

  for (n = 0; n < nfds; n++) {
    if (events[n].data.fd == listenfd) {
      connfd = accept(listenfd, (struct sockaddr *) &addr, &addrlen);
      if (connfd == -1) {
        perror("accept");
        eixt(EXIT_FAILURE);
      }
      
      if ((val = fcntl(connfd, F_GETFL, 0)) == -1) {
        perror("fcntl: F_GETFL");
        exit(EXIT_FAILURE);
      }
      if (fcntl(connfd, F_SETFL, val | O_NONBLOCK) == -1) {
        perror("fcntl: F_SETFL");
        exit(EXIT_FAILURE);
      }

      ev.events = EPOLLIN | EPOLLET;
      ev.data.fd = connfd;
      if (epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) == -1) {
        perror("epoll_ctl: connfd");
        exit(EXIT_FAILURE);
      }
    } else {
      do_use_fd(events[n].data.fd);
    }
  }
}

Unix网络编程补充:epoll