IO多路复用-epoll

叙述

概念

epoll(eventpoll)是一种I/O事件通知机制,是linux内核实现IO多路复用的实现。

IO多路复用指在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用时返回,然后对其进行读写操作。

epoll时select和poll的升级版,改进了工作方式会更加高效。

通知机制

通知机制就是当事件发生时会主动通知,反面就是轮询机制。

通俗解释

epoll的通俗解释是一种当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制

对比select和poll

select/poll 低效的原因之一是将 “添加 / 维护待检测任务” 和 “阻塞进程 / 线程” 两个步骤合二为一。每次调用 select 都需要这两步操作,然而大多数应用场景中,需要监视的 socket 个数相对固定,并不需要每次都修改。epoll 将这两个操作分开,先用 epoll_ctl() 维护等待队列,再调用 epoll_wait() 阻塞进程(解耦)。通过下图的对比显而易见,epoll 的效率得到了提升。

核心函数

epoll_size()

1
2
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_size(int size);

内核会产生一个epoll 实例数据结构并返回一个文件描述符,这个特殊的描述符就是epoll实例的句柄,后面的两个接口都以它为中心(即epfd形参)。

  • 函数参数
    • size:在 Linux 内核 2.6.8 版本以后,这个参数是被忽略的,只需要指定一个大于 0 的数值就可以了。
  • 函数返回值:
    • 失败:返回 - 1
    • 成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的 epoll 实例了

epoll_ctl()

1
2
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

将被监听的描述符添加到红黑树或从红黑树中删除或者对监听事件进行修改。

1
2
3
4
5
6
7
8
9
10
11
typedef union epoll_data {
void *ptr; /* 指向用户自定义数据 */
int fd; /* 注册的文件描述符 */
uint32_t u32; /* 32-bit integer */
uint64_t u64; /* 64-bit integer */
} epoll_data_t;

struct epoll_event {
uint32_t events; /* 描述epoll事件 */
epoll_data_t data; /* 见上面的结构体 */
};
  • 函数参数:
    • epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
    • op:这是一个枚举值,控制通过该函数执行什么操作
      • EPOLL_CTL_ADD:往 epoll 模型中添加新的节点
      • EPOLL_CTL_MOD:修改 epoll 模型中已经存在的节点
      • EPOLL_CTL_DEL:删除 epoll 模型中的指定的节点
    • fd:文件描述符,即要添加 / 修改 / 删除的文件描述符
    • event:epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
      • events:委托 epoll 检测的事件
        • EPOLLIN:读事件,接收数据,检测读缓冲区,如果有数据该文件描述符就绪
        • EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写该文件描述符就绪
        • EPOLLERR:异常事件
    • data:用户数据变量,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。
  • 函数返回值:
    • 失败:返回 -1
    • 成功:返回 0

epoll_wait()

1
2
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中。

  • 函数参数:
    • epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
    • events:传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
    • maxevents:修饰第二个参数,结构体数组的容量(元素个数)
    • timeout:如果检测的 epoll 实例中没有已就绪的文件描述符,该函数阻塞的时长,单位 ms 毫秒
      • 0:函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回
      • 大于 0:如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
      • -1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞
  • 函数返回值:
    • 成功:
      • 等于 0:函数是阻塞被强制解除了,没有检测到满足条件的文件描述符
      • 大于 0:检测到的已就绪的文件描述符的总个数
    • 失败:返回 - 1

工作模式

epoll监控多个文件描述符的I/O事件。epoll支持边缘触发(edge trigger,ET)或水平触发(level trigger,LT),通过epoll_wait等待I/O事件,如果当前没有可用的事件则阻塞调用线程。 select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式。

水平触发(LT)

水平模式可以简称为 LT 模式,LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核通知使用者哪些文件描述符已经就绪,之后就可以对这些已就绪的文件描述符进行 IO 操作了。如果我们不作任何操作,内核还是会继续通知使用者。

  • 读事件:如果文件描述符对应的读缓冲区还有数据,读事件就会被触发,epoll_wait () 解除阻塞
    • 当读事件被触发,epoll_wait () 解除阻塞,之后就可以接收数据了
    • 如果接收数据的 buf 很小,不能全部将缓冲区数据读出,那么读事件会继续被触发,直到数据被全部读出,如果接收数据的内存相对较大,读数据的效率也会相对较高(减少了读数据的次数)
    • 因为读数据是被动的,必须要通过读事件才能知道有数据到达了,因此对于读事件的检测是必须的
  • 写事件:如果文件描述符对应的写缓冲区可写,写事件就会被触发,epoll_wait () 解除阻塞
    • 当写事件被触发,epoll_wait () 解除阻塞,之后就可以将数据写入到写缓冲区了
    • 写事件的触发发生在写数据之前而不是之后,被写入到写缓冲区中的数据是由内核自动发送出去的
    • 如果写缓冲区没有被写满,写事件会一直被触发
    • 因为写数据是主动的,并且写缓冲区一般情况下都是可写的(缓冲区不满),因此对于写事件的检测不是必须的

边缘触发(ET)

边沿模式可以简称为 ET 模式,ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)。如果我们对这个文件描述符做 IO 操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。

  • 读事件:当读缓冲区有新的数据进入,读事件被触发一次,没有新数据不会触发该事件
    • 如果有新数据进入到读缓冲区,读事件被触发,epoll_wait () 解除阻塞
    • 读事件被触发,可以通过调用 read ()/recv () 函数将缓冲区数据读出
    • 如果数据没有被全部读走,并且没有新数据进入,读事件不会再次触发,只通知一次
    • 如果数据被全部读走或者只读走一部分,此时有新数据进入,读事件被触发,并且只通知一次
  • 写事件:当写缓冲区状态可写,写事件只会触发一次
    • 如果写缓冲区被检测到可写,写事件被触发,epoll_wait () 解除阻塞
    • 写事件被触发,就可以通过调用 write ()/send () 函数,将数据写入到写缓冲区中
    • 写缓冲区从不满到被写满,期间写事件只会被触发一次
    • 写缓冲区从满到不满,状态变为可写,写事件只会被触发一次

综上所述:epoll 的边沿模式下 epoll_wait () 检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。 ET模式的设置: 边沿模式不是默认的 epoll 模式,需要额外进行设置。epoll 设置边沿模式是非常简单的,epoll 管理的红黑树示例中每个节点都是 struct epoll_event 类型,只需要将 EPOLLET 添加到结构体的 events 成员中即可:

1
2
struct epoll_event ev;
ev.events = EPOLLIN EPOLLET;

链接

https://zhuanlan.zhihu.com/p/159135478 https://blog.csdn.net/baidu_41388533/article/details/110134366 https://subingwen.cn/linux/epoll/##2-%E6%93%8D%E4%BD%9C%E5%87%BD%E6%95%B0

本文作者:jujimeizuo
本文地址https://blog.jujimeizuo.cn/2023/01/27/epoll/
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0 协议。转载请注明出处!