移除类名没有触发transition_epoll边缘触发模式

epoll(kqueue),支持两种事件触发模式。水平触发以及边缘触发。

epoll实际可以监听多种描述符,下文主要以套接字介绍,并且假设同时注册了读/写。

  • 水平触发:只要套接字可读/可写epollwait都会将描述符返回。即只要套接字的接收缓冲中尚有数据或发送缓冲有空间容纳要发送的数据。这个套接字都会被epoll_wait返回。
  • 边缘触发:当套接字的缓冲状态发生变化时返回。对于读缓冲,有新到达的数据被添加到读缓冲时触发。对于写缓冲,当缓冲发生容量变更的时候触发(对端确认分组,内核删除已经确认的分组,空出空间,写缓冲容量发生变更)。

对于边缘触发,很多人,包括我自己,一个开始对边缘这个定义并不是太清楚。例如对于读缓冲,一直以为边缘的意思是,缓冲从空到有数据。也就是以为只有当读缓冲从空到第一次接到数据的时候才会触发一次,如果不把数据读空,那么后续数据到达时不会再次触发。对于写缓冲,我之前的理解是当缓冲从满到有空闲空间时触发。

下面是linux内核中对epoll readylist的处理介绍。

fs/eventpoll.c

判断一个tcp套接字上是否有激活事件:net/ipv4/tcp.c:tcp_poll函数

epoll_wait返回readylist中的fd。eventpoll.rdllist

epitem是与epollfd关联的.如果一个fd被加入到多个epollfd中每个epollfd都会为它创建一个epitem。

下面看下readylist是如何返回到用户空间的。

epoll_wait通过ep_send_events把fd返回到用户空间。ep_send_events调用ep_scan_ready_list,ep_scan_ready_list调用ep_send_events_proc。在ep_send_events_proc里,将readylist中的epitem一一出列,如果其上确实有用户关注的事件激活,将其添加到用户空间传入的数组中。添加完之后,如果epitem非EPOLLONESHOT,非EPOLLET,会重新将epitem添加回readylist中。供下次epoll_wait时处理。对于EPOLLONESHOT,关注事件将被全部清空,需要用户重新注册事件。

可见,对于水平触发且没有设置的EPOLLONESHOT fd,epoll_wait返回之前会将fd重新添加到readylist中。如果一个fd只注册了in事件,并在epoll_wait返回之后将这个fd的读缓冲读空(假设读空之后再没有后续数据到来)。这个fd也会一直保存在readylist中,直到下一次调用epoll_wait发现它没有激活的关注事件从readylist中移除。

下面看下epitem在哪几种情况下会被主动加入readylist。

  1. EPOLL_ADD:在ep_insert中处理,如果fd上有关注的事件激活,将epitem加入readylist
  2. EPOLL_MOD:在ep_modify中处理,如果fd上有关注的事件激活,将epitem加入readylist
  3. ep_poll_callback:fd有事件到达,如果到达事件是被关注的事件,将epitem加入readylist

实际上,在linux下,如果用边缘触发同时注册了读和写,当读触发的时候,内核向用户返回fd的时候同时会检查fd是否符合可写的条件(有空间容纳待写入的数据),如果满足可写的条件,同时会加上EPOLLOUT标记。在这一点上,mac 下 kqueue的实现更符合边缘触发的描述。

边缘触发可能造成饥饿

边缘模式在什么情况下可能会造成饥饿。我们考虑一个应用,从外来连接接收数据,然后对数据进行处理。如果用边缘触发处理,对一个套接字就需要循环读取,直到没有数据可读为止(通过返回EAGIN,实际上如果读取的数据小于提供的缓冲大小也可以做这样的断定)。如果其中一个连接源源不断的发送数据,这个套接口的读循环就无法退出,导致其它连接没有机会被处理。

为了避免这种情况的出现,epoll_wait返回时,并不直接处理套接字,而是将套接字添加到active list中。等到把epoll_wait返回的套接字都添加完后,再对active list中的套接字执行io处理。

对于active list中的每个元素,如果判定套接字不可读,则从active list中移除。否则最多执行有限次操作,超过次数之后将套接字保留在active list中,处理后续套接字。

伪代码如下:

func 

边缘触发模式是否比水平触发模式更高效

对于这个问题,我实际测试过两种模式,并没有发现肉眼可见的效率差异。

两种模式执行epoll_wait的次数是大致相当的,区别是readylist大小会有所不同,

对于只从套接字接收数据而不发送数据的应用来说。考虑以下处理模式:

每次对一个fd只执行一read,然后处理接收到的数据。

如果每次调用epoll_wait前,所有fd都会接收到数据,那么无论哪种触发模式,readylist都是所有被监听的fd。开销完全一样。

边缘触发只有在下述情形下才能获得优势:

假如有X个连接,套接字接收缓冲为N,对端每次发送都能将N填满,read时提供的用户缓冲为N/M。且发送端的发送频率是接收端处理频率的1/N/M,即读端正好消费完N字节后写端菜继续发送。 在这种模式下水平模式每一轮epoll_wait的readylist都是X。而边缘模式则以N/M为周期,除了周期第一次为X,后续都是N/M-1次0(因为无外来数据到达,不会被添加到readylist中)。

下面再看下发送的情况。对于out事件,水平触发返回的条件是发送缓冲有空间,边缘触发的条件是发送缓冲容量变更(对端确认数据包,发送端释放发送缓冲中的空间)。

对于水平触发模式out事件必须按需注册。主要的注册方式有以下两种:

  • 上层调用send,将数据添加到应用层的发送缓冲,如果当前没有注册out则注册out,当epoll通知out激活时,发送应用缓冲中的数据,如果数据发送完毕注销out。
  • 上层调用send,直接发送,如果数据未发送完或返回EAGAIN,则注册out,当epoll通知out激活时,继续发送未发送完成的数据,如果数据发送完毕注销out。

上述的添加和注销out都是通过epoll_ctl完成。边缘触发模式则无此需要。

我这里主要分析第二种模式。

假如发送每次均能将数据全部发完。那么out的注册和注销都不会发生。

如果接收方慢导致每次均无法将数据全部发送完,那么out将只会注册一次,注销不会发生。因为上层的send会导致待发送队列无法排空。

导致out的注册和注销频繁发生的情况只有一种,发送端每次send请求发送M字节,但实际只能发送M/N字节,因此注册out,每次out触发后都能发送M/N字节,直到M字节全部发送完毕注销out。在这种情况下,在所有有M字节发送完毕前上层不会触发新的send请求。对于这种情况无论哪种模式每次调用epoll_wait,readylist大小都是一样的。开销的差距只会体现在用epoll_ctl注册和注销out上。

因此,边缘触发在机制上应该是更高效的,但是构造出一个能明显看出效率优势的场景并不容易。

下面贴一下我的一个多线程边缘触发模式的实验网络库,支持linux epoll和mac kqueue。区别于muduo的loop per thread模式。这个实验库只启动一个线程监听epoll/kqueue。当套接字被激活后将其交给io线程执行实际的io。

sniperHW/hwnet​github.com
dfb56e2d293216cb182469cc69aa3134.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值