netty(六)NIO、BIO與AIO 一、BIO與NIO 二、IO模型 三、零拷貝 四、AIO

一、BIO與NIO

本小節將BIO與NIO放到一起進行分析,主要爲了突出其差別。

1.1 對比stream和channel

以前我們寫代碼,涉及到IO操作,首先想到的必然是一系列的stream,如InputStream等。如今隨着java中nio的引入,我們多了一個選擇,channel。那麼兩者相比有哪些不同,channel又有哪些優勢呢?

1)stream 不會自動緩衝數據,channel 會利用系統提供的發送緩衝區、接收緩衝區(更爲底層)。
2)stream 僅支持阻塞 API,channel 同時支持阻塞、非阻塞 API,網絡 channel 可配合 selector 實現多路複用。
3)二者均爲全雙工,即讀寫可以同時進行。

二、IO模型

當調用一次 channel.read 或 stream.read 後,會切換至操作系統內核態來完成真正數據讀取,而讀取又分爲兩個階段,分別爲:

1)等待數據準備好

2)從內核向用戶進程複製數據

阻塞IO模型

非阻塞IO模型

應用進程不停的輪詢內核空間,會造成CPU浪費。

多路IO複用模型

用戶進程首先阻塞於select方法,當內核返回可讀狀態後,根據事件類型去做調用,將數據複製到用戶空間緩衝區,處理區間狀態阻塞。

異步IO模型

AIO是java中IO模型的一種,作爲NIO的改進和增強隨JDK1.7版本更新被集成在JDK的nio包中,因此AIO也被稱作是NIO2.0。AIO提供了從建立連接到讀、寫的全異步操作。AIO可用於異步的文件讀寫和網絡通信。

三、零拷貝

3.1 原始IO分析

如下僞代碼,讀取本地文件,通過socket寫出:

File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.length()];
// 包括2次拷貝(DMA(硬件到內核緩衝區),內核緩衝到用戶緩衝)
file.read(buf);

Socket socket = ...;
//包括兩次拷貝(用戶緩衝區到socket緩衝區,DMA(socket緩衝區到網卡))
socket.getOutputStream().write(buf);

其內部實際的工作構成如下所示:

我們根據代碼的過程,結合圖上的步驟逐步分析:
1)創建文件類file,定義byte數組,當真正開始執行read方法時,纔開始獲取數據。

java 本身並不具備 IO 讀寫能力,因此 read 方法調用後,要從 java 程序的用戶態切換至內核態,去調用操作系統(內核)的讀能力,將數據讀入內核緩衝區。這期間用戶線程阻塞,操作系統使用 DMA(Direct Memory Access,可以理解爲硬件單元,解放CPU的同時,完成文件IO)來實現文件讀,其間也不會使用 cpu。

此處可以算作第一次數據拷貝,但是通過DMA技術解決了IO問題。

2) 從內核態切換回用戶態,將數據從內核緩衝區讀入用戶緩衝區(即 byte[] buf),這期間 cpu 會參與拷貝,無法利用 DMA。

此處是第二次數據拷貝。

3)調用 write 方法,這時將數據從用戶緩衝區(byte[] buf)寫入 socket 緩衝區,cpu 會參與拷貝。

此處是第三次拷貝。

4)接下來要向網卡寫數據,這項能力 java 又不具備,因此又得從用戶態切換至內核態,調用操作系統的寫能力,使用 DMA 將 socket 緩衝區的數據寫入網卡,不會使用 cpu。

此處算作第四次拷貝,DMA技術解決IO問題。

總結
通過上面的分析,我們得到java本身不具備物理設備級別的IO讀寫,而是緩存級別的讀寫,通過調用操作系統來完成硬件級別的讀寫。

上述步驟總共經歷3次狀態切換,4次的數據拷貝。

3.2 NIO優化

3.2.1 使用直接內存

在前面我們學習ByteBuffer時,介紹到了其可以使用直接內存DirectByteBuffer。

ByteBuffer buffer = ByteBuffer.allocateDirect(16);

那麼通過這個直接內存,能使我們前面的過程做到哪些優化呢?

如上圖所示,由於直接內存的引入,java 可以使用 DirectByteBuf 將堆外內存(內核緩衝區)映射到 jvm 內存(用戶緩衝區)中來直接訪問使用。而其他的步驟沒有變化。

  • 這塊內存不受 jvm 垃圾回收的影響,因此內存地址固定,有助於 IO 讀寫。

  • java 中的 DirectByteBuf 對象僅維護了此內存的虛引用,內存回收分成以下兩步:
    1)DirectByteBuf 對象被垃圾回收,將虛引用加入引用隊列
    2)通過專門線程訪問引用隊列,根據虛引用釋放堆外內存

  • 減少了一次數據拷貝,用戶態與內核態的切換次數沒有減少

3.2.1 channel的transferTo/transferFrom

底層採用了 linux 2.1
進一步優化(底層採用了 linux 2.1 後提供的 sendFile 方法),java 中對應着兩個 channel 調用transferTo/transferFrom 方法拷貝數據。

