epoll边沿触发/水平触发实现分析

前言

之前在整理 selete 和 epoll 原理时,看到了两篇关于epoll ET模式的不错的文章。因此,借用此文章来加深对epoll ET模式的理解。(文末标注原文链接)

在进行ET模式的正式分析之前,我们来举个例子简单地了解下ET和LT:

假设我们通过 fork函数创建了父子两个进程,并通过 匿名管道来通信,在子进程中,我们一次向管道写入10个字符数据,为"aaaa\nbbbb\n";每隔5s写入10个字符数据;在父进程中,我们从管道中一次读取5个字符数据:
若我们采用的是 LT模式,则在5s的周期内,会先读取5个字符数据,读完之后,因为文件描述符中仍然有数据,epoll_wait会立即返回,会继续读取接下来的5个数据,之后在5s周期以内的剩余时间内,管道中的数据都为空,如下图1。
若我们采用的是 ET模式,则当父进程先读完管道中的5个字符后,由于子进程没有立即向管道中写入字符(间隔5s后才会第二次写入),所以此时父进程会先读到5个字符"aaaa\n",隔5s之后,再读到5个字符"bbbb\n",如下图2,可以看到ET模式下,随时间推移,管道中数据会越来越多。

图1:

图2:


epoll工作流程

想要了解ET模式和LT模式的区别,首先需要熟悉epoll的工作流程:

【注:】上图的poll不要理解成和select相似那个poll,这是通过epoll_ctl调用的,进行回调函数注册。

下面简要分析一下epoll的工作过程:

1)epoll_wait调用ep_poll, 当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。
2)****文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。
ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。
3)ep_events_transfer函数 将rdlist中的epitem拷贝到txlist中,并将rdlist清空。
4)ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对应的poll方法(图中蓝线)。 此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。
5)之后如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的, 则将其重新加入回rdlist(图中蓝线),否则(ET模式)不在加入rdlist。

ET模式 vs LT模式

区别

通过上图epoll的工作流程描述,可以看出ET模式和LT模式的区别主要发生在第5步:是否将文件描述符fd放回rdlist中,而rdlist的是否为空决定了epoll_wait函数的阻塞和非阻塞。因此,总结下二者的区别:

ET
如果是ET, epitem是不会再进入到readly list;除非fd再次发生了状态改变, ep_poll_callback被调用。 【因此,ET模式可减少epoll_wait函数的调用,从而减少系统开销,提高效率。】
LT
如果是非ET, 不管你还有没有有效的事件或者数据,都会被重新插入到ready list, 再下一次epoll_wait时, 会立即返回, 并通知给用户空间。当然如果这个被监听的fds确实没事件也没数据了, epoll_wait会返回一个0,空转一次。
加入rdlist途径分析

红线:fd状态改变才会触发,那什么情况会导致fd状态的改变呢?

对于读取操作:
1)当buffer由不可读状态变为可读的时候,即由空变为不空的时候。
2)当有新数据到达时,即buffer中的待读内容变多的时候。
对于写操作:
1)当buffer由不可写变为可写的时候,即由满状态变为不满状态的时候。
2)当有旧数据被发送走时,即buffer中待写的内容变少得时候。

蓝线:fd的events中有相应的时间(位置1)即会触发。那什么情况下会改变events的相应位呢?

对于读操作:
1)buffer中有数据可读的时候,即buffer不空的时候,fd的events的可读为就置1。
对于写操作:
2) buffer中有空间可写的时候,即buffer不满的时候fd的events的可写位就置1。

【说明:】

  • 红线是时间驱动被动触发(fd状态改变);

  • 蓝线是函数查询主动触发(LT和ET判断是否加入rdlist)。

ET模式如何使用

使用epoll多路复用编程时,会用epoll_wait阻塞等待事件的发生,对应有边沿触发和水平触发两种工作模式。

一、水平触发(EPOLLLT)

水平触发:只要缓冲区有数据,epoll_wait就会一直被触发,直到缓冲区为空;

水平触发(EPOLLLT)是epoll默认的工作模式,其优缺点如下:

优点:保证了数据的完整输出;

缺点:当数据较大时,需要不断从用户态和内核态切换,消耗了大量的系统资源,影响服务器性能;

应用场景:应用较少,一般用于连接请求较少及客户端发送的数据量较少的服务器,可一次性接收所有数据。此时,若使用边沿触发,会多调用一次accept/read等来判断数据是否为空。

二、边沿触发(EPOLLET)

边沿触发:只有所监听的事件状态改变或者有事件发生时,epoll_wait才会被触发;

epoll边沿触发时,假设一个客户端发送100字节的数据,而服务器设定read每次读取20字节,那么一次触发只能读取20个字节,然后内核调用epoll_wait直到下一次事件发生,才会继续从剩下的80字节读取20个字节,由此可见,这种模式其工作效率非常低且无法保证数据的完整性,因此边沿触发不会单独使用。

边沿触发通常与非阻塞IO一起使用,其工作模式为:epoll_wait触发一次,在while(1)循环内非阻塞IO读取数据,直到缓冲区数据为空(保证了数据的完整性),内核才会继续调用epoll_wait等待事件发生。

边沿触发(EPOLLET)+非阻塞IO的优缺点如下:

优点:每次epoll_wait只用触发一次,就可以读取缓冲区的所有数据,工作效率高,大大提升了服务器性能;

缺点:数据量很小时,至少需要调用两次非阻塞IO函数,而边沿触发只用调用一次。

三、什么时候用边沿触发?什么时候用水平触发?

我的答案是:任何情况都应该优先选择“边沿触发(EPOLLET)+非阻塞IO”模式。

理由如下:根据以上水平触发和边沿触发的分析,毋庸置疑,当服务端连接请求多且数据量大的时候,应该选择“边沿触发+非阻塞IO”模式,因为只用触发一次epoll_wait,就可以读取缓冲区的所有数据。那么连接请求少而且数据量也小的时候偶,为什么也优先选择边沿触发+非阻塞IO呢?在我看来,既然数据量小,那么服务端性能要求自然也不高,即使非阻塞IO读取数据多了一次判断数据为空的情况,但是其影响也不大,而且边沿触发也能满足接收大量数据的情况。

四、水平触发是否需要使用非阻塞IO?

答案是:不管水平触发还是边沿触发,都要使用非阻塞IO。

理由如下:假设水平触发使用阻塞read读取数据,且设定一次性读取20字节,现在假设客户端只发送了10个字节,那么服务端内核就会阻塞在read调用中,等待客户端再发送至少10个字节的数据,才能返回继续执行程序。但是服务端已经阻塞在系统调用read中了,无法再调用epoll_wait来监听该客户端的下一次就绪事件,也就无法接受数据,read也不可能再达到20字节了,从而就形成死锁,因此水平触发也要使用非阻塞IO。

五、服务器项目中遇到的问题

写服务器的第一个版本时,使用的是“边沿触发(EPOLLET)+非阻塞IO”模式,但是只调用了一次IO,没有循环遍历直到数据为空。这样就产生了一个问题,如下:如果给了1000个连接请求,在60S时间内,但是实际接收的连接数不到一半。这是因为每次触发只调用一次IO时,一次只能accept一个连接请求,那么需要不断的连接请求触发,才能继续accept连接,效率非常低。但是使用了while循环遍历直至数据为空之后,同样的测试,服务器能接收全部的连接请求,其原因就是一次触发就可以处理该次触发所接收的所有连接请求,大大减少了epoll_wait系统调用,减小了内核资源消耗。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值