1. 程式人生 > 其它 >Java基礎(一):I/O多路複用模型及Linux中的應用

Java基礎(一):I/O多路複用模型及Linux中的應用

IO多路複用模型廣泛的應用於各種高併發的中介軟體中,那麼區別於其他模式他的優勢是什麼、其核心設計思想又是什麼、其在Linux中是如何實現的?

I/O模型

I/O模型主要有以下五種:

  1. 同步阻塞I/O:I/O操作將同步阻塞使用者執行緒
  2. 同步非阻塞I/O:所有操作都會立即返回,但需要不斷輪詢獲取I/O結果
  3. I/O多路複用:一個執行緒監聽多個I/O操作是否就緒,依然是阻塞I/O,需要不斷去輪詢是否有就緒的fd
  4. 訊號驅動I/O:當I/O就緒後,作業系統傳送SIGIO訊號通知對應程序,避免空輪詢導致佔用CPU(linux中的訊號驅動本質還是使用的epoll
  5. 非同步I/O:應用告知核心啟動某個操作,並讓核心在整個操作完成之後,通知應用,這種模型與訊號驅動模型的主要區別在於,訊號驅動IO只是由核心通知我們可以開始下一個IO操作,而非同步IO模型是由核心通知我們操作什麼時候完成

聊聊Linux 五種IO模型

其中應用最廣的當屬I/O多路複用模型,其核心就是基於Reactor設計模式,僅一個執行緒就可以監聽多個I/O事件,使得在高併發場景下節約大量執行緒資源

Reactor設計模式

處理WEB通常有兩種請求模型:

  1. 基於執行緒:每個請求都建立一個執行緒來處理。併發越高,執行緒數越多,記憶體佔用越高,效能也會越低,執行緒上下文切換造成效能損耗,執行緒等待IO也會浪費CPU時間。一般應用於併發量少的小型應用。
  2. 事件驅動:每個請求都由Reactor執行緒監聽,當I/O就緒後,由Reactor將任務分發給對用的Handler。

顯然事件驅動模型更適用於目前動輒幾十萬併發的場景。

網路伺服器的基本處理模型如下:建立連線->讀取請求->解析請求->處理服務->編碼結果->返回結果。

基於網路伺服器的基本模型,Reactor衍生出了以下三種模型。

1.單執行緒模型

Reactor單執行緒模型,指的是所有的I/O操作都在同一個NIO執行緒上面完成,NIO執行緒的職責如下:

  • 作為NIO服務端,接收客戶端的TCP連線
  • 作為NIO客戶端,向服務端發起TCP連線
  • 讀取通訊對端的請求或者應答訊息
  • 向通訊對端傳送訊息請求或者應答訊息

Reactor執行緒負責多路分離套接字,Accept新連線,並分派請求到處理器鏈中。該模型 適用於處理器鏈中業務處理元件能快速完成的場景。不過,這種單執行緒模型不能充分利用多核資源,所以實際使用的不多。

2.多執行緒模型

Reactor多執行緒模型與單執行緒模型最大區別就是引入了執行緒池,負責非同步呼叫Handler處理業務,從而使其不會阻塞Reactor,它的流程如下:

  1. Reactor 物件通過 select 監控客戶端請求事件,收到事件後,通過 dispatch 進行分發
  2. 如果是建立連線請求,則由 Acceptor 通過 accept 處理連線請求,然後建立一個 Handler 物件處理完成連線後的各種事件
  3. 如果不是連線請求,則由 Reactor 物件會分發呼叫連線對應的 Handler 來處理
  4. Handler 只負責響應事件,不做具體的業務處理,通過 read 讀取資料後,會分發給後面的 Worker 執行緒池的某個執行緒處理業務
  5. Worker 執行緒池會分配獨立執行緒完成真正的業務,並將結果返回給 Handler
  6. Handler 收到響應後,通過 send 將結果返回給 Client

3.主從多執行緒模型

將連線請求控制代碼和資料傳輸控制代碼分開處理,使用單獨的Reactor來處理連線請求控制代碼,提高資料傳送控制代碼的處理能力。

服務端用於接收客戶端連線的不再是1個單獨的NIO執行緒,而是一個獨立的NIO執行緒池。Acceptor接收到客戶端TCP連線請求處理完成後(可能包含接入認證等),將新建立的SocketChannel註冊到I/O執行緒池(sub reactor執行緒池)的某個I/O執行緒上,由它負責SocketChannel的讀寫和編解碼工作。

著名的Netty即採用了此種模式

Linux中的I/O多路複用

linux實現I/O多路複用,主要涉及三個函式select、poll、epoll,目前前兩個已經基本不用了,但作為面試必考點還是應該知曉其原理。

幾個重要概念:

  1. 使用者空間和核心空間:為保護linux系統,將可能導致系統崩潰的指令定義為R0級別,僅允許在核心空間的程序使用,而普通應用則執行在使用者空間,當應用需要執行R0級別指令時需要由使用者態切換到核心態(極其耗時)。
  2. 檔案描述符(File descriptor):當應用程式請求核心開啟/新建一個檔案時,核心會返回一個檔案描述符用於對應這個開啟/新建的檔案,其fd本質上就是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。

select

int select(int maxfd1,			// 最大檔案描述符個數,傳輸的時候需要+1
		   fd_set *readset,	// 讀描述符集合
		   fd_set *writeset,	// 寫描述符集合
		   fd_set *exceptset,	// 異常描述符集合
		   const struct timeval *timeout);// 超時時間

select通過陣列儲存使用者關心的fd並通知核心,核心將fd集合拷貝至核心空間,遍歷後將就緒的fd集合返回

其缺點主要有以下幾點:

  1. 最大支援的fd_size為1024(有爭議?),遠遠不足以支撐高併發場景
  2. 每次涉及fd集合使用者態到核心態切換,開銷巨大
  3. 遍歷fd的時間複雜度為O(n),效能並不好

poll

int poll(struct pollfd *fds, 	        // fd的檔案集合改成自定義結構體,不再是陣列的方式,不受限於FD_SIZE
		 unsigned long nfds,     // 最大描述符個數
				int timeout);// 超時時間

struct pollfd {
	int fd;			// fd索引值
	short events;		// 輸入事件
	short revents;		// 結果輸出事件
};

poll技術與select技術實現邏輯基本一致,重要區別在於其使用連結串列的方式儲存描述符fd,不受陣列大小影響

說白了對於select的缺點poll只解決了第一點,依然存在很大效能問題

epoll

// 建立儲存epoll檔案描述符的空間,該空間也稱為“epoll例程”
int epoll_create(int size);    // 使用連結串列,現在已經棄用
int epoll_create(int flag);    // 使用紅黑樹的資料結構

// epoll註冊/修改/刪除 fd的操作
long epoll_ctl(int epfd,                        // 上述epoll空間的fd索引值
               int op,                         // 操作識別,EPOLL_CTL_ADD |  EPOLL_CTL_MOD  |  EPOLL_CTL_DEL
               int fd,                          // 註冊的fd
               struct epoll_event *event);      // epoll監聽事件的變化
struct epoll_event {
	__poll_t events;
	__u64 data;
} EPOLL_PACKED;

// epoll等待,與select/poll的邏輯一致
epoll_wait(int epfd,                            // epoll空間
           struct epoll_event *events,           // epoll監聽事件的變化
           int maxevents,                        // epoll可以儲存的最大事件數
        int timeout);                         // 超時時間

為了解決select&poll技術存在的兩個效能問題,epoll應運而生

  1. 通過epoll_create函式建立epoll空間(相當於一個容器管理),在核心中儲存需要監聽的資料集合,通過紅黑樹實現,插入刪除的時間複雜度為O(nlogn)
  2. 通過epoll_ctl函式來註冊對socket事件的增刪改操作,並且在核心底層通過利用mmap技術保證使用者空間與核心空間對該記憶體是具備可見性,直接通過指標引用的方式進行操作,避免了大記憶體資料的拷貝導致的空間切換效能問題
  3. 通過ep_poll_callback回撥函式,將就緒的fd插入雙向連結串列fd中,避免通過輪詢的方式獲取,事件複雜度為O(1)
  4. 通過epoll_wait函式的方式阻塞獲取rdlist中就緒的fd

EPOLL事件有兩種模型 Level Triggered (LT) 和 Edge Triggered (ET):

  1. LT(level triggered,水平觸發模式)是預設的工作方式,並且同時支援 block 和 non-block socket。在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式程式設計出錯誤可能性要小一點。
  2. ET(edge-triggered,邊緣觸發模式)是高速工作方式,只支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,並且不會再為那個檔案描述符傳送更多的就緒通知,等到下次有新的資料進來的時候才會再次出發就緒事件。

Don't let emotion cloud your judgment.
不要讓情緒影響你的判斷。

本文來自部落格園,作者:新人十三,轉載請註明原文連結:https://www.cnblogs.com/hystrix/p/15109264.html