epoll模型中LT、ET模式分析

 

分类: C/C++

 
水平触发(level-triggered,也被称为条件触发)LT: 只要满足条件,就触发一个事件(只要有数据没有被获取,内核就不断通知你)
边缘触发(edge-triggered)ET: 每当状态变化时,触发一个事件

ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.
    LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
    ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

平时大家使用 epoll 时都知道其事件触发模式有默认的 level-trigger 模式和通过 EPOLLET 启用的 edge-trigger 模式两种。从 epoll 发展历史来看,它刚诞生时只有 edge-trigger 模式,后来因容易产生 race-cond且不易被开发者理解,又增加了 level-trigger 模式并作为默认处理方式。二者的差异在于 level-trigger 模式下只要某个 fd 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 fd;而 edge-trigger模式下只有某个 fd 从 unreadable 变为 readable 或从 unwritable 变为 writable 时,epoll_wait 才会返回该fd。

通常的误区是,level-trigger 模式在 epoll 池中存在大量 fd 时效率要显著低于 edge-trigger 模式。

但从 kernel 代码来看,edge-trigger/level-trigger 模式的处理逻辑几乎完全相同,差别仅在于 level-trigger 模式在 event 发生时不会将其从 ready list 中移除,略为增大了 event 处理过程中 kernel space 中记录数据的大小。

然而,edge-trigger 模式一定要配合 user app 中的 ready list 结构,以便收集已出现 event 的 fd,再通过 round-robin 方式挨个处理,以此避免通信数据量很大时出现忙于处理热点 fd 而导致非热点 fd 饿死的现象。统观 kernel 和 user space,由于 user app 中 ready list 的实现千奇百怪,不一定都经过仔细的推敲优化,因此 edge-trigger 的总内存开销往往还大于 level-trigger 的开销。

一般号称 edge-trigger 模式的优势在于能够减少 epoll 相关系统调用,这话不假,但 user app 里可不是只有 epoll 相关系统调用吧?为了绕过饿死问题,edge-trigger 模式的 user app 要自行进行 read/write 循环处理,这其中增加的系统调用和减少的 epoll 系统调用加起来,有谁能说一定就能明显地快起来呢?

实际上,epoll_wait 的效率是 O(ready fd num) 级别的,因此 edge-trigger 模式的真正优势在于减少了每次epoll_wait 可能需要返回的 fd 数量,在并发 event 数量极多的情况下能加快 epoll_wait 的处理速度,但别忘了这只是针对 epoll 体系自己而言的提升,与此同时 user app 需要增加复杂的逻辑、花费更多的 cpu/mem 与其配合工作,总体性能收益究竟如何?只有实际测量才知道,无法一概而论。不过,为了降低处理逻辑复杂度,常用的事件处理库大部分都选择了 level-trigger 模式(如 libevent、boost::asio等)

结论:
    epoll 的 edge-trigger 和 level-trigger 模式处理逻辑差异极小,性能测试结果表明常规应用场景 中二者性能差异可以忽略;使用 edge-trigger 的 user app 比使用 level-trigger 的逻辑复杂,出错概率更高;edge-trigger 和 level-trigger 的性能差异主要在于 epoll_wait 系统调用的处理速度,是否是 user app 的性能瓶颈需要视应用场景而定,不可一概而论。


LT自动挡,ET手动挡(epoll)

epoll有 ET和LT两种模式, 默认是LT模式。

LT模式的时候,epoll_wait 会把有事件的 file 再次加到 rdllist 列表中,以便下次epoll_wait可以再检查一遍。

点击(此处)折叠或打开

  1. if (epi->event.events & EPOLLONESHOT)
  2.                 epi->event.events &= EP_PRIVATE_BITS;
  3.             else if (!(epi->event.events & EPOLLET)) { //LT模式
  4.                 /*
  5.                  * If this file has been added with Level
  6.                  * Trigger mode, we need to insert back inside
  7.                  * the ready list, so that the next call to
  8.                  * epoll_wait() will check again the events
  9.                  * availability. At this point, noone can insert
  10.                  * into ep->rdllist besides us. The epoll_ctl()
  11.                  * callers are locked out by
  12.                  * ep_scan_ready_list() holding "mtx" and the
  13.                  * poll callback will queue them in ep->ovflist.
  14.                  */
  15.                 list_add_tail(&epi->rdllink, &ep->rdllist);
  16.             }

LT 模式下,状态不会丢失,程序完全可以于 epoll 来驱动。
ET模式下,程序首先要自己驱动逻辑,如果遇到 EAGAIN错误的时候,就要依赖epoll_wait来驱动,这时epoll帮助程序从阻碍中脱离。
所以LT是自动挡, ET是手动挡。 
ET 边沿触发的 边沿是 AGAIN错误,属于下降沿触发。
ET 的驱动事件依靠 socket的 sk_sleep 等待队列唤醒,这只有在有新包到来才能发生,数据包导致POLLIN, ACK确认导致 sk_buffer destroy从而导致POLLOUT, 但这不是一对一得关系,是多对一(多个网络包产生一个POLLIN, POLLOUT事件)。

ET常见错误:
 recv到了合适的长度, 程序处理完毕后就epoll_wait
 这时程序可能长期阻塞,因为这时socket的 rev_buffer里还有数据,或对端close了连接,但这些信息都在上次通知了你,你没有处理完,就epoll_wait了

正确做法是:
recv到了合适的长度, 程序处理; 再次recv, 若果是EAGAIN则epoll_wait。 

使用ET模式时, 程序自己驱动下,会发生socket被关闭的情况,这时要处理EPIPE信号和返回值。(如果不处理EPIPE那么会导致程序core掉) 
总结:ET 使用准则,只有出现EAGAIN错误才调用epoll_wait。
 
场景:短连接,短数据包(一个recv和send就可以完成所有的网络操作),这种情形下测试的结果是ET和LT一样的效率。(所以不要以为用了ET效率就有提高)
strace -c -fF ./your_prog 后得到信息是 epoll_ctl 消耗的时间比 recv+send 还多。 

总结原因:整个程序是以网络事件来驱动的,所以每个连接都要epoll_ctl 3次; 如果程序自己主动recv+send, 不行的时候再网络驱动的话,可以节省这些epoll_ctl开销。

对于长连接,大数据包应用,因为 LT模式只能设置当时感兴趣的事件(如果不写数据也设置POLLOUT的话,会导致cpu 100%) ,所以要频繁调用epoll_ctl,内核也要多次操作链表,所以效率会比ET模式低。
原文地址:https://www.cnblogs.com/xinxihua/p/12643216.html