写点什么

揭开 epoll 面纱:Nginx,Redis 等都在用的多路复用,到底是什么?

  • 2022-10-26
    上海
  • 本文字数:3604 字

    阅读完需:约 12 分钟

揭开epoll面纱:Nginx,Redis等都在用的多路复用,到底是什么?

最近在看一些文章,出现在眼前频率比较高的一个单词就是 epoll。这个在众多优秀软件中都出现的内容,我看大家或多或少有接触过,例如在 Nginx 中使用 epoll 处理百万并发连接,Redis 使用 epoll 来处理高并发的客户端网络请求,Java 的 NIO 底层也是基于 epoll 的等等。


在 Redis 的面试八股文中,有一个最频繁提及的问题就是:Redis 为什么这么快?对于这个问题,我相信对于八股文的职位玩家而言,可以说是张口就来:Redis 是基于内存的,Redis 内部采用了多线程+epoll 多路复用模型,有优秀的数据结构......等等。


可以看出在上面的答案中提及到了 epoll 多路复用,其实我相信对于大多数没有编写过 C/C++的同学而言,你只会背,不会用吧,更没有见过它到底长什么样子?心血来潮,今天简单写一篇文章介绍下 epoll 的相关知识点,不会面面俱到每一个函数是如何定义的,接口是如何使用的,只希望让你了解到 epoll 到底是如何工作的。


一、epoll 要解决什么问题?

每个技术点都有其要解决的问题与应用场景,epoll 也一样,其设计的目的就在于:处理大批量的文件描述符(基本为网络 Socket 连接),提升系统处理网络高并发的能力。


外,epoll 被称为"IO 多路复用",每个含义拆开来理解:

  • IO:epoll 的应用场景大多数在处理网络 IO 上。

  • 多路复用:百度百科的解释是"多路复用技术能把多个信号组合起来在一条物理信道上进行传输",在编程中就对应着"多路复用技术能把多个网络连接组合起来在一条 epoll 上进行处理"。


1.1-五大 IO 模型

对于 IO 的处理上,有五大 IO 模型:

  • 阻塞式 IO(blocking I/O)。

  • 非阻塞式 IO(non-blocking I/O)。

  • IO 复用:实现上有 select、poll、epoll,本文介绍 epoll。

  • 信号驱动式 IO。

  • 异步 IO。


可以看到 epoll 是属于 IO 复用模型的,对于这五大 IO 模型,大家经常用来在一起进行比较,我认为"比较"这个动词不太适合,只能说每一种 IO 模型都有自己的适用场景,下面我简单介绍下其中提到比较多的一些 IO 模型的"特点"。


另外,本文在提及 IO 时,没有特殊说明,指的都是网络 IO。


阻塞式 IO:

  • 对于阻塞式 IO,一般都是针对于单个网络而言的。

  • 在软件层面的表现:当线程(or 进程)尝试从网络连接中读取数据,但是无数据可读时,线程阻塞。

  • 在硬件层面的表现:线程产生阻塞之后,产生上下文切换,让出 CPU 给其他线程运行。


非阻塞式 IO:

  • 对于非阻塞式 IO,也是针对于单个网络而言的。

  • 在软件层面的表现:当线程(or 进程)尝试从网络连接中读取数据,但是无数据可读时,线程不阻塞,立即返回,可以处理其他逻辑。

  • 在硬件层面的表现:线程不产生阻塞,CPU 按照给出的时间片时间进行处理。



IO 复用:

  • 这个就是本文介绍的重点,其在 IO 处理方面,不拘束于一个 IO 连接,而是千级别/万级别的网络 IO 连接,这些连接都由 select/poll/epoll 来进行管理。

  • 在软件层面的表现:select/poll/epoll 对管理的千级别/万级别的网络连接进行监听,当任何一个 IO 有消息/事件产生时,对这批 IO 连接进行处理。如果没有任何一个网络连接有消息产生,select/poll/epoll 本身也会产生阻塞。

  • 在硬件层面的表现:根据软件层面的阻塞/非阻塞表现,硬件层面同理。


注:下图中是以 select 为例的,epoll 同理。


我相信到这里,可能还是有点蒙的,不妨接着看,后面伪代码部分看的时候可能就清晰些了。


1.2-epoll 解决了哪些问题?

回到标题,epoll 要解决什么问题?


我认为主要是下面两个方面。


1.2.1-软件层面:应对高并发连接

设想你编写的服务器,有千级别,万级别的客户端来连接,你如何管理这些 socket 连接哪?

  • 每个 socket 去遍历一遍,看看是否有数据到来?

  • 发数据的时候,找到特定的 socket,然后往里面发?

  • ......


可以看到,与 socket 的交互,最核心的场景就是网络 IO 的处理,而这些操作 epoll 都可以协助你去完成,你新建立一个网络 socket 的时候,交管给 epoll 就可以了,当网络 IO 有数据到来时它会通知你,当你要往网络 IO 写数据时,epoll 也会自动帮你发送。


1.2.2-硬件层面:提升 CPU 使用率

想一想你是否写过这样的程序,while 循环等待网络 IO 的到来?

socket = socket() // 创建socket连接
// 从socket上接收数据while(true){ msg = socket.recv() // 对msg进行处理}
复制代码


上面代码的缺点很明显:

  • 消耗 CPU 的时间。while 循环会间歇性的获取 CPU 时间片运行,并尝试从网络 socket 上获取数据。当然,此过程也伴随着大量的上下文切换。

  • socket 过多你该如何处理?总不能每个 socket 都开一个 while 循环在哪里等待吧?


epoll 是怎么样处理的?见下面 epoll 伪代码的介绍。


二、epoll 的伪代码

