Epoll 是捋遍理收个很老的知识点,是获颇后端工程师的经典必修课。这种知识具备的捋遍理收特点就是研究的人多,所以研究的获颇趋势就会越来越深。当然分享的捋遍理收人也多,由于分享者水平参差不齐,获颇也产生的捋遍理收大量错误理解。 图片来自 Pexels 今天我再次分享 epoll,获颇肯定不会列个表格,捋遍理收对比一下差异,获颇那就太无聊了。捋遍理收 我将从线程阻塞的获颇原理,中断优化,捋遍理收网卡处理数据过程出发,获颇深入的捋遍理收介绍 epoll 背后的原理,最后还会 diss 一些流行的观点。相信无论你是否已经熟悉 epoll,本文都会对你有价值。 正文开始前,站群服务器先问大家几个问题: ①epoll 性能到底有多高。很多文章介绍 epoll 可以轻松处理几十万个连接。而传统 IO 只能处理几百个连接,是不是说 epoll 的性能就是传统 IO 的千倍呢? ②很多文章把网络 IO 划分为阻塞,非阻塞,同步,异步。并表示:非阻塞的性能比阻塞性能好,异步的性能比同步性能好。 如果说阻塞导致性能低,那传统 IO 为什么要阻塞呢?epoll 是否需要阻塞呢?Java 的 NIO 和 AIO 底层都是 epoll 实现的,这又怎么理解同步和异步的区别? ③都是 IO 多路复用。既生瑜何生亮,为什么会有 select,poll 和 epoll 呢?为什么 epoll 比 select 性能高? PS:本文共包含三大部分:初识 epoll、epoll 背后的原理 、Diss 环节。本文的重点是亿华云介绍原理,建议读者的关注点尽量放在:“为什么”。 Linux 下进程和线程的区别其实并不大,尤其是在讨论原理和性能问题时,因此本文中“进程”和“线程”两个词是混用的。 初识 epoll epoll 是 Linux 内核的可扩展 I/O 事件通知机制,其最大的特点就是性能优异。 下图是 libevent(一个知名的异步事件处理软件库)对 select,poll,epoll ,kqueue 这几个 I/O 多路复用技术做的性能测试。 很多文章在描述 epoll 性能时都引用了这个基准测试,但少有文章能够清晰的解释这个测试结果。 这是一个限制了 100 个活跃连接的基准测试,每个连接发生 1000 次读写操作为止。纵轴是请求的响应时间,云服务器横轴是持有的 socket 句柄数量。 随着句柄数量的增加,epoll 和 kqueue 响应时间几乎无变化,而 poll 和 select 的响应时间却增长了非常多。 可以看出来,epoll 性能是很高的,并且随着监听的文件描述符的增加,epoll 的优势更加明显。 不过,这里限制的 100 个连接很重要。epoll 在应对大量网络连接时,只有活跃连接很少的情况下才能表现的性能优异。 换句话说,epoll 在处理大量非活跃的连接时性能才会表现的优异。如果15000个 socket 都是活跃的,epoll 和 select 其实差不了太多。 为什么 epoll 的高性能有这样的局限性?问题好像越来越多了,看来我们需要更深入的研究了。 epoll 背后的原理 阻塞 ①为什么阻塞 我们以网卡接收数据举例,回顾一下之前我分享过的网卡接收数据的过程。 为了方便理解,我尽量简化技术细节,可以把接收数据的过程分为四步: 网卡何时接收到数据是依赖发送方和传输路径的,这个延迟通常都很高,是毫秒(ms)级别的。而应用程序处理数据是纳秒(ns)级别的。 也就是说整个过程中,内核态等待数据,处理协议栈是个相对很慢的过程。这么长的时间里,用户态的进程是无事可做的,因此用到了“阻塞(挂起)”。 ②阻塞不占用 CPU 阻塞是进程调度的关键一环,指的是进程在等待某事件发生之前的等待状态。 请看下表,在 Linux 中,进程状态大致有 7 种(在 include/linux/sched.h 中有更多状态): 从说明中其实就可以发现,“可运行状态”会占用 CPU 资源,另外创建和销毁进程也需要占用 CPU 资源(内核)。重点是,当进程被"阻塞/挂起"时,是不会占用 CPU 资源的。 换个角度来讲。为了支持多任务,Linux 实现了进程调度的功能(CPU 时间片的调度)。 而这个时间片的切换,只会在“可运行状态”的进程间进行。因此“阻塞/挂起”的进程是不占用 CPU 资源的。 另外讲个知识点,为了方便时间片的调度,所有“可运行状态”状态的进程,会组成一个队列,就叫“工作队列”。 ③阻塞的恢复 内核当然可以很容易的修改一个进程的状态,问题是网络 IO 中,内核该修改那个进程的状态。 socket 结构体,包含了两个重要数据:进程 ID 和端口号。进程 ID 存放的就是执行 connect,send,read 函数,被挂起的进程。 在 socket 创建之初,端口号就被确定了下来,操作系统会维护一个端口号到 socket 的数据结构。 当网卡接收到数据时,数据中一定会带着端口号,内核就可以找到对应的 socket,并从中取得“挂起”进程的 ID。 将进程的状态修改为“可运行状态”(加入到工作队列)。此时内核代码执行完毕,将控制权交还给用户态。通过正常的“CPU 时间片的调度”,用户进程得以处理数据。 ④进程模型 上面介绍的整个过程,基本就是 BIO(阻塞 IO)的基本原理了。用户进程都是独立的处理自己的业务,这其实是一种符合进程模型的处理方式。 上下文切换的优化 上面介绍的过程中,有两个地方会造成频繁的上下文切换,效率可能会很低: ①网卡驱动的 NAPI 机制 在 NIC 上,解决频繁 IRQ 的技术叫做 New API(NAPI)。 原理其实特别简单,把 Interrupt Handler 分为两部分: 所以使用了 NAPI 的驱动,接收数据过程可以简化描述为: 一句话概括就是:等着收到一批数据,再一次批量的处理数据。 ②单线程的 IO 多路复用 内核优化“进程间上下文切换”的技术叫的“IO 多路复用”,思路和 NAPI 是很接近的。 每个 socket 不再阻塞读写它的进程,而是用一个专门的线程,批量的处理用户态数据,这样就减少了线程间的上下文切换。 作为 IO 多路复用的一个实现,select 的原理也很简单。所有的 socket 统一保存执行 select 函数的(监视进程)进程 ID。 任何一个 socket 接收了数据,都会唤醒“监视进程”。内核只要告诉“监视进程”,那些 socket 已经就绪,监视进程就可以批量处理了。 IO 多路复用的进化 ①对比 epoll 与 select select,poll 和 epoll 都是“IO 多路复用”,那为什么还会有性能差距呢?篇幅限制,这里我们只简单对比 select 和 epoll 的基本原理差异。 对于内核,同时处理的 socket 可能有很多,监视进程也可能有多个。所以监视进程每次“批量处理数据”,都需要告诉内核它“关心的 socket”。 内核在唤醒监视进程时,就可以把“关心的 socket”中,就绪的 socket 传给监视进程。 换句话说,在执行系统调用 select 或 epoll_create 时,入参是“关心的 socket”,出参是“就绪的 socket”。 而 select 与 epoll 的区别在于: select (一次O(n)查找): 然而监视进程必须遍历一遍 socket 数组 O(n),才知道哪些 socket 就绪了。 epoll (全是O(1)查找): 所以监视进程,可以直接一个个处理数据,无需再遍历确认。 Select 示例代码: Epoll 示例代码: 另外,epoll_create 底层实现,到底是不是红黑树,其实也不太重要(完全可以换成 hashtable)。 重要的是 efd 是个指针,其数据结构完全可以对外透明的修改成任意其他数据结构。 ②API 发布的时间线 另外,我们再来看看网络 IO 中,各个 api 的发布时间线: 就可以得到两个有意思的结论: 因为 API 设计缺陷,无法在不改变 API 的前提下优化内部逻辑。所以用 poll 替代 select,再用 epoll 替代 poll。 总结 我们花了三个章节,阐述 epoll 背后的原理,现在用三句话总结一下: Diss 环节 讲完“epoll 背后的原理”,已经可以回答最初的几个问题。这已经是一个完整的文章,很多人劝我删掉下面的 diss 环节。 我的观点是:学习就是个研究+理解的过程。上面是研究,下面再讲一下我的个人“理解”,欢迎指正。 关于 IO 模型的分类 关于阻塞,非阻塞,同步,异步的分类,这么分自然有其道理。但是在操作系统的角度来看“这样分类,容易产生误解,并不好”。 ①阻塞和非阻塞 Linux 下所有的 IO 模型都是阻塞的,这是收发数据的基本原理导致的。阻塞用户线程是一种高效的方式。 你当然可以写一个程序,socket 设置成非阻塞模式,在不使用监视器的情况下,依靠死循环完成一次 IO 操作。但是这样做的效率实在是太低了,完全没有实际意义。 换句话说,阻塞不是问题,运行才是问题,运行才会消耗 CPU。IO 多路复用不是减少了阻塞,是减少了运行。 上下文切换才是问题,IO 多路复用,通过减少运行的进程,有效的减少了上下文切换。 ②同步和异步 Linux 下所有的 IO 模型都是同步的。BIO 是同步的,select 同步的,poll 同步的,epoll 还是同步的。 Java 提供的 AIO,也许可以称作“异步”的。但是 JVM 是运行在用户态的,Linux 没有提供任何的异步支持。 因此 JVM 提供的异步支持,和你自己封装成“异步”的框架是没有本质区别的(你完全可以使用 BIO 封装成异步框架)。 所谓的“同步“和”异步”只是两种事件分发器(event dispatcher)或者说是两个设计模式(Reactor 和 Proactor)。 都是运行在用户态的,两个设计模式能有多少性能差异呢? ③我的分类 我认为 IO 模型只分两类: 对于“IO 多路复用”的事件分发,又分为两类:Reactor 和 Proactor。 关于 mmap epoll 到底用没用到 mmap?答案:没有! 这是个以讹传讹的谣言。其实很容易证明的,用 epoll 写个 demo。strace 一下就清楚了。 作者:冯志明 简介:2019 年至今负责搜索算法的相关工作,擅长处理复杂的业务系统,对底层技术有浓厚兴趣。 编辑:陶家龙 出处:转载自公众号Qunar技术沙龙(ID:QunarTL)