[轉]: 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.
不要讓情緒影響你的判斷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章