118-epoll(函数)

前面了解了 epoll 的种种优点以及基本的使用框架后,相信你应该跃跃欲试了。不过再此之前,还需要将 epoll 的那个三函数细致的详解一下。

1. epoll 接口

epoll 提供了三个函数:

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.1 epoll_create

int epoll_create(int size);
  • 函数语义

用于创建一个 epoll 对象epoll instance),同时返回该对象的描述符。

  • 参数 size

表示你想监听几个描述符,或者说待会儿你想添加多少个描述符到 epoll 对象中. 从 Linux 2.6.8 内核开始,参数 size 已经没什么用了,但是使用的时候必须大于 0.

1.2 epoll_ctl

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 事件 */
  epoll_data_t data;        /* 用户数据 */
};
  • 函数语义

根据参数 op 决定向 epoll 对象中添加、修改还是删除描述符。

  • 参数 epfd

epoll 对象的描述符,由 epoll_create 函数返回。

  • 参数 op

它有三个可选值:

表1 参数 op 的可选值

含义
EPOLL_CTL_ADD将参数 fd 指定的描述符添加到 epoll 对象中,同时将其关联到一个 epoll 事件对象——即参数 event 所指定的值
EPOLL_CTL_MOD修改描述符 fd 所关联的事件对象 event,前提是该 fd 已经添加到了 epoll 对象中
EPOLL_CTL_DEL将描述符 fd 从 epoll 对象中移除,此时参数 event 被忽略,也可指定为 NULL

  • 参数 event

该参数的类型是 struct epoll_event 结构体指针,结构体定义在上而已经给出了。event 参数关联到参数 fd 上,表示想监听描述符 fd 上的哪种 IO 事件,比如可读事件,可写事件。有关 IO 事件的含义,在上一篇文章已经详细说明了。

这里我们主要关心的是结构体成员 events. 它有下面的值:

表2 events 的可选值

含义
EPOLLIN监听 fd 是否可读
EPOLLOUT监听 fd 是否可写
EPOLLRDHUPLinux 2.6.17 后可用。监听流式套接字对象是否关闭或半关闭
EPOLLPRI监听是否有紧急数据可读

上面这四个值都需要我们主动去监听才行,然后使用 epoll_wait 函数去等待你关心的这些描述符是否有事件发生。

除此之外,还有一个可选项 EPOLLET,这个不是要监听的事件类型,但是它却要通过 events 成员传递。它表示设置关联的描述符 IO 事件触发模式(大白话就是什么时候应该产生 IO 事件)。

EPOLLET 表示触发模式为边沿触发(Edge Triggered)。如果不指定触发模式,默认情况下为水平触发(Level Triggered)。目前,我们不设置 EPOLLET 选项,让其默认为水平触发。有关触发模式后面会详细讲,这里大家不用关心。

1.3 epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 函数语义

监听所有描述符上是否有事件发生。这些描述符之前都由 epoll_ctl 添加到了由 epfd 参数所引用的 epoll 对象中。

如果所有描述符上都没有 IO 事件发生,该函数会阻塞,直到有事件到来。一旦有事件到来,epoll_wait 函数就返回。同时将所有发生的事件保存到数组中,该数组的地址以及数组大小由你自己通过参数 events 和 maxevents 指定。同时 epoll_wait 会返回发生事件的个数。

  • 参数 events

events 数组的结构体类型前面已经详细介绍过了,这里还有补充。

epoll_wait 函数中的 events 是一个输出参数,充当了函数的返回值。如果 epoll_wait 返回了,会把所有发生的事件保存在数组 events 中,如果发生事件的个数比 events 数组的大小还要多……这个没问题,因为参数 maxevents 已经告诉内核,我数组只有这么大,其它的放不下的就下次再给我吧。

返回的 events 数组中,每个元素都表示一个事件,假设 epoll_wait 返回值是 3,就表示有 3 个事件发生了,那么你就挨个处理 events[0]、events[1] 和 events[2] 就行了。

如何知道 events[i] 是哪个描述符发生的事件?注意该结构体还有一个成员,是用户数据,即 data 成员,一般来说你在添加描述符的时候,需要将 data 设置成描述符 fd 的值,当 events 返回的时候,epoll 会帮你把一开始由 epoll_ctl 函数传给它的那个 data 放到这里。

