Java NIO原理 (Selector、Channel、Buffer、零拷貝、IO多路復用)

来源:https://www.cnblogs.com/cuzzz/archive/2023/06/11/17473398.html
-Advertisement-
Play Games

[系列文章目錄和關於我](https://www.cnblogs.com/cuzzz/p/16609728.html) ## 零丶背景 最近有很多想學的,像netty的使用、原理源碼,但是苦於自己對於操作系統和nio瞭解不多,有點無從下手,遂學習之。 ## 一丶網路io的過程 ![image-202 ...


系列文章目錄和關於我

零丶背景

最近有很多想學的,像netty的使用、原理源碼,但是苦於自己對於操作系統和nio瞭解不多,有點無從下手,遂學習之。

一丶網路io的過程

image-20230610201828763

上圖粗略描述了網路io的過程,瞭解其中的拷貝過程有利於我們理解非阻塞io,以及IO多路復用的必要性。

  1. 數據從網卡到內核緩衝區
    網卡通過DMA的方式將網路幀copy到內核空間

    並不是拷貝到內核空間就完事了,因為還需要根據協議對數據進行處理。

    所以網卡使用硬中斷通知cpu,cpu響應後會使用網卡註冊函數進行收包,然後協議層處理網路幀。

  2. 數據從內核緩衝區到用戶空間

    根據協議處理好的數據,還需要拷貝到用戶空間才能被運行在內核態的應用程式使用==>cpu進行數據拷貝。隨後內核喚醒用戶進程,相當於我們的java程式從阻塞io中被喚醒,繼續執行下一行代碼的執行。

二丶Socket通信過程與其中的阻塞點

img

這其中有幾個阻塞的過程

  • accept 系統調用:等待客戶端建立tcp連接

    這個問題不大,沒有連接那麼阻塞服務端線程,可以節約cpu資源。

  • read系統調用:等待請求數據來到用戶空間

    數據從網卡到用戶空間的過程,線程時阻塞的

  • Servlet#service 處理請求是一個同步過程

    tomcat根據http協議構造request,並和response作為參數,找到對應Servlet調用service方法,Servlet#service方法執行結束,返回內容才能通過write系統調用回應數據。

    這導致在業務處理上需要使用線程池來讓服務端可以處理多個併發請求。

  • write系統調用:響應數據寫回

    write系統調用將servlet處理後的響應數據,寫回到文件描述符中。

三丶NIO解決了什麼問題

1.單線程監測若幹個文件描述符是否可以執行IO操作

這就是常說的IO多路復用,那為什麼需要IO多路復用?

儘量使用較少的系統資源處理更多的連接,如果當前單台伺服器接收了1w個請求,服務端當如何處理?

1.1 傳統BIO模型

image-20230611122304883

上面是一段java BIO模型併發處理多請求的實例代碼,它有以下不足

  • 大量的線程占用很大的記憶體空間
  • 線程切換會帶來很大的開銷
  • process方法中需要需要調用read系統調用,阻塞直到可讀,並沒有真正進行讀寫操作。

1.2. 非阻塞IO

image-20230611124511674

上面是非阻塞IO的一個實例

socketChannel.configureBlocking(false)可以讓後續的read在通道數據沒有就緒的時候直接返回-1,而不是讓線程阻塞。這個特性讓調度線程池中的線程減少了阻塞,從而節省了線程資源。

image-20230405164149306

但是這種方式也不是沒有任何缺點,多次系統意味著多次系統調用,每次系統調用都需要,用戶態<=>內核態的來回切換,需要cpu保存進程的上下文,調用結束還需要恢復進程的上下文。

image-20230611133144345

1.3 IO多路復用

image-20230611135115721

如上是Java IO多路復用的簡陋例子。操作系統提供了多路復用的機制,將連接上來的客戶端都進行註冊,然後不斷迴圈掃描各個客戶端連接,監聽客戶端的請求。但是,多路復用輪詢掃描各個客戶端連接的過程在操作系統內核中進行極大的加快了多路復用的效率,減少了用戶態和內核態的切換

2.減少堆內記憶體<=>堆外記憶體的拷貝開銷

使用NIO Channel讀寫時需要需要先讀到堆外記憶體,然後拷貝到堆內記憶體,如果直接使用堆外記憶體則可以減少堆外到堆內的拷貝過程。

下圖是將Channel數據讀取到Buffer,調用IOUtil#read的源碼

image-20230611154939538

下圖是將Buffer數據寫入到Channel,調用IOUtil#write的源碼

image-20230611155307043

2.1 為什麼需要再堆外記憶體和堆內記憶體來回捯飭?

寫入Buffer數據到文件描述符,or讀取文件描述符數據到Buffer都是需要進行系統調用的,執行系統調用依賴於執行native方法,而執行native方法的線程被認為是處於SafePoint,處於SafePoint就有可能發生 GC 重排列對象記憶體的情況。

並且這個寫入和讀取是針對地址的(如下圖,最終的native調用需要傳入地址)如果寫入或者讀取buffer由於gc移動,那麼地址會改變,但是native方法調用可不管這個,就導致讀寫出現錯誤。因此需要依賴於堆外記憶體。

image-20230611155717440

2.2 為什麼Socket基於Inpustream,OutputStream沒有這個問題

image-20230611160657076

以SokcetInputStream的讀為例,讀最終調用socktRead0這個native方法,入參fd是當前Socket對應的文件描述符,byte數組就是數據最終讀入的目的地。

下圖是native 方法socketRead0的實現

image-20230611161135896

可以看到,其實是先將socket fd內容讀取到c語言聲明的數組,然後拷貝到Java byte[],這個c語言聲明的數組其實作用類似於直接記憶體!

3.減少內核空間和用戶空間的拷貝開銷

上面說了直接記憶體的作用:減少堆外堆內的拷貝開銷。無論堆外堆內,都是用戶空間的拷貝。

3.1 DMA控制器替CPU打工

image-20230611162822090

上圖是讀取磁碟文件的時序圖,可以看到如果沒有DMA技術,藍色部分需要CPU來完成,將浪費寶貴的資源。

再DMA讀取到足夠數據後,會發送中斷信號給CPU,讓CPU將內核緩衝區數據,拷貝到用戶緩衝區,隨後CPU再來調度Java程式,Java程式才能操作到用戶緩衝區的數據。

3.2 零拷貝

3.2.1 傳統文件傳輸

如下圖是我們使用IO流,讀取磁碟文件,通過Socket API 發送的流程,其中需要read,和 write 系統調用,每次系統調用都意味著用戶態與內核態的上下文切換

並且還有四次數據拷貝,其中兩次由DMA負責打工,兩次由CPU負責拷貝。

image-20230611163605920

如何優化:

  • 如果Java程式不需要對磁碟數據內容進行再加工(業務操作)那麼不需要拷貝到用戶空間,從而減少拷貝次數
  • 由於用戶空間沒有操作網卡和磁碟的許可權,操作這些設備需要由操作系統內核完成,那麼如果操作系統提供新的系統調用函數,豈不是就可以減少用戶態與內核態的上下文切換
3.2.2 mmap + write

image-20230611164712477

  • 應用進程調用了 mmap() 後,DMA 會把磁碟的數據拷貝到內核的緩衝區里。接著,應用進程跟操作系統內核共用這個緩衝區;
  • 應用進程再調用 write(),操作系統直接將內核緩衝區的數據拷貝到 socket 緩衝區中,這一切都發生在內核態,由 CPU 來搬運數據;
  • 最後,把內核的 socket 緩衝區里的數據,拷貝到網卡的緩衝區里,這個過程是由 DMA 搬運的

所以mmap優化了什麼?

mmap並沒有減少系統調用帶來的內核態用戶態切換開銷,只是應用程式和內核共用緩衝區,從而讓cpu可以直接將內核緩衝區的數據,拷貝到socket緩衝區,不需要拷貝到用戶緩衝區,再從用戶緩衝區拷貝到socket緩衝區。

3.2.3 sendfile

linux 提供sendfile系統調用,只需這一個系統調用就可以從一個文件描述符拷貝數據到另外一個文件描述符

image-20230611165644768

image-20230611165437243

sendfile可以減少write,read導致的系統調用,從而優化效率。

如果網卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技術,那麼還可以進一步優化。

image-20230611165914874

  1. 通過 DMA 將磁碟上的數據拷貝到內核緩衝區里;
  2. 緩衝區描述符和數據長度傳到 socket 緩衝區,這樣網卡的 SG-DMA 控制器就可以直接將內核緩存中的數據拷貝到網卡的緩衝區里,此過程不需要將數據從操作系統內核緩衝區拷貝到 socket 緩衝區中,這樣就減少了一次數據拷貝。

這便是所謂的零拷貝,減少記憶體層面拷貝數據的次數,以及系統調用內核態用戶態的切換,從而優化性能。

3.3 NIO中的零拷貝

3.3.1 FileChannel#map

NIO中的FileChannel.map()方法使用了mmap系統調用實現記憶體映射方式

將內核緩衝區的記憶體和用戶緩衝區的記憶體做了一個地址映射。這種方式適合讀取大文件,同時也能對文件內容進行更改,但是如果其後要通過SocketChannel發送,還是需要CPU進行數據的拷貝。

image-20230611172227076

如上是MappedByteBuffer的獲取方式,其實底層是通過反射調用DirectByteBuffer的構造方法實現的,其中的cleaner是直接記憶體的回收器,傳入的unmapper會被回調,從而調用native方法實現資源釋放。

image-20230611172125469

這種方式適合讀取大文件,同時也能對文件內容進行更改。

3.3.2 FileChannel#transferTo,transerFrom

image-20230611173326560

在操作系統層面是調用的一個sendFile系統調用。通過這個系統調用,可以在內核層直接完成文件內容的拷貝。

4.FileChannel#force強制刷盤

由於CPU的運行速度非常快,所以CPU在執行指令時,通常只能與緩存進行交互,而不適合直接操作像磁碟、網卡這樣的硬體。也因此,在進行文件寫入時,操作系統也是先寫入到page cache中,緩存起來,然後再往硬體寫入。

緩存有利也有弊,使用page cache頁緩存,應用程式將數據都寫入到了page cache中,但是卻沒有真正寫入磁碟。如果這個時候出現斷電,那麼將出現緩存數據丟失。

FileChannel#force會進行fsync系統調用

image-20230611174940240

fsync可以實現將page cache緩存內容進行落盤,從而保證不丟失(redis aof可以設置持久化機制,通常設置每秒落盤一次,這裡落盤也是fsync系統調用)。為了性能考慮,應用程式不可能每寫入一點數據就調用fsync,fsync也是有性能損耗的。

四丶IO多路復用 select/poll/epoll

上面我們聊到了IO多路復用解決了什麼問題,以及NIO Selector的基本使用,但是沒有探究在操作系統層面是如何實現的,下麵來學習一下。

1.select系統調用

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout)
  • nfds: 最大的文件描述符+1,代表監聽這一組描述符(為什麼要+1?因為除了當前最大描述符之外,還有可能有新的fd連接上來)
  • fd_set: 是一個點陣圖集合, 對於同一個文件描述符,可以監聽不同的事件
  • readfds:文件描述符“可讀”事件
  • writefds:文件描述符“可寫”事件
  • exceptfds:文件描述符“異常”事件,一般內核用的,實際編程很少使用
  • timeout:超時時間:0是立即返回,-1是一直阻塞,如果大於0,則達到設置值的微秒數即返回
  • 返回值: 所監聽的所有監聽集合中滿足條件的總數(滿足條件的讀、寫、異常事件的總數),出錯時返回-1,並設置errno。如果超時時間觸發,則返回0

