1. 程式人生 > >I/O多路複用模型

I/O多路複用模型

背景

在文章《unix網路程式設計》(12)五種I/O模型中提到了五種I/O模型,其中前四種:阻塞模型、非阻塞模型、訊號驅動模型、I/O複用模型都是同步模型;還有一種是非同步模型。

想寫一個系列的文章,介紹從I/O多路複用到非同步程式設計和RPC框架,整個演進過程,這一系列可能包括:

  1. I/O多路複用模型
  2. epoll介紹與使用
  3. Reactor和Proactor模型
  4. 為什麼需要非同步程式設計
  5. enable_shared_from_this用法分析
  6. 網路通訊庫和RPC

為什麼有多路複用?

多路複用技術要解決的是“通訊”問題,解決核心在於“同步事件分離器”(de-multiplexer),linux系統帶有的分離器select、poll、epoll網上介紹的比較多,大家可以看看這篇介紹的不錯的文章:我讀過的最好的epoll講解。通訊的一方想要知道另一方的狀態(以決定自己做什麼),有兩種方法: 一是輪詢,二是訊息通知。

輪詢

輪詢的一種典型的實現可能是這樣的:當然這裡的epoll_wait()也可以使用poll()或者select()替換。

while (true) {
    active_stream[] = epoll_wait(epollfd)
    for i in active_stream[] {
        read or write till
    }
}

輪詢方式主要存在以下不足:

  • 增加系統開銷。無論是任務輪詢還是定時器輪詢都需要消耗對應的系統資源。
  • 無法及時感知裝置狀態變化。在輪詢間隔內的裝置狀態變化只有在下次輪詢時才能被發現,這將無法滿足對實時性敏感的應用場合。
  • 浪費CPU資源。無論裝置是否發生狀態改變,輪詢總在進行。在實際情況中,大多數裝置的狀態改變通常不會那麼頻繁,輪詢空轉將白白浪費CPU時間片。

訊息通知

其實現方式通常是: "阻塞-通知"機制。阻塞會導致一個任務(task_struct,程序或者執行緒)只能處理一個"I/O流"或者類似的操作,要處理多個,就要多個任務(需要多個程序或執行緒),因此靈活性上又不如輪詢(一個任務足夠),很矛盾。

 

select、poll、epoll對比

矛盾的根源就是"一"和"多"的矛盾: 希望一個任務處理多個物件,同時避免處理阻塞-通知機制的內部細節。解決方案是多路複用(muliplex)。多路複用有3種基本方案,select()/poll()/epoll(),都是來解決這一矛盾的。

  • 通知代理: 使用者把需要關心的物件註冊給select()/poll()/epoll()函式。
  • 一對多: 所有的被關心的物件,只要有一個物件有了通知事件,select()/poll()/epoll()就會結束阻塞狀態。
  • 方便性: 使用者(程式設計師)不用再關心如何阻塞和被通知,以及哪些情況下會有通知產生。這件事情已經由上述幾個系統呼叫做了,使用者只需要實現"通知來了我該做什麼"。

 

那麼上面3個系統呼叫的區別是什麼呢?
第一個select(),結合了輪詢和阻塞兩種方式,沒有問題,每次有一個物件事件發生的時候,select()只是知道有事件發生了,具體是哪個物件發生的,不知道,需要從頭到尾輪詢一遍,複雜度是O(n)。poll函式相對select函式變化不大,只是提升了最大的可輪詢的物件個數。epoll函式把時間複雜度降到O(1)。

 

為什麼select慢而epoll效率高?
select()之所以慢,有幾個原因: select()的引數是一個FD陣列,意味著每次select呼叫,都是一次新的註冊-阻塞-回撥,每次select都要把一個數組從使用者空間拷貝到核心空間,核心檢測到某個物件狀態變化並寫入後,再從核心空間拷貝回用戶空間,select再把這個陣列讀取一遍,並返回。這個過程非常低效。

epoll的解決方案相當於是一種對select()的演算法優化: 它把select()一個函式做的事情分解成了3步,首先epoll_create()建立一個epollfd物件(相當於一個池子),然後所有被監聽的fd通過epoll_ctrl()註冊到這個池子,也就是為每個fd指定了一個內部的回撥函式(這樣,就沒有了每次呼叫時的來回拷貝,使用者空間的陣列到核心空間只有這一次拷貝)。epoll_wait阻塞等待。在核心態有一個和epoll_wait對應的函式呼叫,把就緒的fd,填入到一個就緒列表中,而epoll_wait讀取這個就緒列表,做到了快速返回(O(1))。

詳細的對比可以參考select、poll、epoll之間的區別總結:https://www.cnblogs.com/Anker/p/3265058.html?spm=ata.13261165.0.0.4ec468f3ruw05F

 

有了上面的原理介紹,這裡舉例來說明下epoll到底是怎麼使用的,加深理解。舉兩個例子:

一個是比較簡單的父子程序通訊的例子,單個小程式,不需要跑多個應用例項,不需要使用者輸入。https://www.cnblogs.com/goya/p/11903838.html
一個是比較實戰的socket+epoll,畢竟現實案例中哪有兩個父子程序間通訊這麼簡單的應用場景。

有了多路複用,難道還不夠?

有了I/O複用,有了epoll已經可以使伺服器併發幾十萬連線的同時,維持高TPS了,難道這還不夠嗎?答案是,技術層面足夠了,但在軟體工程層面卻是不夠的。例如,總要有個for迴圈去呼叫epoll,總來處理epoll的返回,這是每次都要重複的工作。for迴圈體裡面寫什麼----通知返回之後,做事情的程式最好能以一種回撥的機制,提供一個程式設計框架,讓程式更有結構一些。另一方面,如果希望每個事件通知之後,做的事情能有機會被代理到某個執行緒裡面去單獨執行,而執行緒完成的狀態又能通知回主任務,那麼"非同步"的進位制就必須被引入。

所以,還有兩個問題要解決,一是"程式設計框架",一是"非同步"。我們先看幾個目前流行的框架,大部分框架已經包含了某種非同步的機制。我們接下來的篇章將介紹“程式設計框架”和“非同步I/O模型