I/O多路复用中的水平触发和边缘触发

简介: I/O多路复用中的水平触发和边缘触发

水平触发(level-triggered,也被称为条件触发)LT:只要满足条件,就触发一个事件。

边缘触发(edge-triggered)ET:当状态变化时触发事件。

大家可能还不能完全了解这两种模式的区别,我们可以举例说明:一个管道收到了1kb的数据,epoll会立即返回,此时读了512字节数据,然后再次调用epoll.这时如果是水平触发的,epoll会立即返回,因为有数据准备好了.如果是边缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来,直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取).

下面我们还从电子的角度来解释一下:

水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.上面提到的只要有数据可读(描述符就绪)那么水平触发的epoll就立即返回.

边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即使有数据可读,但是没有新的IO活动到来,epoll也不会立即返回.

JAVA 的 NIO 属于水平触发,而 epoll 既支持水平触发也支持边缘触发。epoll 性能高于 poll 很重要的一点便是 epoll 支持了边缘触发。

在水平触发的情况下,必须不断的轮询监控每个文件描述符的状态,判断其是否可读或可写。内核空间中维护的 I/O 状态列表可能随时会被更新,因此用户程序想要拿到 I/O 状态列表必须访问内核空间。

而边缘触发的情况下,只有在数据到达网卡,也就是说 I/O 状态发生改变时才会触发事件,在两次数据到达的间隙,I/O 状态列表是不会发生改变的。这就使得用户程序可以缓存一份 I/O 状态列表在用户空间中,减少系统调用的次数。

但是在边缘触发的情况下,I/O 操作必须一次性的将数据处理完。因为如果没有处理完数据,只有等待下次数据包到达网卡才会再次触发事件。

在水平触发的情况下,可以处理内核缓冲区中任意长度的数据。如果数据没有处理完,内核会再次触发事件。因此剩余数据在下次事件到来时继续处理即可。

至于网上很多文章说的边缘触发需要非阻塞读写,个人认为水平触发也需要非阻塞读写。因为它们都属于多路复用技术的实现方式,而使用多路复用技术的触发点便是用更少的线程做更多的事。单线程情况下,无论水平触发还是边缘触发,使用阻塞读写都会造成线程无法处理其它事件的情况。

简单看一下 epoll 的运作过程:

  1. epoll初始化时,会向内核注册一个文件系统,用于存储被监控的句柄文件,调用epoll_create时,会在这个文件系统中创建一个file节点。同时epoll会开辟自己的内核高速缓存区,以红黑树的结构保存句柄,以支持快速的查找、插入、删除。还会再建立一个list链表,用于存储准备就绪的事件。
  2. 当执行epoll_ctl时,除了把socket句柄放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后,就把socket插入到就绪链表里。
  3. 当epoll_wait调用时,仅仅观察就绪链表里有没有数据,如果有数据就返回,否则就sleep,超时时立刻返回。在这里我的猜测是,如果采用边缘触发,流程便是上述情况。但如果是水平触发,epoll 还会扫描每个 file 节点,查看其是否存在可读数据。这个还需查资料考证。

我们来一个 Demo 更直观的感受一下 JAVA SocketChannel 的水平触发。代码中对 read 事件的处理是仅读取定长字节,但是依然可以将长请求读取完成,因为在处理完内核缓冲区的已到达数据前,可读事件会被不断的触发。

package p20210105;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
/**
 * @author zhouyanxiang
 * @Date 2021-01-2021/1/5-14:41
 */
public class Test {
    public static void main(String[] args) throws IOException {
        server();
    }
    private static void server() throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel severChannel = ServerSocketChannel.open();
        severChannel.configureBlocking(false);
        severChannel.bind(new InetSocketAddress(8888));
        System.out.println("Server start!");
        severChannel.register(selector, SelectionKey.OP_ACCEPT);
        //select会阻塞,知道有就绪连接写入selectionKeys
        while (!Thread.currentThread().isInterrupted()) {
            if (selector.select(100) == 0) {
                continue;
            }
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                //SelectionKey为select中记录的就绪请求的数据结构,其中包括了连接所属的socket及就绪的类型
                SelectionKey key = keys.next();
                //处理事件,不管是否可以处理完成,都删除 key。因为 soketChannel 为水平触发的,
                // 未处理完成的事件删除后会被再次通知
                keys.remove();
                if (key.isAcceptable()) {
                    System.out.println("触发连接事件");
                    SocketChannel socketChannel = severChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(8);
                    int len = socketChannel.read(byteBuffer);
                    byteBuffer.flip();
                    if (len == -1) {
                        socketChannel.close();
                    }
                    if ( byteBuffer.remaining() > 0) {
                        System.out.print(new String(getString(byteBuffer)));
                    }
                    socketChannel.register(selector, SelectionKey.OP_READ);
//                    System.out.println("触发读事件");
                }
            }
        }
    }
    public static String getString(ByteBuffer buffer) {
        Charset charset = null;
        CharsetDecoder decoder = null;
        CharBuffer charBuffer = null;
        try {
            charset = StandardCharsets.UTF_8;
            decoder = charset.newDecoder();
            // charBuffer = decoder.decode(buffer);//用这个的话,只能输出来一次结果,第二次显示为空
            charBuffer = decoder.decode(buffer.asReadOnlyBuffer());
            return charBuffer.toString();
        } catch (Exception ex) {
            ex.printStackTrace();
            return "";
        }
    }
}

这个时候用浏览器发送请求127.0.0.1:8888

可以在console控制台收到这个报文

https://www.cnblogs.com/niuyourou/p/12977075.html


相关文章
|
存储 算法 编译器
【C++ TypeName用法 】掌握C++中的TypeName:模板编程的瑞士军刀
【C++ TypeName用法 】掌握C++中的TypeName:模板编程的瑞士军刀
968 1
|
9月前
|
机器学习/深度学习 自然语言处理 并行计算
《深度揭秘:利用Hugging Face Transformer库打造独特混合专家(MoE)模型》
混合专家(MoE)模型是一种创新架构,通过融合多个“专家”子模型,针对不同任务提供更优解决方案。相比传统单一模型,MoE能更好地应对复杂多样的语言任务。借助Hugging Face Transformer库,可利用预训练模型定制专家,并设计门控网络协调任务分配。本文详细解析了MoE模型的设计、训练与优化方法,探讨其在智能客服、机器翻译等领域的应用潜力,以及未来推动自然语言处理技术发展的可能性。
408 1
|
网络协议 安全 网络安全
图解OSI七层模型,2024最强科普!
【7月更文挑战第20天】
4210 2
图解OSI七层模型,2024最强科普!
|
数据采集 JSON 监控
Haskell爬虫:为电商运营抓取京东优惠券的实战经验
Haskell爬虫:为电商运营抓取京东优惠券的实战经验
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
651 4
Java ArrayList扩容的原理
|
监控 大数据 应用服务中间件
epoll的水平触发LT以及边沿触发ET的原理及使用及优缺点
epoll的水平触发LT以及边沿触发ET的原理及使用及优缺点
995 0
|
存储 XML 关系型数据库
深入理解MySQL中的BLOB和TEXT数据类型
【8月更文挑战第31天】
1732 0
|
数据采集 数据可视化 小程序
vue3+echarts可视化——记录我的2023编程之旅
vue3+echarts可视化——记录我的2023编程之旅
427 1
|
存储 Web App开发 编译器
C语言程序设计——int,double,char的用法
C语言程序设计——int,double,char的用法

热门文章

最新文章