wangyilin
发布于 2023-03-02 / 21 阅读
0
0

水平触发与边缘触发

背景

我们知道epoll是对多路复用接口select/poll的一个更进一步的优化,它是真正基于事件驱动的IO模型,epoll的实现依赖于在IO的过程中当状态为可读或者不可读的时候发出信号,随后CPU执行中断,并根据先前注册的回调方法调用用户代码进行处理。

这里其实就衍生出一个问题,就是什么时间发送中断信号,这里通常有两种触发方式:水平触发和边缘触发。

水平触发

Level trigger-水平触发,它表示只要内核缓冲区里面还有数据就会一直触发可读信号,它允许在任意时刻重复检测IO的状态,也只要socket处于可读或可写状态,那么不论何时进行epoll_wait都会返回该socket。

在默认的情况下,触发方式就是水平触发,它同时支持BIO和NIO,更重要的是,在这种模式中,即便没有对信息及时做出处理,内核还是会不断地触发中断来告知用户代码这里的数据是可读或可写的,这样出错的概率会低很多,传统的select/poll都是这种模型的代表。

边缘触发

Edge trigger-边缘触发,边缘触发表示只有在缓冲区的状态发生变化的时候才会产生中断信号告知用户代码当前缓冲区已经可用。

例如,只有当socket由可读变为不可读或者由可写变为不可写诸如此类的状态变化时才会发出中断信号。

边缘触发需要在一次触发中尽可能多的读取缓冲区的数据,否则在新数据到来之前是无法再次触发中断的的,这显然是有一定的风险的,但是边缘触发的性能相对更高,也就是说更加适合高速工作的一个模式。

在这种状态下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知用户代码,。随后内核会认定用户代码已知文件描述符就绪,并且不再产生新的通知,直到文件描述符的状态再一次发生改变。

常见的IO模型的触发模式

select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型既支持水平触发,也支持边缘触发,默认是水平触发 。

二者的区别

水平触发

  1. 对于读操作,只要缓冲区不为空,那么socket_wait就会一直返回就绪

  2. 对于写操作,只要缓冲区未满,那么sicket_wait就会一直返回就绪

边缘触发

对于读操作

  1. 有新的数据到来的时候

    • 缓冲区由空变为非空

    • 缓冲区可读数据增加

  2. 当缓冲区有可读的数据且通过EPOLL_CTL_MOD对EPOLLIN事件进行修改的时候

对于写操作

  1. 当缓冲区有新的可用空间时

    • 缓冲区由不可写变为可写

    • 有旧的数据被刷走的时候

  2. 当缓冲区有可写的空间且通过EPOLL_CTL_MOD对EPOLLOUT事件进行修改的时候

优缺点

优点:水平触发编程更符合用户直觉,实现逻辑更简单

缺点:水平触发效率相对边缘触发要低一些

为什么理论上边缘触发效率更高

在IO多路复用的场景下,不论是poll,select还是epoll都会维护一个链表用于记录处于就绪状态的fd,然后每当处理socket的线程被唤醒的时候,都会遍历这个链表,然后针对这个链表做出不同的处理,由于遍历行为是线性的,因此链表的大小直接决定了性能的差异。

在epoll_wait的逻辑中,当因为某个socket被唤醒的时候,会遍历目前的ready_list,并决定上一次的socket要怎么处理。

  • 对于边缘触发来说,会直接移除原本所有的socket,并调用这些socket的poll逻辑收集发生的事件

  • 对于水平触发来说,会先将数据从原本的ready_list中移除,然后调用poll逻辑,如果收集到的事件中有需要监听的事件,例如可读事件,那么这个socket就会重新加入到ready_list当中。

对于可读事件而言,在ET模式下,只有某个socket有新的数据到达,那么该sk才会被排入epoll的ready_list,从而epoll_wait 能收到可读事件的通知(对于边缘触发通常理解的缓冲区状态变化(从无到有)的理解是不准确的,准确的理解应该是是否有新的数据达到缓冲区)。 在LT模式下,某个socket被探测到只要有数据可读,那么该socket会被重新加入到read_list,所以在该socket的数据被全部取走前,下次调epoll_wait就一定能够收到该socket的可读事件(调用sk的poll逻辑一定能收集到可读事件),从而epoll_wait就能返回。