epoll 的原型由 C 语言 POSIX C 提供,下面提供出了一段伪代码,方便你理解。

// 第1步: 这里有一批socket连接// 在redis中你可以想象为redis的客户端连接, 在nginx中你可以想象为请求nginx服务器的客户端连接socket1 = socket()socket2 = socket()// socket3, socket4 ...
// 第2步: 创建epoll管理器epoll = epoll_create()
// 第3步: 将创建的socket交给epoll进行管理epoll_ctl("add", epoll, socket1)epoll_ctl("add", epoll, socket2)// epoll_ctl("add", epoll, socket3)
while(true){ // 第4步: 核心 // epoll_wait监听上面所管理的所有socket连接, 如果socket有数据交互, 将这些socket通过返回值返回 readySockets := epoll_wait() for readySockets { // 对每一个有数据的socket进行处理 }}
复制代码


2.1-工作模型

通过上面的伪代码,可以大致理解 epoll 的工作模型:

  • 针对于所有的 socket,交由 epoll 来进行管理。

  • 在编码层面,也是不断的循环调用 epoll_wait,监听哪些 socket 已经准备好进行数据收发了。

  • 获取到准备好的 socket,进行数据读写。


2.2-epoll 是如何提升 CPU 使用率的?

回到在 1.2.2 我们提到的,epoll 如何提升 CPU 的使用率?我看伪代码中也是使用 while 循环来调用 epoll_wait,这就不浪费 CPU 资源了么?


其实,大家可以这样进行理解:

  • epoll 管理的是千级别/万级别数量的 socket 连接,肯定是每个时刻都有 socket 处于有数据收发的情况,不可能你的代码中只有若干个 socket 连接,也使用 epoll 吧?

  • 既然每个时刻都有 socket 进行数据收发,那么 while 循环占用的 CPU 时间是在进行合理的工作的,这不属于浪费 CPU 的资源。只有循环占用 CPU 时间,但是实际上没有进行任何工作的,才是浪费 CPU 资源。


三、epoll 的底层设计是什么样的?

epoll 管理着千级别/万级别的 IO 连接,又有着非常高的性能,下面简单从一些方面介绍下 epoll 的设计。


注:epoll 的涉及的内容非常多,这里挑出一些比较核心的概念进行讲解,让大家有这么回事就行了。


3.1-epoll 的数据结构

之前说过,所有的 socket 连接都是由 epoll 进行管理的,在伪代码中我们使用 epoll_create 创建 epoll 之后,其实就是定义下面这样的一个 struct:

struct eventpoll {  ...   // 红黑树的根节点指针, 存储着所有的事件, 你也可以理解为所有的socket连接都在红黑树中存储  struct rb_root rbr;  // 双向链表, 存储所有就绪的socket的连接, epoll_wait的返回值就是从这个地方读取的  struct list_head rdllist;  ...};
复制代码


成员如下:

  • rbr:被 epoll 所管理的所有 socket 都存储在红黑树中。

  • rdllist:存储着所有就绪的 socket(所谓"就绪",意思指有网络数据到来,或者有数据要发送出去。)


3.1.1-为什么使用红黑树?

来看看 epoll 都有哪些操作:

  • socket 的查找。

  • socket 的新增。

  • socket 的删除。


红黑树是"平衡搜索树",其各种操作的事件复杂度可以维持在 O(logn),因此 epoll 选择使用红黑树来管理这些 socket。


3.1.2-就绪列表的优点

就绪列表这一块其实才是 epoll 能够高效的核心,对于每一个就绪的 socket,会直接放入 rdllist 中,epoll_wait 获取到就绪的 socket 时,直接从 rdllist 中进行获取就可以了,无需再进行查找。


3.2-数据的收发

epoll 是基于事件回调机制地,当 epoll 所管理地 socket 有数据到来时,会出发事件回调函数,将就绪的 socket 放置在 rdllist 中,并且通知 epoll_wait 所在的进程来处理数据。


四、扩展阅读:select

在多路复用中,还有一个经常提起的就是 select,这也是一个经常被使用的多路复用函数。


4.1-select 是如何工作的?

select 并没有像 epoll 一样,将所有的 socket 存储在红黑树中,而是使用一个数组,存储了所有的 socket 连接。


当建立新的 socket 连接时,会根据 socket id,将对应的索引位置置为 1。


select 运行时,会遍历整个数组,然后判断每一个 socket 是否就绪,就绪的话就进行处理,没有就绪就忽略。


4.2-epoll 相比于 select 的优点

  • 更快的 IO 处理:epoll 判断 socket 是否就绪,直接从 rdllist 中进行读取,事件复杂度为(1);而 select 需要遍历整个数组查找 socket 是否就绪,复杂度为 O(n)。

  • 能够处理的 socket 数量有限:epoll 能够处理千级别/万级别的 socket 连接;但是 select 默认情况下最多只能处理 1024 个连接,因此这是其并不能用于高并发的主要原因。


五、小结

最后,简单总结下 epoll 中的一些亮点吧:

  • 支持海量并发连接。

  • 使用时间复杂度为 O(logn)的红黑树管理所有的 socket。

  • 提高 CPU 的使用率,高效地使用 CPU 时间片处理所管理的 socket 连接。

  • 基于事件回调机制处理消息,而不是主动轮询机制。

  • ......

发布于: 刚刚阅读数: 3
用户头像

简单写一点散文 2021-07-29 加入

哔哩哔哩后端开发工程师。公粽号:董哥的黑板报

评论

发布
暂无评论
揭开epoll面纱:Nginx,Redis等都在用的多路复用,到底是什么?_源码_董哥的黑板报_InfoQ写作社区