• 水平触发与边缘触发


    水平触发(level-triggered,也被称为条件触发)LT:只要满足条件,就触发一个事件。
    边缘触发(edge-triggered)ET:当状态变化时触发事件。
    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 事件的处理是仅读取定长字节,但是依然可以将长请求读取完成,因为在处理完内核缓冲区的已到达数据前,可读事件会被不断的触发。

    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 "";
            }

    用浏览器发送一个 HTTP 请求,报文会被完整的读取出来:

  • 相关阅读:
    目标跟踪之meanshift---均值漂移搞起2000过时的
    目标检测之人头---人头检测,安全帽检测,头盔检测,人流检测
    图像处理之opencv---常用函数
    图像处理之滤波---gabor
    图像处理之滤波---滤波在游戏中的应用boxfilter
    模式识别之不变矩---SIFT和SURF的比较
    Java容器集合类的区别用法
    java读取txt字符串挨个写入int数组
    阶段3 3.SpringMVC·_06.异常处理及拦截器_1 SpringMVC异常处理之分析和搭建环境
    阶段3 3.SpringMVC·_06.异常处理及拦截器_4 SpringMVC拦截器之介绍和搭建环境
  • 原文地址:https://www.cnblogs.com/niuyourou/p/12977075.html
Copyright © 2020-2023  润新知