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()替換。

whiletrue) {
    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/11925954.html
一個是比較實戰的socket+epoll,畢竟現實案例中哪有兩個父子進程間通訊這么簡單的應用場景。

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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM