水平觸發與邊緣觸發


水平觸發(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 請求,報文會被完整的讀取出來:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM