『 Linux 』高级IO (四) - Epoll 的工作模式
文章目录
- Epoll的工作模式
- Reactor模式
Epoll的工作模式
Epoll
的工作模式有两种,分别为水平触发模式(Level Triggered, LT)与边缘触发模式(Edge Triggered, ET);
在信号中,水平触发(LT)指一直处于高电平状态(为真)时将不断触发,而边缘触发(ET)指由低电平到高电平的变化状态(由假至真)才会触发一次;
-
水平触发模式(Level Triggered, LT)
LT模式是
Epoll
默认的触发模式,不仅如此,Select/Poll
多路转接方案同样采用该出发模式;该触发模式的特点是,只要文件描述符处于就绪状态(比如缓冲区中有数据未读取所产生的读事件就绪或是写缓冲区中有空闲空间未被写满所产生的写事件就绪)都会使得其不停向上通知,在
Epoll
中若是模式被设置为LT模式,只要对应事件处于就绪状态,epoll_wait()
将会反复返回该事件(无论该事件是否被处理,只要是没有处理干净即表示事件仍为就绪状态); -
边缘触发模式(Edge Triggered, ET)
ET模式相比LT模式而言更加高效,因为在
Epoll
中边缘触发模式只有当数据或连接"从无到有,从少到多"产生变化时才会通知一次;在该工作模式下无论这个就绪事件是否被处理或者是否完全处理干净
Epoll
都不在关心,对应也不会再次对上层进行通知提醒;
ET效率高的模式主要是其通知效率要高于LT,只有当数据或连接"从无到有,从少到多"产生变化时才会通知一次;
相比之下LT需要频繁向上层发送通知;
ET模式只通知一次的方式使得其并不关心数据到达后是否会因上层不处理而造成数据丢失,因此单纯以ET模式来看,其通知效率高会间接使得IO的效率也变高,同时由于ET模式下Epoll
不关心所到达缓冲区数据是否被用户取走,从而倒逼程序员为避免数据的丢失而在每次通知后都取走本轮所有数据;
在ET模式下,每次通知时程序员必须循环读取缓冲区内的数据,直至缓冲区内数据被读完,当缓冲区内数据读取出错时则表示读取已经完成;
这里的读取错误以读取完成来描述更为合适;
当然这里还有一个问题,当循环调用recv()
或是read()
进行读取时,当所有数据被读取完毕时再次进行读取时由于recv()
和read()
在进行读取默认为阻塞等待读取,对应的文件描述符fd
默认也是阻塞状态,因此可能会出现读取函数阻塞等待新数据的到来,而执行流因阻塞无法继续向下执行导致类似死锁的问题,因此在使用ET工作模式时必须保证所有的文件描述符为非阻塞状态;
-
文件描述符未设置非阻塞导致的停滞现象解释为:
(相关面试题: 为什么在使用ET模式下相应文件描述符需要设置为
non-block
)-
ET通知触发
Epoll通知应用程序某个文件描述符有可用数据;
-
缓冲区被读取完毕
程序调用
recv()/read()
读取缓冲区数据,当缓冲区内所有数据被读走后缓冲区进入空状态; -
对空状态缓冲区读取
对空状态缓冲区读取时由于文件描述符处于阻塞模式,并且没有新的数据到达,函数将已知处于阻塞等待新数据的状态;
由于执行流被"卡住",事件循环将无法继续,包括处理其他文件描述符等事件;
-
ET模式不会再次触发通知
ET机制不会对缓冲区"仍然为空"的状态产生二次通知,因此应用程序永远不知道此时是否有新数据到达;
阻塞模式下产生的挂起问题难以被打破,表现为类似"死锁"的停滞现象;
-
当然ET的IO效率更高的另一个原因为,当ET模式的通知降低频率后所倒逼程序员对每轮数据读取完毕,这意味着TCP将通告一个更大的窗口给对方,在概率上使得对端每次都发送更大窗口大小的数据,提高每次IO的吞吐量;
当然根据不同程序的编码,实际上只能说ET模式的效率普遍高于LT模式的效率,因为实际上两者只是通知方式不同从而使得程序设计者在程序设计方式上不同,当然LT模式也可以编写为类似于ET的方式,即将文件描述符设置为非阻塞模式,并且在每次通知后都循环将本轮的所有数据取走,因此实际上在效率上还是依靠程序的设计;
以内核为视角时LT模式与ET模式的通知即为红黑树对应节点链入就绪队列的方式相关;
假设内核中Epoll
模型对应红黑树节点分别为A~G
七个节点,其中事件就绪的节点为D,E,F,G
四个节点,并且假设就绪所有就绪事件节点中的就绪事件均未处理(或所有对应就绪事件均未处理干净)在不同模式下:
-
LT模式
当处于LT模式时,由于为水平触发模式,
epoll_wait()
将会不停遍历红黑树,并将所有的就绪事件节点链入就绪队列中;而假设根据上面的假设,所有的就绪事件均为处理(或所有对应就绪事件均未处理干净),LT模式下的
Epoll
在每次epoll_wait()
时并不会在意当前就绪队列中就绪事件的情况,其将会清除当前就绪队列,并重新遍历红黑树将就绪节点重新接入就绪队列中;对应在上述问题中
A~G
七个节点其中D,E,F,G
四个节点并未被消费或者消费不完全时,每次epoll_wait()
都会遍历一遍红黑树,将就绪队列进行清除并重新将所有就绪节点链入就绪队列中,epoll_wait()
返回仍为D,E,F,G
四个节点对应事件; -
ET模式
在ET模式中,由于为边缘触发模式,
epoll_wait()
将仍遍历红黑树中的所有注册节点,但不同的是epoll_wait()
不会清空就绪队列,而是对就绪队列状态进行更新,将已经完全消费完成的就绪事件节点由就绪队列移除(不通知),随后根据红黑树中注册节点判断其事件就绪节点是否存在于就绪队列中,将会有以下结果:-
就绪事件节点不存在于就绪队列中
将就绪事件节点链入就绪队列并向上层通知一次;
-
就绪事件节点存在于就绪队列中
不重新链入,也不通知上层;
因此对于上面所提出的例子而言,在
ET
模式下epoll_wait()
并不会进行返回,因为所有事件均未被消费或所有时间均消费不完全; -
Reactor模式
Reactor
是一种半同步半异步模型;
这个模型结合了同步和异步的方式;
-
同步
调用方发起请求后需要等待操作完成(如IO操作)才能继续进行下一步操作;
在同步操作中,一般会占用调用方线程的执行权;
如
read()
函数在读取操作完成之前将会阻塞执行流; -
异步
调用方完成请求的发起后,无需等待结果就可以立即返回并处理其他任务,当操作完成后由系统通知调用方;
异步方式通常通过回调,事件通信或信号处理机制实现;
如
epoll
注册一个事件,当事件就绪时进行通知;
Reactor
模型的核心基于IO
多路复用,如select
,poll
或者是epoll
,通过异步非阻塞的方式;
通常情况下服务端将通过一个事件的循环,当有新的连接,数据可读或者写入完成时,操作系统将会以事件通知的方式告知应用程序,从而体现了异步特性;
当事件循环检测到某个事件时,如读事件就绪(监听套接字监听到新的连接),操作系统将会以事件通知的方式告诉上层,体现了异步性;
当上层接收通知得事件就绪时将进行任务处理,这个事件的处理可能通过事件分发器分发给各个回调函数或者线程对数据对数据进行处理;
然而通常处理的逻辑是同步的,如读取socket
数据,处理请求响应等操作将以阻塞式的方式进行,例如使用read()
函数读取数据或者accept()
获取连接;
简单来说Reactor
的同步与异步部分可以分为:
-
IO
等待和就绪状态检查(异步部分)由
epoll
等工具负责,主线程不是阻塞式线程,而是通过事件驱动机制实现非阻塞IO
; -
业务处理逻辑(同步部分)
当
IO
就绪时,由专门的线程或任务执行处理,该处理通常被认为是同步和阻塞的逻辑;