其過程如下圖所示:

1)java 調用 transferTo 方法後,要從 java 程序的用戶態切換至內核態,使用 DMA將數據讀入內核緩衝區,不會使用 cpu
2)數據從內核緩衝區傳輸到 socket 緩衝區,cpu 會參與拷貝
3)最後使用 DMA 將 socket 緩衝區的數據寫入網卡,不會使用 cpu

如上圖和過程所示,其中只經歷了一次狀態切換,數據拷貝仍然是3次。

底層採用了 linux 2.4
linux底層對於整體的效率又有了優化,如下圖所示:

1)java 調用 transferTo 方法後,要從 java 程序的用戶態切換至內核態,使用 DMA將數據讀入內核緩衝區,不會使用 cpu
2)只會將一些 offset 和 length 信息拷入 socket 緩衝區,幾乎無消耗
3)使用 DMA 將 內核緩衝區的數據寫入網卡,不會使用 cpu

整個過程只發生了一次狀態切換,實際只通過DMA經過兩次數據拷貝。

實際所謂的零拷貝並不是真正的沒有拷貝過程,而是不會有數據拷貝到用戶態,即jvm內存中的過程。

四、AIO

4.1 簡單介紹及使用

AIO 用來解決數據複製階段的阻塞問題

  • 同步意味着,在進行讀寫操作時,線程需要等待結果,還是相當於閒置
  • 異步意味着,在進行讀寫操作時,線程不必等待結果,而是將來由操作系統來通過回調方式由另外的線程來獲得結果

異步模型需要底層操作系統(Kernel)提供支持

  • Windows 系統通過 IOCP 實現了真正的異步 IO
  • Linux 系統異步 IO 在 2.6 版本引入,但其底層實現還是用多路複用模擬了異步 IO,性能沒有優勢

示例代碼:

public class TestAio {

    public static void main(String[] args) throws IOException {
        try {
            AsynchronousFileChannel s = AsynchronousFileChannel.open(
                            Paths.get("C:\\Users\\P50\\Desktop\\text.txt"), StandardOpenOption.READ);
            ByteBuffer buffer = ByteBuffer.allocate(10);
            System.out.println("begin...");
            s.read(buffer, 0, null, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("read completed..." + result);
                    buffer.flip();
                    System.out.println(Thread.currentThread().getName() + ",內容是:" + print(buffer));
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("ead failed...");
                }
            });

        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("do other things...");
        System.in.read();
    }

    static String print(ByteBuffer b) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < b.limit(); i++) {
            stringBuilder.append((char) b.get(i));
        }
        return stringBuilder.toString();
    }
}

結果,打印內容的並不是主線程,多次嘗試,每次都是不同的,並且主線程並沒有阻塞:

begin...
do other things...
read completed...10
Thread-7,內容是:helloworld

默認文件 AIO 使用的線程都是守護線程,所以最後要執行 System.in.read() 以避免守護線程意外結束。

4.2 網絡編程

服務端示例代碼:

public class AioServer {

    public static void main(String[] args) throws IOException {
        AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        ssc.accept(null, new AcceptHandler(ssc));
        System.in.read();
    }

    private static void closeChannel(AsynchronousSocketChannel sc) {
        try {
            System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress());
            sc.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel sc;

        public ReadHandler(AsynchronousSocketChannel sc) {
            this.sc = sc;
        }

        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            try {
                if (result == -1) {
                    closeChannel(sc);
                    return;
                }
                System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());
                attachment.flip();
                System.out.println(Charset.defaultCharset().decode(attachment));
                attachment.clear();
                // 處理完第一個 read 時,需要再次調用 read 方法來處理下一個 read 事件
                sc.read(attachment, attachment, this);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            closeChannel(sc);
            exc.printStackTrace();
        }
    }

    private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel sc;

        private WriteHandler(AsynchronousSocketChannel sc) {
            this.sc = sc;
        }

        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            // 如果作爲附件的 buffer 還有內容,需要再次 write 寫出剩餘內容
            if (attachment.hasRemaining()) {
                sc.write(attachment);
            }
        }

        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            exc.printStackTrace();
            closeChannel(sc);
        }
    }

    private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
        private final AsynchronousServerSocketChannel ssc;

        public AcceptHandler(AsynchronousServerSocketChannel ssc) {
            this.ssc = ssc;
        }

        @Override
        public void completed(AsynchronousSocketChannel sc, Object attachment) {
            try {
                System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());
            } catch (IOException e) {
                e.printStackTrace();
            }
            ByteBuffer buffer = ByteBuffer.allocate(16);
            // 讀事件由 ReadHandler 處理
            sc.read(buffer, buffer, new ReadHandler(sc));
            // 寫事件由 WriteHandler 處理
            sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));
            // 處理完第一個 accpet 時,需要再次調用 accept 方法來處理下一個 accept 事件
            ssc.accept(null, this);
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            exc.printStackTrace();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章