epoll
可以说是编写高性能服务端程序必不可少的技术,在介绍 epoll
之前,我们先来了解一下 多路复用I/O
吧。
多路复用I/O
:是指内核负责监听多个 I/O 流,当任何一个 I/O 流处于就绪状态(可读或可写)时都会通知进程,以便可以处理该 I/O 流上的数据。如 图1 所示:
如 图1 所示,内核负责监听多个 I/O 流,当某些 I/O 流变为就绪状态,内核会把这些 I/O 流添加到就绪队列中,然后通知进程处理就绪队列中的 I/O 流。
与传统的阻塞型 I/O 相比,多路复用 I/O 的优点是可以同时监听多个 I/O 流,并且会把就绪的 I/O 流告知进程。
介绍完多路复用 I/O,接下来开始介绍我们的主角:epoll。
在 Linux 系统中,有多种多路复用 I/O 的实现,比如 select 和 poll 等。而 epoll 也是多路复用 I/O 一种实现,与 select 和 poll 相比,epoll 在性能上有较大的提升。
epoll 内部使用红黑树来保存所有监听的 socket,红黑树是一种平衡二叉树,添加和查找元素的时间复杂度为 O(log n),其结构如 图2 所示:
epoll 通过 socket 句柄来作为 key,把 socket 保存在红黑树中。如 图2 所示,每个节点中的数字代表着 socket 句柄。
把监听的 socket 保存在红黑树中的目的是,为了在修改监听 socket 的读写事件时,能够通过 socket 句柄快速找到对应的 socket 对象。
另外,epoll 还维护着一个就绪队列,当 epoll 监听的 socket 状态发生改变(变为可读或可写)时,就会把就绪的 socket 添加到就绪队列中。如 图3 所示:
当 socket 从网络中获取到数据后,会发生通知给 epoll,epoll 会将当前 socket 添加到就绪队列中,并且唤醒等待中的进程(也就是调用 epoll_wait
的进程)。
当 socket 状态发生变化时,会调用 ep_poll_callback
函数来通知 epoll,我们来看看这个函数的处理过程:
static int ep_poll_callback(wait_queue_t *wait, unsigned mode,
int sync, void *key)
{
...
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
...
// 1) 把 socket 添加到就绪队列中
list_add_tail(&epi->rdllink, &ep->rdllist);
is_linked:
// 2) 唤醒调用 epoll_wait() 而被阻塞的进程
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
...
return 1;
}
ep_poll_callback
函数的意图很清晰,主要完成两个工作:
epoll_wait
函数而被阻塞的进程。当进程被唤醒后,就会从就绪队列中,把就绪的 socket 复制到用户提供的数组中。如 图4 所示:
如 图4 所示,在调用 epoll_wait
时需要提供一个 events
数组来存储就绪的 socket。当 epoll_wait
返回后,用户就可以从events
数组中获取到就绪的 socket,并可对其进行读写操作。
本文主要通过图解的方式大概介绍了 epoll
的原理,但很多实现的细节只能通过阅读源码来了解。如果对 epoll
的实现有兴趣,可以参考《epoll 如何工作的》这篇文章。