在服务端有大量连接在活跃的时候,水平触发的场景下,epoll_wait返回的时候,就会有大量的socket被重新放入到ready_list,假设用户第一次就将所有的数据都处理掉了,那么这样的操作确实会带来一定的性能损失,但是实际场景中并不是这样的。

实际应用中无法证明边缘触发效率更高

先前提到一个假设,就是用户在第一次获取到socket的时候就将缓冲区的数据全部处理掉,这样的情况下二次扫描ready_list就显得有些多余,但是这个假设很难成立,一方面是无法保证用户对缓冲区数据的处理总是这样高效的,另一方面则是,如果在用户处理的过程中有新的数据加入进来,那么此时的二次扫描就不再是多余的。

同时,我们需要意识到,epoll更适用于虽然有海量的连接,但是实际活跃的连接数比较少的情况,因此在实际应用中,边缘触发很难表现出压倒性的优势。

边缘触发并不安全 死锁与饿死

在监听读事件的场景下,epoll_wait的语义为监控并探测socket是否有数据可读

  • 水平触发保留了这个语义,它能够重复的探测数据是否可读,用户可以任意的选择时间来读取数据,这能够保证socket不会被饿死

  • 边缘触发则修改了这个语义,它所表达的语义是:监听并探测是否有新的数据可读。

边缘触发模式下,如果epoll_wait返回可读的时候,就要小心处理,至少要尽可能的多读取数据或者连接知道直到EAGAIN。

否则假设同时到来三个连接,只accept了一个连接,那么在新的连接到来之前,剩下的两个连接是不会被处理的,如果一直没有新的连接到来,那么这两个连接就会被饿死。

对于数据也是同样的,如果数据没有一次性全部读取,那么就有可能会有一部分数据一直被阻塞无法处理。# 水平触发与边缘触发

背景

我们知道epoll是对多路复用接口select/poll的一个更进一步的优化,它是真正基于事件驱动的IO模型,epoll的实现依赖于在IO的过程中当状态为可读或者不可读的时候发出信号,随后CPU执行中断,并根据先前注册的回调方法调用用户代码进行处理。

这里其实就衍生出一个问题,就是什么时间发送中断信号,这里通常有两种触发方式:水平触发和边缘触发。

水平触发

Level trigger-水平触发,它表示只要内核缓冲区里面还有数据就会一直触发可读信号,它允许在任意时刻重复检测IO的状态,也只要socket处于可读或可写状态,那么不论何时进行epoll_wait都会返回该socket。

在默认的情况下,触发方式就是水平触发,它同时支持BIO和NIO,更重要的是,在这种模式中,即便没有对信息及时做出处理,内核还是会不断地触发中断来告知用户代码这里的数据是可读或可写的,这样出错的概率会低很多,传统的select/poll都是这种模型的代表。

边缘触发

Edge trigger-边缘触发,边缘触发表示只有在缓冲区的状态发生变化的时候才会产生中断信号告知用户代码当前缓冲区已经可用。

例如,只有当socket由可读变为不可读或者由可写变为不可写诸如此类的状态变化时才会发出中断信号。

边缘触发需要在一次触发中尽可能多的读取缓冲区的数据,否则在新数据到来之前是无法再次触发中断的的,这显然是有一定的风险的,但是边缘触发的性能相对更高,也就是说更加适合高速工作的一个模式。

在这种状态下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知用户代码,。随后内核会认定用户代码已知文件描述符就绪,并且不再产生新的通知,直到文件描述符的状态再一次发生改变。

常见的IO模型的触发模式

select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型既支持水平触发,也支持边缘触发,默认是水平触发 。

二者的区别

水平触发

  1. 对于读操作,只要缓冲区不为空,那么socket_wait就会一直返回就绪

  2. 对于写操作,只要缓冲区未满,那么sicket_wait就会一直返回就绪

