首页 Linux 的 epoll
文章
取消

Linux 的 epoll

epoll 是一个 Linux 内核的高效时间处理机制,它可以用来监控多个文件描述符的状态变化。主要应用在需要处理大量并发套接字的情况下,例如 HTTP 服务器。在这种情况下,epoll 可以提供高效的事件通知机制,避免轮询和阻塞。

epoll 的两种工作模式

epoll 的工作模式有两种,分别是 LT(水平触发)和 ET(边缘触发)。

  • LT 模式是默认的工作模式,它支持阻塞和非阻塞套接字。在 LT 模式下,只要文件描述符有事件发生时, epoll_wait 会返回该文件描述符,如果没有处理完该事件,下次调用 epoll_wait 时仍然会返回该文件描述符。这样可以保证不会丢失任何事件,但也可能导致重复处理同一个事件。
  • ET 模式是高效的工作模式,它只支持非阻塞套接字。在 ET 模式下,只有文件描述符的状态发生改变时(比如从不可读变成可读),epoll_wait 只会返回一次该文件描述符,如果没有处理完该事件,在没有新的事件到来之前,再次调用epoll_wait 不会返回该文件描述符。这样可以避免重复处理同一个事件,但也可能导致漏掉某些事件。
  • 一般来说,ET 模式比 LT 模式更快速高效,但也更难编程和调试。

epoll 正确读写方式

epoll 的正确的读写方式是在 ET 模式下,循环读写直到遇到 EAGAIN 错误为止。这样做的原因是为了保证不会漏掉任何事件,因为 ET 模式下只有在状态发生变化时才会通知用户程序。如果只读写一次,可能会导致缓冲区中还有数据没有处理,或者缓冲区还有空间可以写入,但是用户程序不知道,从而降低效率和性能。

epoll 如何将 socket 设为非阻塞模式

epoll可以通过 fcntl 函数来将 socket 设为非阻塞模式,例如:

1
2
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

这样做的原因是为了避免在 ET 模式下,如果对端因为 TCP 窗口太小,send 函数刚好不能将数据全部发送出去,将会造成阻塞,进而导致整个服务“卡住”。 同理,在接收数据时,如果没有读取完所有的数据,下次调用 epoll_wait 不会返回该文件描述符,也会造成数据丢失或延迟。

当你使用 ET 模式,并且将 socket 设为阻塞模式,那么可能会出现一些问题。比如说,当你要发送数据时,如果对方的接收缓冲区已经满了,那么 send 函数就会阻塞住,直到对方有空间接收数据为止。这样的话,你就不能处理其他的文件描述符或事件了。同样地,当你要接收数据时,如果没有把所有的数据都读取完毕,并且还有剩余的数据在缓冲区里面,那么下次调用 epoll_wait 时,并不会返回这个文件描述符给你。因为它的状态并没有改变(还是可读)。这样的话,你就可能会丢失或延迟处理这些数据。

所以为了避免这些问题,在使用 ET 模式时,最好将 socket 设为非阻塞模式。这样当 send 或 recv 函数不能立刻完成时,它们就会返回一个错误码(比如 EAGAIN 或 EWOULDBLOCK ),告诉你现在不能发送或接收数据了。这样你就可以根据错误码来决定下一步该怎么做了。

epoll 在 ET 模式下,EPOLLOUT 和EPOLLIN 事件在什么情况下触发

  • EPOLLOUT 事件:只有在连接时触发一次,表示可写。其他时候想要触发,需要满足以下两个条件之一:
    • 某次 write,写满了发送缓冲区,返回错误码为 EAGAIN。
    • 对端读取了一些数据,又重新可写了。
  • EPOLLIN 事件:只有在可读状态发生变化时触发一次,表示可读。其他时候想要触发,需要满足以下两个条件之一:
    • 某次 read,读空了接收缓冲区,返回错误码为 EAGAIN。
    • 对端发送了一些数据,又重新可读了。

EPOLLOUT 和 EPOLLIN 事件都是边缘触发模式(ET)下的事件,它们只在状态改变时被触发一次。因此,在处理 EPOLLOUT 和 EPOLLIN 事件时,应该尽可能地处理完所有可读或可写的数据,否则可能会导致数据丢失或阻塞。

epoll 内部使用了哪些数据结构来实现高效的事件处理