select 其實就是把NIO中用戶態要遍歷的fd數組拷貝到了內核態,讓內核態來遍歷,因為用戶態判斷socket是否有數據依舊需要通過系統調用,切換到內核態進行。

可以看到select依賴了很多點陣圖參數,系統調用完後需要用戶程式遍歷一次點陣圖才能直到哪一個fd具備了io事件,並且這個點陣圖大小最大為1024,導致select用起來需要很多位操作並且最多只能支持1024路IO。

2.poll系統調用

int poll(struct pollfd *fds, nfds_t nfds/*最大監聽的文件描述符個數*/, int timeout/*最大監聽的文件描述符個數*/);

其中pollfd為:

struct pollfd {
      int   fd;         /* file descriptor */
      short events;     /* requested events */
      short revents;    /* returned events */
};

poll可以看作升級版select,它突破了1024個文件描述符的限制,並且poll函數的監聽和返回是分開的,簡化了代碼實現。

雖然poll不需要遍歷所有的文件描述符了,只需要遍歷加入數組中的描述符,範圍縮小了很多,但缺點仍然是需要遍歷,當加入數組描述符很多,但是存在事件的fd很少,這個遍歷操作還是有點不划算的。

3.epoll系統調用

在linux環境下,java nio中的selector就是基於epoll實現的。