边缘触发

对于读操作

  1. 有新的数据到来的时候

    • 缓冲区由空变为非空

    • 缓冲区可读数据增加

  2. 当缓冲区有可读的数据且通过EPOLL_CTL_MOD对EPOLLIN事件进行修改的时候

对于写操作

  1. 当缓冲区有新的可用空间时

    • 缓冲区由不可写变为可写

    • 有旧的数据被刷走的时候

  2. 当缓冲区有可写的空间且通过EPOLL_CTL_MOD对EPOLLOUT事件进行修改的时候

优缺点

优点:水平触发编程更符合用户直觉,实现逻辑更简单

缺点:水平触发效率相对边缘触发要低一些

为什么理论上边缘触发效率更高

在IO多路复用的场景下,不论是poll,select还是epoll都会维护一个链表用于记录处于就绪状态的fd,然后每当处理socket的线程被唤醒的时候,都会遍历这个链表,然后针对这个链表做出不同的处理,由于遍历行为是线性的,因此链表的大小直接决定了性能的差异。

在epoll_wait的逻辑中,当因为某个socket被唤醒的时候,会遍历目前的ready_list,并决定上一次的socket要怎么处理。

  • 对于边缘触发来说,会直接移除原本所有的socket,并调用这些socket的poll逻辑收集发生的事件

  • 对于水平触发来说,会先将数据从原本的ready_list中移除,然后调用poll逻辑,如果收集到的事件中有需要监听的事件,例如可读事件,那么这个socket就会重新加入到ready_list当中。

对于可读事件而言,在ET模式下,只有某个socket有新的数据到达,那么该sk才会被排入epoll的ready_list,从而epoll_wait 能收到可读事件的通知(对于边缘触发通常理解的缓冲区状态变化(从无到有)的理解是不准确的,准确的理解应该是是否有新的数据达到缓冲区)。 在LT模式下,某个socket被探测到只要有数据可读,那么该socket会被重新加入到read_list,所以在该socket的数据被全部取走前,下次调epoll_wait就一定能够收到该socket的可读事件(调用sk的poll逻辑一定能收集到可读事件),从而epoll_wait就能返回。

在服务端有大量连接在活跃的时候,水平触发的场景下,epoll_wait返回的时候,就会有大量的socket被重新放入到ready_list,假设用户第一次就将所有的数据都处理掉了,那么这样的操作确实会带来一定的性能损失,但是实际场景中并不是这样的。

实际应用中无法证明边缘触发效率更高

先前提到一个假设,就是用户在第一次获取到socket的时候就将缓冲区的数据全部处理掉,这样的情况下二次扫描ready_list就显得有些多余,但是这个假设很难成立,一方面是无法保证用户对缓冲区数据的处理总是这样高效的,另一方面则是,如果在用户处理的过程中有新的数据加入进来,那么此时的二次扫描就不再是多余的。

同时,我们需要意识到,epoll更适用于虽然有海量的连接,但是实际活跃的连接数比较少的情况,因此在实际应用中,边缘触发很难表现出压倒性的优势。

边缘触发并不安全 死锁与饿死

在监听读事件的场景下,epoll_wait的语义为监控并探测socket是否有数据可读

  • 水平触发保留了这个语义,它能够重复的探测数据是否可读,用户可以任意的选择时间来读取数据,这能够保证socket不会被饿死

  • 边缘触发则修改了这个语义,它所表达的语义是:监听并探测是否有新的数据可读。

边缘触发模式下,如果epoll_wait返回可读的时候,就要小心处理,至少要尽可能的多读取数据或者连接知道直到EAGAIN。

否则假设同时到来三个连接,只accept了一个连接,那么在新的连接到来之前,剩下的两个连接是不会被处理的,如果一直没有新的连接到来,那么这两个连接就会被饿死。

对于数据也是同样的,如果数据没有一次性全部读取,那么就有可能会有一部分数据一直被阻塞无法处理。


评论