当 epoll 返回时,events[i].events 中就保存了该描述符发生了哪些事件,除了表 2 中的值以外,还有一些异常事件,即使你不主动监听,如果发生了也会主动通知你:

含义
EPOLLERR描述符有错误,这种非常少见,比如硬件上的问题
EPOLLHUP关联的描述符有一端挂断,比如管道一端关闭

  • 参数 timeout

超时参数。

timeout = -1,永远等待。
timeout = 0,立即返回。
timeout > 0,最长等待 timeout 毫秒。

这个参数和 poll 是一样的。

  • 返回值

返回值 > 0,表示有几个事件发生。
返回值 = 0,表示超时时间到了。
返回值 < 0,则出错,同时设置 errno 的值。

2. 实验

程序 epoll.c 仍然使用 select 和 poll 中的那个案例,这里只是改成了 epoll 的方式。

2.1 代码

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

#define PERR(msg) do { perror(msg); exit(1); } while(0);

// 信号处理函数,验证 epoll_wait 会被信号打断
void handler(int sig) {
  if (sig == SIGINT) {
    puts("hello SIGINT");
  }
}

// 处理描述符上发生的事件
int process(char* prompt, int fd) {
  int n;
  char buf[64];
  char line[64];
  n = read(fd, buf, 63);
  if (n < 0) {
    // error
    PERR("read");
  }
  else if (n == 0) {
    // peer close
    sprintf(line, "%s closed\n", prompt);
    puts(line);
    return 0;
  }
  else if (n > 0) {
    buf[n] = 0;
    sprintf(line, "%s say: %s", prompt, buf);
    puts(line);
  }
  return n;
}

int main () {
  int i, n, res;
  char buf[64];
  int fds[3];
  int fd;

  if (SIG_ERR == signal(SIGINT, handler)) {
    PERR("signal");
  }

  fds[0] = STDIN_FILENO;
  fds[1] = open("a.fifo", O_RDONLY);
  printf("open pipe: fd = %d\n", fds[1]);
  fds[2] = open("b.fifo", O_RDONLY);
  printf("open pipe: fd = %d\n", fds[2]);


  // 事件数组 evts 用来保存 epoll_wait 返回的事件
  struct epoll_event evts[4];

  // 创建一个 epoll 实例对象
  int epfd = epoll_create(4);

  // 添加你所关心的描述符到 epoll 实例对象中
  for (i = 0; i < 3; ++i) {
    struct epoll_event ev;
    ev.data.fd = fds[i]; // 注意这个值必须要指定,不然 epoll_wait 返回了你也不知道是谁发生了事件
    ev.events = EPOLLIN; // 想监听可读事件,因为没有指定 EPOLLET 选项,所以默认是水平触发
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fds[i], &ev) < 0) {
      PERR("epoll_ctl");
    }
  }


  while(1) {
    // 开始等待事件发生,res 表示发生了事件的个数
    res = epoll_wait(epfd, evts, 4, -1);
    printf("res = %d\n", res);

    if (res < 0) {
      // error
      if (errno == EINTR) {
        perror("epoll_wait");
        continue;
      }
      PERR("epoll_wait");
    }
    else if (res == 0) {
      // timeout
      continue;
    }

    // 开始处理所有事件
    for (i = 0; i < res; ++i) {
      // 这个 fd 就是你一开始通过 event 的 data 成员传进去的。
      fd = evts[i].data.fd;
      if (evts[i].events & EPOLLIN) {
        sprintf(buf, "fd%d", fd);
        process(buf, fd);
      }
      // 这里我们根据没有监听可写事件,所以这种情况不会发生。
      if (evts[i].events & EPOLLOUT) {
        printf("fd%d can write\n", i);
      }
      // 下面这两个事件就算你没有监听,也可能会产生,需要单独处理
      if (evts[i].events & EPOLLERR) {
        printf("fd%d Error\n", i);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
      }
      if (evts[i].events & EPOLLHUP) {
        printf("fd%d Hang up\n", i);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
      }
    }
  }
}

2.2 编译和运行

  • 编译
gcc epoll.c -o epoll
  • 运行


这里写图片描述
图1 运行结果

3. 总结

  • 掌握 epoll 的三个函数
  • 知道 epoll 的两种模式

练习:当然是写代码了。。。

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值