3.1 epoll_create

int epoll_create(int size)
    //返回一個fd
    //傳入大小作為參考值

epoll_create返回一個特殊的文件描述符,它代表紅黑樹的根節點。size則是樹的大小,它代表你將監聽多少個文件描述符。epoll_create將按照傳入的大小,構造出一棵大小為size的紅黑樹。

3.2 epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epfd 是epoll_create的返回值,也就說紅黑樹的根節點
// op 表示操作,比如增加,修改,刪除
//fd 是需要增加,修改,刪除的文件描述符
// struct epoll_event *event 是一個結構體,如下
struct epoll_event {
               uint32_t     events;      /* Epoll events 讀事件or寫事件,or 異常事件*/
               epoll_data_t data;        /* User data variable */
           };
 typedef union epoll_data {
               void        *ptr;
               int          fd;//代表一個文件描述符,初始化的時候傳入需要監聽的文件描述符,當監聽返回時,此處會傳出一個有事件發生的文件描述符,因此,無需我們遍歷得到結果了
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

用來操作epoll句柄,可以使用該函數往紅黑樹里增加文件描述符,修改文件描述符,和刪除文件描述符。

3.3 epoll_wait

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
//epfd 是epoll_create的返回值,也就說紅黑樹的根節點
// struct epoll_event *events 是一個數組,返回的所有觸發了事件的文件描述符集合
//maxevents代表這個數組的大小
//timeout 0代表立即返回,-1代表永久阻塞,如果大於0,則代表超時等待毫秒數

3.4 水平觸發,邊緣觸發

epoll有兩種觸發方式,分別為水平觸發邊沿觸發

  • 水平觸發

    只要有數據處於就緒狀態,那麼可讀事件就會一直觸發。

    舉個例子,假設客戶端一次性發來了4K數據 ,但是伺服器recv函數定義的buffer大小僅為1024位元組,那麼一次肯定是不能將所有數據都讀取完的,這時候就會繼續觸發可讀事件,直到所有數據都處理完成。

    epoll預設的觸發方式就是水平觸發。

  • 邊緣觸發

    只有數據發送過來的時候會觸發一次,即使數據沒有讀取完,也不會繼續觸發。

  • 觸發方式的設置:

    水平觸發和邊沿觸發在內核里 使用兩個bit mask區分,分別為:

    • EPOLLLT 水平 觸發
    • EPOLLET 邊沿觸發

    需要在註冊事件的時候將其與需要註冊的事件做一個位或運算即可:

    ev.events = EPOLLIN;    //LT
    ev.events = EPOLLIN | EPOLLET;   //ET
    

4.總結

select函數需要一次性傳入所有需要監控的連接(在內核中是FD),併在內核中對這些FD進行持續的掃描。當發現其中有FD不老實時,就會通知應用程式有客戶端事件發生了, 上層應用接到通知後,就只能自己再去遍歷所有的FD,尋找有事件發生的連接,然後進行業務處理。
但是select受限於操作系統,掃描的FD個數是受限的。

於是出現了Poll函數,解決了slelect文件描述符受限的問題。但是,上層應用程式依然要自己去遍歷所有客戶端,尋找哪個客戶端上有事件發 生。高併發場景下,性能依然嚴重受限。
於是又出現了epoll機制。

epoll機制會直接返回有事件發生的FD。這樣就省掉了上層應用頻繁掃描所有客戶端的消耗,進一步解決多路復用的高併發問題。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 轉載請註明出處❤️ 作者:[測試蔡坨坨](https://www.caituotuo.top/) 原文鏈接:[caituotuo.top/400bd75c.html](https://www.caituotuo.top/400bd75c.html) 你好,我是測試蔡坨坨。 在日常測試工作中,我們經常 ...
  • 某日二師兄參加XXX科技公司的C++工程師開發崗位第11面: > 面試官:在C++中,你都知道都哪些運算符? > > 二師兄:啥?運算符?`+-*/=`這些算嗎? > > 面試官:嗯,還有其他的嗎? > > 二師兄:當然還有,`+=,-=,*=,/=,==`,還有邏輯運算,位運算等。 > > 面試官 ...
  • ## 什麼是GTH GTH 是Xilinx UltraScale系列FPGA上高速收發器的一種類型,本質上和其它名稱如GTP, GTX等只是器件類型不同、速率有差異;GTH 最低速率在500Mbps,最高在16Gbps ![](https://img2023.cnblogs.com/blog/274 ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • List 介面是 Collection 介面的子介面。List 中元素有序,是按照元素的插入順序進行排序的。每個元素都有一個與之關聯的整數型索引(索引從 0 開始),可以根據索引來訪問和操作元素,可以使用普通 for 迴圈遍歷。List 中可以包含重覆的元素。 ...
  • # Go 實現 MySQL 資料庫事務 ## 一、MySQL事務 MySQL事務是指一組資料庫操作,它們被視為一個邏輯單元,並且要麼全部成功執行,要麼全部回滾(撤銷)。事務是資料庫管理系統提供的一種機制,用於確保數據的一致性和完整性。 事務具有以下特性(通常由ACID原則定義): 1. 原子性(At ...
  • 前言 本文主要介紹使用spring boot 配置多個資料庫,即動態資料庫 開始搭建 首先創建一個SpringWeb項目——dynamicdb(spring-boot2.5.7) 然後引入相關依賴lombok、swagger2、mybatis-plus,如下: <?xml version="1.0" ...
  • # todo 列表 - [ ] clang-format - [ ] c++ 整合 # 軟體安裝 略 # 基本的環境搭建 ## 最基本的 vscode 插件 只需要安裝如下兩個插件即可 c/c++ 擴展是為了最基本的代碼提示和調試支持 cmake language support 是為了提示 CMa ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...