一文搞懂I/O多路複用及其技術

尹瑞星發表於2020-12-16

前言

​ 高效能是每個程式設計師的追求,無論寫一行程式碼還是做一個系統,都希望能夠達到高效能的效果。高效能架構設計主要集中在兩方面:

  • 儘量提升單伺服器的效能,將單伺服器的效能發揮到極致
  • 如果單伺服器無法支撐效能,設計伺服器叢集方案

​ 單伺服器高效能的關鍵之一就是伺服器採取的網路程式設計模型。伺服器如何管理連線,如何處理請求等。這兩個設計點最終都和作業系統的I/O模型及程式模型相關。

  • I/O模型:阻塞、非阻塞、同步、非同步
  • 程式模型:單程式、多程式、多執行緒。

​ 我們所說的I/O模型是指網路I/O模型,就是服務端如何管理連線,如何請求連線的措施,是用一個程式管理一個連線(PPC),還是一個執行緒管理一個連線(TPC),亦或者一個程式管理多個連線(Reactor)。

​ 因此IO多路複用中多路就是多個TCP連線(或多個Channel),複用就是指複用一個或少量執行緒,理解起來就是多個網路IO複用一個或少量執行緒來處理這些連線。

常見I/O模型

  • 同步阻塞IO(Blocking IO):即傳統IO模型
  • 同步非阻塞IO(Non-blocking IO):預設常見的socket都是阻塞的,非阻塞IO要求socket被設定成NONBLOCK
  • IO多路複用(IO Multiplexing):即經典的Reactor設計模式,也被稱為非同步阻塞IO,Java中的selector和linux中的epoll都是這種模型
  • 非同步IO(Asychronous IO):即Proactor設計模式,也被稱為非同步非阻塞IO

​ 同步和非同步的概念描述的是使用者執行緒與核心的互動方式,這裡所說的使用者程式/執行緒和核心是以傳輸層為分割線的,傳輸層以上是指使用者程式,傳輸層以下(包括傳輸層)是指核心(處理所有通訊細節,傳送資料,等待確認,給無序到達的資料排序等,這四層是作業系統核心的一部分)。同步是指使用者執行緒發起IO請求後需要等待或者輪詢核心IO操作,完成後才能繼續執行。非同步是指使用者執行緒發起IO請求後仍繼續執行,當核心IO操作完成後回通知使用者執行緒,或者呼叫使用者執行緒註冊的回撥函式。

​ 阻塞和非阻塞的概念描述的是使用者執行緒呼叫核心IO操作的方式,阻塞時指IO操作需要徹底完成後才能返回使用者空間,非阻塞時指IO操作被呼叫後立即返回給使用者一個狀態值,無需等待IO操作徹底完成。

同步阻塞IO

​ 同步阻塞IO是最簡單的IO模型,使用者執行緒在核心進行IO操作時被阻塞。使用者執行緒通過呼叫系統呼叫read發起IO讀操作,由使用者空間轉到核心空間。核心等到資料包到達後,然後將接受的資料拷貝到使用者空間,完成read操作。整個IO請求過程,使用者執行緒都是被阻塞的,對CPU利用率不夠

img

同步非阻塞IO

​ 在同步基礎上,將socket設定為NONBLOCK,這樣使用者執行緒可以在發起IO請求後立即返回。雖說可以立即返回,但並未讀到任何資料,使用者執行緒需要不斷的發起IO請求,直到資料到達後才能真正讀到資料,然後去處理。

​ 整個IO請求中,雖然可以立即返回,但是因為是同步的,為了等到資料,需要不斷的輪詢、重複請求,消耗了大量的CPU資源。因此,這種模型很少使用,實際用處不大。

img

IO多路複用

​ 不管是同步阻塞還是同步非阻塞,對系統效能的提升都是很小的。而通過複用可以使一個或一組執行緒(執行緒池)處理多個TCP連線。IO多路複用使用兩個系統呼叫(select/poll/epoll和recvfrom),blocking IO只呼叫了recvfrom。select/poll/epoll核心是可以同時處理多個connection,而不是更快,所以連線數不高的話,效能不一定比多執行緒+阻塞IO好。

​ select是核心提供的多路分離函式,使用它可以避免同步非阻塞IO中輪詢等待問題。

img

​ 使用者首先將需要進行IO操作的socket新增到select中,然後阻塞等待select系統呼叫返回。當資料到達時,socket被啟用,select函式返回,使用者執行緒正式發起read請求,讀取資料並繼續執行。

​ 這麼一看,這種方式和同步阻塞IO並沒有太大區別,甚至還多了新增監視socket以及呼叫select函式的額外操作,效率更差。但是使用select以後,使用者可以在一個執行緒內同時處理多個socket的IO請求,這就是它的最大優勢。使用者可以註冊多個socket,然後不斷呼叫select讀取被啟用的socket,即可達到同一個執行緒同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多執行緒方式才能達到這個目的。所以IO多路複用設計目的其實不是為了快,而是為了解決執行緒/程式數量過多對伺服器開銷造成的壓力。

