为何 epoll 如此受青睐

283 阅读3分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

从网络编程说起

刚学习网络编程的时候,相信很多人都用过下面这几个函数。

bind()
listen()
accept()
recv()

其中,recv() 是一个阻塞方法,当 socket 接收到数据后,recv() 返回接收到的数据继续向下执行。recv() 只能监视单个 socket,监视多个 socket 需要多个线程。为了同时监视多个 socket ,出现了 select 和 poll 模型。

select 和 poll

select 和 poll 类似,都准备了一个 fd 集合,这个集合里存放着所有需要监听的 socket ,调用 select() 或 poll() 时,如果 fd 集合没有 socket 就绪则阻塞,直到有一个 socket 就绪,唤醒 select() 或 poll() 进程。用户遍历 fd 集合,判断具体哪个 socket 收到数据后进行处理。

select 和 poll 的区别是两者的 fd 集合的数据结构,select 的 fd 集合是一个数组,有数量限制,限制1024或2048,poll 的 fd 集合是一个链表,没有数量限制。

select 和 poll 存在的问题是,如果有100万个连接同时与一个进程保持着 TCP 连接,而每一时刻只有几十个或几百个 TCP 是活跃的,每次收集事件时,都需要把这100万连接的 socket 传给操作系统,再由操作系统遍历 fd 集合找出哪个 socket 收到数据,这是巨大的资源浪费。因此,epoll 闪亮登场。

epoll 原理

epoll 在 Linux 内核中申请了一个简易的文件系统,把原先的一个 select 或 poll 调用分成了3部分。

  • 调用 epoll_create 建立一个 epoll 对象;
  • 调用 epoll_ctl 向 epoll 对象中添加这100万个连接的 socket;
  • 调用 epoll_wait 收集接收到数据的 socket。

创建 epoll 对象

当某一个进程调用 epoll_create 方法时,Linux 内核会创建一个 eventpoll 结构体,如下图所示,重点关注下这个结构体里的 rbr 和 rdllist 成员。rbr 成员是红黑树的根节点,这棵树存储着所有需要监听的 socket。rdllist 是双向链表的头结点,这个链表存储着收到数据的 socket。

image-20220125111728049.png

添加监听的 socket

当进程有某个需要监听的 socket 时,通过 epoll_ctl 方法将这个 socket 添加到 epoll 对象的 rbr 红黑树中。红黑树的添加操作效率很高,因此 epoll_ctl 效率很快。如果有一百万个连接的 socket 需要监听,红黑树会有一百万个结点。

接收数据

所有添加到 epoll 中的 socket 都会与设备(如网卡)驱动程序建立回调关系,当 socket 接收到数据时,会执行回调方法,回调方法会把这些接收到数据的待处理的 socket 放到 epoll 对象的 rdllist 链表中。

收集就绪 socket

当调用 epoll_wait 检查是否有 socket 接收到数据时,只是检查 eventpoll 对象中的 rdllist 双向链表,如果链表不为空,将这些 socket 复制到用户态内存。并不用向操作系统内核传递这100万个连接,内核也不需要去遍历全部的连接,因此效率很高。

总结

epoll 底层使用了红黑树和链表数据结构。红黑树存储了监听的 socket,向红黑树添加、删除结点都很快,从红黑树中查找 socket 也非常快。链表存储了收到数据的 socket,收集事件时,不用像 select 和 poll 一样把所有连接的 socket 传给操作系统,再由操作系统遍历查找。因此,epoll 是非常高效的,可以处理百万级别的并发连接。