epoll 内部使用了以下数据结构来实现高效的事件处理:

  • eventpoll:这是 epoll 的核心结构,它包含了一个红黑树和一个就绪链表。红黑树用于存储所有注册的 fd 和对应的事件,就绪链表用于存储发生了事件的 fd 和对应的事件。
  • epitem:这是红黑树中的节点,它包含了一个 fd 和对应的事件,以及一个指向 eventpoll 的指针。
  • epollevent:这是就绪链表中的节点,它包含了一个 epitem 的指针和一个指向下一个节点的指针。
  • file:这是 Linux 内核中表示打开文件的结构,它包含了一个操作集合(ops)和一个私有数据(private_data)。epoll 会将 eventpoll 结构存储在 file 的私有数据中,以便在用户态和内核态之间传递 epoll 实例。
  • wait_queue_head_t:这是 Linux 内核中表示等待队列头部的结构,它包含了一个自旋锁和一个等待队列。epoll 会将 wait_queue_head_t 结构存储在 eventpoll 结构中,以便在 epoll_wait 时将当前进程加入到等待队列中。
  • 红黑树和就绪链表的更新和遍历过程:
    • 更新:当用户调用 epoll_ctl 时,内核会根据操作类型(添加、删除、修改)在红黑树中插入或删除相应的 epitem 节点,并设置其事件类型。当内核检测到某个 fd 发生了事件时,会将对应的 epitem 节点插入到就绪链表的尾部,并唤醒等待队列中的进程。
    • 遍历:当用户调用 epoll_wait 时,内核会从就绪链表的头部开始遍历,将每个 epitem 节点的 fd 和事件复制到用户空间的 events 数组中,并从就绪链表中删除该节点。如果就绪链表为空,或者复制的节点数达到了用户指定的最大值,或者超时时间到了,内核会返回给用户实际复制的节点数。

使用红黑树的原因是为了提高查找和插入的效率:

  • 查找:当用户调用 epoll_wait 时,内核需要在红黑树中查找是否有 fd 发生了事件。如果使用链表或数组,查找的时间复杂度是 O(n),而使用红黑树,查找的时间复杂度是 O(log n)。
  • 插入:当用户调用 epoll_ctl 时,内核需要在红黑树中插入或删除一个 fd。如果使用链表或数组,插入或删除的时间复杂度是 O(n),而使用红黑树,插入或删除的时间复杂度是 O(log n)。

select、poll 和 epoll 区别

epoll 与 select 和 poll 的区别和联系主要有以下几点:

  • select、poll 和 epoll 都是实现 I/O 多路复用的技术,可以让一个线程同时监控多个文件描述符(fd)的状态变化,从而提高 I/O 效率。
  • select 使用一个 fd_set 结构来存储要监控的 fd,但是 fd_set 的大小是固定的,最多只能监控 1024 个 fd。poll使用一个 pollfd 结构数组来存储要监控的 fd,没有数量限制。epoll 使用一个红黑树来存储要监控的 fd,也没有数量限制。
  • select 和 poll 每次调用时都需要将用户态的 fd 拷贝到内核态,并且遍历所有的 fd 来检查是否有就绪事件,这样会造成不必要的开销。epoll 只需要在第一次调用 epoll_ctl 时将用户态的 fd 拷贝到内核态,并且使用回调函数来通知就绪事件,这样可以减少拷贝和遍历的开销。
  • select 和 poll 是水平触发(LT)模式,即当某个 fd 有就绪事件时,如果不处理该事件,下次调用时还会通知该事件。epoll 既支持水平触发模式,也支持边缘触发(ET)模式,即当某个 fd 有就绪事件时,只通知一次该事件,如果不处理该事件,下次调用时不会再通知该事件。

epoll 的 ET 模式的优点与缺陷

  • 优势:ET 模式可以减少不必要的事件通知和遍历,提高效率。ET 模式也可以更方便地处理 EPOLLOUT 事件,省去打开和关闭 EPOLLOUT 的 epoll_ctl 调用,从而有可能提升性能。
  • 缺陷:ET 模式需要使用非阻塞 IO,并且需要一次读写完所有数据,否则可能会遗漏事件。ET 模式也需要注意避免饥饿现象,即某些 fd 频繁触发事件而导致其他 fd 无法得到处理。ET 模式的编码也比较复杂,需要考虑多种情况和异常。

epoll 的 LT 模式的优点与缺陷

  • 优势:LT 模式可以使用阻塞 IO,并且不需要一次读写完所有数据,可以避免遗漏事件。LT 模式的编码也比较简单,不需要考虑多种情况和异常。
  • 缺陷:LT 模式会导致不必要的事件通知和遍历,降低效率。LT 模式也不方便处理 EPOLLOUT 事件,需要打开和关闭 EPOLLOUT 的 epoll_ctl 调用,从而可能影响性能。LT 模式在并发量高的时候,epoll_wait 返回的就绪队列比较大,遍历比较耗时。

epoll 的 ET 模式和 LT 模式的选择

  • 连接数:如果连接数很少,推荐使用 LT 模式,因为它的实现比较简单,而且对于少量的 fd,轮询的开销也不大。如果连接数很多,推荐使用 ET 模式,因为它可以有效地减少内核态和用户态之间的数据拷贝和通知次数。
  • 实时性:如果对实时性要求很高,推荐使用 ET 模式,因为它可以支持 ET 模式,只有 fd 的状态发生变化才会通知用户态,这样可以避免重复处理相同的事件。
  • 兼容性:如果要考虑兼容性问题,推荐使用 LT 模式,因为它是 POSIX 标准中定义的,并且在多种平台上都有支持。而 epoll 是 Linux 特有的,并且在不同版本的 Linux 内核中可能有不同的实现细节。 ​

写在最后

感谢你在茫茫人海中找到我🕵🏼

🎉你是第 个读者

㊗️ 你平安喜乐,顺遂无忧!

希望你读完有所收获~

🥂🥂🥂

本文由作者按照 CC BY 4.0 进行授权

C++11 的常用特性

Linux 的 I/O 多路复用机制