网络编程学习18--I/O多路复用(epoll)

1,589 阅读6分钟

和之前学过的两种多路复用方法 select、poll相比,epoll的性能最好。

epoll的用法

epoll通过监听注册的多个文件描述符,来进行I/O事件的分发处理。和poll不同的是,epoll不仅提供了默认的 level-triggered(条件触发)机制,还提供了性能更好的 edge-triggered (边缘触发)机制。

epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。(epoll实例就是这个事件表)

为了使用epoll进行网络编程,通常需要以下几步:1)epoll_create,2)epoll_ctl,3)epoll_wait

epoll_create

函数原型

 int epoll_create(int size);
 int epoll_create1(int flags);

通过epoll_create()函数,可以创建一个epoll实例,从Linux2.6.8开始,参数size被自动忽略,但是该值仍然需要一个大于0的整数。size参数只是用来给内核一个提示,告诉它事件表需要多大。

这个epoll实例被用来调用 epoll_ctl 和 epoll_wait,当这个epoll实例不再需要时,比如服务器正常关机,那么需要调用close()函数释放epoll实例,让系统内核回收epoll实例所分配使用的内核资源。

关于这个参数 size,在一开始的 epoll_create 实现中,是用来告知内核期望监控的文件描述字大小,然后内核使用这部分的信息来初始化内核数据结构,在新的实现中,这个参数不再被需要,因为内核可以动态分配需要的内核数据结构。我们只需要注意,每次将 size 设置成一个大于 0 的整数就可以了

epoll_create1()的用法和epoll()基本一致,当输入的flags为0时,则和epoll_create()一样,内核自动忽略。

返回值

若成功,则返回一个大于0的值,表示epoll实例;失败则返回-1。

epoll_ctl

函数原型

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

创建完epoll实例后,通过调用epoll_ctl 往这个epoll实例增加或删除监控的事件。

参数

1)epfd参数是刚刚调用epoll_create()创建的epoll实例描述字,即epoll句柄。

2)op参数表示增加还是删除一个监控事件,它有三个选项:

  • EPOLL_CTL_ADD:向epoll实例注册文件描述符对应的事件
  • EPOLL_CTL_DEL:向epoll实例删除文件描述符对应的事件
  • EPOLL_CTL_MOD:修改文件描述符对应的事件

3)fd参数表示注册事件的文件描述符

4)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事件 */
      epoll_data_t data;        /* User data variable 用户数据 */
  };

而epoll事件的类型和poll类型,都是基于 mask 的事件类型,事件类型有以下几种:

  • EPOLLIN:表示对应的文件描述字可以读;
  • EPOLLOUT:表示对应的文件描述字可以写;
  • EPOLLRDHUP:表示套接字的一端已经关闭,或者半关闭;
  • EPOLLHUP:表示对应的文件描述字被挂起;
  • EPOLLET:设置为 edge-triggered,默认为 level-triggered。

返回值

成功返回0,失败返回-1,并设置errno。

epoll_wait

函数原型

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

epoll_wait()函数和select、poll函数一样,调用者的进程被挂起,等待内核I/O事件的分发。

参数

1)epfd参数代表epoll实例描述字

2)events参数返回给用户空间需要处理的I/O事件,该参数是一个数组,数组的大小由epoll_wait的返回值决定,返回的数组中每个元素都是一个需要处理的I/O事件。(该数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件)

events表示具体的事件类型的取值和epoll_ctl中可设置的值一样。

由于epoll_wait只返回检测到的就绪事件,所以可以极大地提高应用程序索引就绪文件描述符地效率。(select和poll都需要遍历所有已经注册的文件描述符并找到其中的就绪者

3)maxevents参数是一个大于0的整数,表示epoll_wait可以返回的最大值。

4)timeout参数是epoll_wait阻塞调用的超时值,如果这个值为-1,表示一直阻塞,直到有事件发生;如果设置为0,则立即返回,即使没有任何I/O事件发生。

返回值

成功返回一个大于0的数,表示就绪的文件描述符个数;返回0,表示超时时间到;出错则返回-1。

LT(Level Trigger)和ET模式(Edge Trigger)

epoll对文件描述符的操作有两种模式:LT(条件触发)和ET(边缘触发)。LT是默认工作模式,此模式下,epoll相当于一个效率较高的poll。为了使用ET模式,应该往epoll内核事件表中注册一个文件描述符上的EPOLLET事件。

对于LT模式,当epoll_wait检测到其上有事件发生并将事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件直到该事件被处理

而对于ET模式,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件

所以,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

注意:使用ET模式的文件描述符应该是非阻塞的。如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态

实验

对select和poll分别进行实验,相关程序中,当接收到对端发来的数据后,只打印一条信息,但是并不从缓冲区中读出数据,以此来判断 select 和 poll 是采用的LT模式还是ET模式。

image-20220111171626645

对于select进行实验,可以发现,程序一直在循环打印信息。

image-20220111173902387

对于poll进行实验,可以发现,程序同样一直在循环打印信息。

综上,select和poll都是条件触发的,即LT模式