2020- 深入浅出让你彻底理解epoll
深入浅出让你彻底理解epoll
1. 简介
2. 引言
正文开始前,先问大家几个问题。
-
epoll 性能到底有多高。很多文章介绍epoll 可以轻松处理几十万个连接。而传统IO 只能处理几百个连接,是不是说epoll 的性能就是传统IO 的千倍呢? -
很多文章把网络
IO 划分为阻塞,非阻塞,同步,异步。并表示:非阻塞的性能比阻塞性能好,异步的性能比同步性能好。
- 如果说阻塞导致性能低,那传统
IO 为什么要阻塞呢? epoll 是否需要阻塞呢?Java 的NIO 和AIO 底层都是epoll 实现的,这又怎么理解同步和异步的区别?
- 都是
IO 多路复用。
- 既生瑜何生亮,为什么会有
select ,poll 和epoll 呢? - 为什么
epoll 比select 性能高?
3. 初识epoll

很多文章在描述
可以看出来,
为什么
4.epoll 背后的原理
4.1 阻塞
4.1.1 为什么阻塞
我们以网卡接收数据举例,回顾一下之前我分享过的网卡接收数据的过程。

为了方便理解,我尽量简化技术细节,可以把接收数据的过程分为
- NIC(网卡) 接收到数据,通过
DMA 方式写入内存(Ring Buffer 和sk_buff) 。 NIC 发出中断请求(IRQ) ,告诉内核有新的数据过来了。Linux 内核响应中断,系统切换为内核态,处理Interrupt Handler ,从RingBuffer 拿出一个Packet , 并处理协议栈,填充Socket 并交给用户进程。- 系统切换为用户态,用户进程处理数据内容。
网卡何时接收到数据是依赖发送方和传输路径的,这个延迟通常都很高,是毫秒
4.1.2 阻塞不占用cpu
阻塞是进程调度的关键一环,指的是进程在等待某事件发生之前的等待状态。请看下表,在

从说明中其实就可以发现
另外讲个知识点,为了方便时间片的调度,所有“可运行状态”状态的进程,会组成一个队列,就叫
4.1.3 阻塞的恢复
内核当然可以很容易的修改一个进程的状态,问题是网络

当网卡接收到数据时,数据中一定会带着端口号,内核就可以找到对应的
4.1.4 进程模型
上面介绍的整个过程,基本就是
4.2 上下文切换的优化
上面介绍的过程中,有两个地方会造成频繁的上下文切换,效率可能会很低。
-
如果频繁的收到数据包,
NIC 可能频繁发出中断请求(IRQ) 。CPU 也许在用户态,也许在内核态,也许还在处理上一条数据的协议栈。但无论如何,CPU 都要尽快的响应中断。这么做实际上非常低效,造成了大量的上下文切换,也可能导致用户进程长时间无法获得数据。 (即使是多核,每次协议栈都没有处理完,自然无法交给用户进程) -
每个
Packet 对应一个socket ,每个socket 对应一个用户态的进程。这些用户态进程转为“可运行状态”,必然要引起进程间的上下文切换。
4.2.1 网卡驱动的NAPI 机制
在
- 函数名为
napi_schedule ,专门快速响应IRQ ,只记录必要信息,并在合适的时机发出软中断softirq 。 - 函数名为
netrxaction ,在另一个进程中执行,专门响应napi_schedule 发出的软中断,批量的处理RingBuffer 中的数据。
所以使用了

NIC 接收到数据,通过DMA 方式写入内存(Ring Buffer 和sk_buff) 。NIC 发出中断请求(IRQ) ,告诉内核有新的数据过来了。driver 的napi_schedule 函数响应IRQ ,并在合适的时机发出软中断(NET_RX_SOFTIRQ)driver 的net_rx_action 函数响应软中断,从Ring Buffer 中批量拉取收到的数据。并处理协议栈,填充Socket 并交给用户进程。- 系统切换为用户态,多个用户进程切换为“可运行状态”,按
CPU 时间片调度,处理数据内容。
一句话概括就是:等着收到一批数据,再一次批量的处理数据。
4.2.2 单线程的IO 多路复用
内核优化“进程间上下文切换”的技术叫的“

作为
4.3 IO 多路复用的进化
4.3.1 对比epoll 与select
select,
换句话说,在执行系统调用
- select (一次
O(n) 查找)
- 每次传给内核一个用户空间分配的
fd_set 用于表示“关心的socket ”。其结构(相当于bitset )限制了只能保存1024 个socket 。 - 每次
socket 状态变化,内核利用fd_set 查询O(1) ,就能知道监视进程是否关心这个socket 。 - 内核是复用了
fd_set 作为出参,返还给监视进程(所以每次select 入参需要重置) 。
然而监视进程必须遍历一遍
- epoll (全是
O(1) 查找)
- 每次传给内核一个实例句柄。这个句柄是在内核分配的红黑树
rbr+ 双向链表rdllist 。只要句柄不变,内核就能复用上次计算的结果。 - 每次
socket 状态变化,内核就可以快速从rbr 查询O(1) ,监视进程是否关心这个socket 。同时修改rdllist ,所以rdllist 实际上是“就绪的socket ”的一个缓存。 - 内核复制
rdllist 的一部分或者全部(LT 和ET ) ,到专门的epoll_event 作为出参。
所以监视进程,可以直接一个个处理数据,无需再遍历确认。

另外,
4.3.2 API 发布的时间线
另外,我们再来看看网络
- 1983,
socket 发布在Unix(4.2 BSD) - 1983,
select 发布在Unix(4.2 BSD) - 1994,
Linux 的1.0 ,已经支持socket 和select - 1997,
poll 发布在Linux 2.1.23 - 2002,
epoll 发布在Linux 2.5.44
1、
2、select、
5.Diss 环节
5.1 关于IO 模型的分类
关于阻塞,非阻塞,同步,异步的分类,这么分自然有其道理。但是在操作系统的角度来看这样分类,容易产生误解,并不好。

5.1.1 阻塞和非阻塞
换句话说,阻塞不是问题,运行才是问题,运行才会消耗
5.1.2 同步和异步
所谓的“同步“和”异步”只是两种事件分发器(event dispatcher)或者说是两个设计模式(
Reactor 对应java 的NIO ,也就是Channel ,Buffer 和Selector 构成的核心的API 。Proactor 对应java 的AIO ,也就是Async 组件和Future 或Callback 构成的核心的API 。
5.1.3 我的分类
我认为
- 更加符合程序员理解和使用的,进程模型;
- 更加符合操作系统处理逻辑的,
IO 多路复用模型。
对于“