select(socket); #向select註冊socket
while(true){
		sockets = select(); #獲取被啟用的socket
		for(socket in sockets){
				if(can_read(socket)){	#socket可讀,呼叫read讀取資料
						read(socket,buffer);
						process(buffer);
				}
		}
}

​ 雖然這種方式允許單執行緒內處理多個IO請求,但是每個IO請求的過程還是阻塞的(在select函式上阻塞),平均時間甚至比同步阻塞IO模型還要長。如果使用者執行緒只註冊自己感興趣的socket,然後去做自己的事情,等到資料到來時在進行處理,則可以提高CPU利用率。

img

​ 通過Reactor方式,使用者執行緒輪詢IO操作狀態的工作統一交給handle_events事件迴圈處理。使用者執行緒註冊事件處理器之後可以繼續執行做其他的工作(非同步),而Reactor執行緒負責呼叫核心的select函式檢查socket狀態。當有socket被啟用時,則通知相應的使用者執行緒(或執行使用者執行緒的回撥函式),執行handel_envent進行資料的讀取、處理工作。

​ 由於select函式是阻塞的,因此多路IO複用模型就被稱為非同步阻塞IO模型,這裡阻塞不是指socket。因為使用IO多路複用時,socket都設定NONBLOCK,不過不影響,因為使用者發起IO請求時,資料已經到達了,使用者執行緒一定不會被阻塞。

​ IO多路複用是最常用的IO模型,但其非同步程度還不徹底,因為它使用了回阻塞執行緒的select系統呼叫。因此IO多路複用只能稱為非同步阻塞IO,而非真正的非同步IO。

附:Reactor設計模式

img

非同步非阻塞IO

​ 在IO多路複用模型中,事件迴圈檔案控制程式碼的狀態事件通知給使用者執行緒,由使用者執行緒自行讀取資料、處理資料。而非同步IO中,當使用者執行緒收到通知時候,資料已經被核心讀取完畢,並放在了使用者執行緒指定的緩衝區內,核心在IO完成後通知使用者執行緒直接使用就行了。因此這種模型需要作業系統更強的支援,把read操作從使用者執行緒轉移到了核心。

​ 相比於IO多路複用模型,非同步IO並不十分常用,不少高效能併發服務程式使用IO多路複用+多執行緒任務處理的架構基本可以滿足需求。不過最主要原因還是作業系統對非同步IO的支援並非特別完善,更多的採用IO多路複用模擬非同步IO方式(IO事件觸發時不直接通知使用者執行緒,而是將資料讀寫完畢後放到使用者指定的緩衝區)。

select、poll、epoll詳解

​ select,poll,epoll都是IO多路複用的機制。I/O多路複用就是通過一種機制,一個程式可以監視多個描述符(socket),一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。雖說IO多路複用被稱為非同步阻塞IO,但select,poll,epoll本質上都是同步IO,因為它們都需要在續寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而真正意義上的非同步IO無需自己負責進行讀寫。

select

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

​ select函式監視的檔案描述符有三類,readfds,writefds,exceptfds。呼叫後函式會阻塞,直到有描述符就緒(有資料讀、寫、或者有except),或者超時(timeout指定時間,如果立即返回設定null),函式返回。當select函式返回後,可以通過便利fdset,來找到就緒的描述符。

​ 優點:良好的跨平臺性。

​ 缺點:單個程式能夠監視的檔案描述符的數量存在最大限制,在Linux上為1024,可以通過修改巨集定義甚至重新編譯核心的方式提升這一限制,但這樣會造成效率的降低。

poll

int poll(struct poll *fds, unsigned int nfds, int timeout);
struct pollfd{
  int fd;
  short events;
  short revents;
};

​ 與select使用三個點陣圖來表示fdset,poll使用一個pollfd的指標實現。pollfd結構包含了要監視的event和發生的event,不在使用select引數傳值的方式。同時pollfd並沒有最大數量的限制(但數量過大效能也會下降)。和select一樣,poll返回後,需要輪詢pollfd來或許就緒的描述符。

epoll

​ epoll是select和poll的增強版本,相比於前兩者,它更加的靈活,沒有描述符的限制。epoll使用一個檔案描述符管理多個描述符,將使用者關係的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需要一次。

參考連結:

https://blog.csdn.net/sehanlingfeng/article/details/78920423

https://www.cnblogs.com/wlwl/p/10293057.html

https://www.cnblogs.com/natian-ws/p/10785649.html

https://segmentfault.com/a/1190000003063859

《架構修煉之道》

《從零開始學架構》

相關文章