深入理解Linux IO復用之epoll

2019年12月20日11:05:09 科技 1580


深入理解Linux IO復用之epoll - 天天要聞

作者:後端技術指南針 來自:後端技術指南針

0.概述

通過本篇文章將了解到以下內容:

  • I/O復用的定義和產生背景
  • Linux系統的I/O復用工具
  • epoll設計的基本構成
  • epoll高性能的底層實現
  • epoll的ET模式和LT模式


1.復用技術和I/O復用

  • 復用的概念

復用技術(multiplexing)並不是新技術而是一種設計思想,在通信和硬體設計中存在頻分復用、時分復用、波分復用、碼分復用等,在日常生活中復用的場景也非常多,因此不要被專業術語所迷惑。

從本質上來說,復用就是為了解決有限資源和過多使用者的不平衡問題,且此技術的理論基礎是資源的可釋放性。

  • 資源的可釋放性

舉個實際生活的例子:

不可釋放場景:ICU病房的呼吸機作為有限資源,病人一旦佔用且在未脫離危險之前是無法放棄佔用的,因此不可能幾個情況一樣的病人輪流使用。

可釋放場景:對於一些其他資源比如醫護人員就可以實現對多個病人的同時監護,理論上不存在一個病人佔用醫護人員資源不釋放的場景。

  • 理解IO復用

I/O的含義:在計算機領域常說的IO包括磁碟IO和網路IO,我們所說的IO復用主要是指網路IO,在Linux中一切皆文件,因此網路IO也經常用文件描述符FD來表示。

復用的含義:那麼這些文件描述符FD要復用什麼呢?在網路場景中復用的就是任務處理線程,所以簡單理解就是多個IO共用1個線程。

IO復用的可行性:IO請求的基本操作包括read和write,由於網路交互的本質性,必然存在等待,換言之就是整個網路連接中FD的讀寫是交替出現的,時而可讀可寫,時而空閑,所以IO復用是可用實現的。

綜上認為,IO復用技術就是協調多個可釋放資源的FD交替共享任務處理線程完成通信任務,實現多個fd對應1個任務處理線程。

現實生活中IO復用就像一隻邊牧管理幾百隻綿羊一樣:

深入理解Linux IO復用之epoll - 天天要聞


  • IO復用的設計原則和產生背景

高效IO復用機制要滿足:協調者消耗最少的系統資源、最小化FD的等待時間、最大化FD的數量、任務處理線程最少的空閑、多快好省完成任務等。

在網路並發量非常小的原始時期,即使per req per process地處理網路請求也可以滿足要求,但是隨著網路並發量的提高,原始方式必將阻礙進步,所以就刺激了IO復用機制的實現和推廣。

2.Linux中IO復用工具

在Linux中先後出現了select、poll、epoll等,FreeBSD的kqueue也是非常優秀的IO復用工具,kqueue的原理和epoll很類似,本文以Linux環境為例,並且不討論過多select和poll的實現機制和細節。

  • 開拓者select

select大約是2000年初出現的,其對外的介面定義:

深入理解Linux IO復用之epoll - 天天要聞


作為第一個IO復用系統調用,select使用一個宏定義函數按照bitmap原理填充fd,默認大小是1024個,因此對於fd的數值大於1024都可能出現問題,看下官方預警:

深入理解Linux IO復用之epoll - 天天要聞


也就是說當fd的數值大於1024時在將不可控,官方不建議超過1024,但是我們也無法控制fd的絕對數值大小,之前針對這個問題做過一些調研,結論是系統對於fd的分配有自己的策略,會大概率分配到1024以內,對此我並沒有充分理解,只是提及一下這個坑。

存在的問題:

  • 可協調fd數量和數值都不超過1024 無法實現高並發
  • 使用O(n)複雜度遍歷fd數組查看fd的可讀寫性 效率低
  • 涉及大量kernel和用戶態拷貝 消耗大
  • 每次完成監控需要再次重新傳入並且分事件傳入 操作冗餘

綜上可知,select以樸素的方式實現了IO復用,將並發量提高的最大K級,但是對於完成這個任務的代價和靈活性都有待提高。無論怎麼樣select作為先驅對IO復用有巨大的推動,並且指明了後續的優化方向,不要無知地指責select。

  • 繼承者epoll

epoll最初在2.5.44內核版本出現,後續在2.6.x版本中對代碼進行了優化使其更加簡潔,先後面對外界的質疑在後續增加了一些設置來解決隱藏的問題,所以epoll也已經有十幾年的歷史了。

在《Unix網路編程》第三版(2003年)還沒有介紹epoll,因為那個時代epoll還沒有出現,書中只介紹了select和poll,epoll對select中存在的問題都逐一解決,簡單來說epoll的優勢包括:

  • 對fd數量沒有限制(當然這個在poll也被解決了)
  • 拋棄了bitmap數組實現了新的結構來存儲多種事件類型
  • 無需重複拷貝fd 隨用隨加 隨棄隨刪
  • 採用事件驅動避免輪詢查看可讀寫事件

綜上可知,epoll出現之後大大提高了並發量對於C10K問題輕鬆應對,即使後續出現了真正的非同步IO,也並沒有(暫時沒有)撼動epoll的江湖地位,主要是因為epoll可以解決數萬數十萬的並發量,已經可以解決現在大部分的場景了,非同步IO固然優異,但是編程難度比epoll更大,權衡之下epoll仍然富有生命力。

3.epoll的基本實現

  • epoll的api定義

深入理解Linux IO復用之epoll - 天天要聞


  • epoll_create是在內核區創建一個epoll相關的一些列結構,並且將一個句柄fd返回給用戶態,後續的操作都是基於此fd的,參數size是告訴內核這個結構的元素的大小,類似於stl的vector動態數組,如果size不合適會涉及複製擴容,不過貌似4.1.2內核之後size已經沒有太大用途了;
  • epoll_ctl是將fd添加/刪除於epoll_create返回的epfd中,其中epoll_event是用戶態和內核態交互的結構,定義了用戶態關心的事件類型和觸發時數據的載體epoll_data;
  • epoll_wait是阻塞等待內核返回的可讀寫事件,epfd還是epoll_create的返回值,events是個結構體數組指針存儲epoll_event,也就是將內核返回的待處理epoll_event結構都存儲下來,maxevents告訴內核本次返回的最大fd數量,這個和events指向的數組是相關的;
  • epoll_event是用戶態需監控fd的代言人,後續用戶程序對fd的操作都是基於此結構的;


  • 通俗描述:

可能上面的描述有些抽象,不過其實很好理解,舉個現實中的例子:

  • epoll_create場景
  • 大學開學第一周,你作為班長需要幫全班同學領取相關物品,你在學生處告訴工作人員,我是xx學院xx專業xx班的班長,這時工作人員確定你的身份並且給了你憑證,後面辦的事情都需要用到(也就是調用epoll_create向內核申請了epfd結構,內核返回了epfd句柄給你使用);
  • epoll_ctl場景
  • 你拿著憑證在辦事大廳開始辦事,分揀辦公室工作人員說班長你把所有需要辦理事情的同學的學生冊和需要辦理的事情都記錄下來吧,於是班長開始在每個學生手冊單獨寫對應需要辦的事情:
  • 李明需要開實驗室許可權、孫大熊需要辦游泳卡......就這樣班長一股腦寫完並交給了工作人員(也就是告訴內核哪些fd需要做哪些操作);
  • epoll_wait場景
  • 你拿著憑證在領取辦公室門前等著,這時候廣播喊xx班長你們班孫大熊的游泳卡辦好了速來領取、李明實驗室許可權卡辦好了速來取....還有同學的事情沒辦好,所以班長只能繼續(也就是調用epoll_wait等待內核反饋的可讀寫事件發生並處理);
  • 官方DEMO

通過man epoll可以看到官方的demo:

深入理解Linux IO復用之epoll - 天天要聞


深入理解Linux IO復用之epoll - 天天要聞


深入理解Linux IO復用之epoll - 天天要聞


在epoll_wait時需要區分是主監聽線程fd的新連接事件還是已連接事件的讀寫請求,進而單獨處理。

4.epoll的底層實現

epoll底層實現最重要的兩個數據結構:epitem和eventpoll。

可以簡單的認為epitem是和每個用戶態監控IO的fd對應的,eventpoll是用戶態創建的管理所有被監控fd的結構,詳細的定義如下:

深入理解Linux IO復用之epoll - 天天要聞


深入理解Linux IO復用之epoll - 天天要聞


  • 底層調用過程

epoll_create會創建一個類型為struct eventpoll的對象,並返回一個與之對應文件描述符,之後應用程序在用戶態使用epoll的時候都將依靠這個文件描述符,而在epoll內部也是通過該文件描述符進一步獲取到eventpoll類型對象,再進行對應的操作,完成了用戶態和內核態的貫穿。

epoll_ctl底層主要調用epoll_insert實現操作:

  • 創建並初始化一個strut epitem類型的對象,完成該對象和被監控事件以及epoll對象eventpoll的關聯;
  • 將struct epitem類型的對象加入到epoll對象eventpoll的紅黑樹中管理起來;
  • 將struct epitem類型的對象加入到被監控事件對應的目標文件的等待列表中,並註冊事件就緒時會調用的回調函數,在epoll中該回調函數就是ep_poll_callback();
  • ovflist主要是暫態處理,比如調用ep_poll_callback()回調函數的時候發現eventpoll的ovflist成員不等於EP_UNACTIVE_PTR,說明正在掃描rdllist鏈表,這時將就緒事件對應的epitem加入到ovflist鏈表暫存起來,等rdllist鏈表掃描完再將ovflist鏈表中的元素移動到rdllist鏈表中;
  • 如圖展示了紅黑樹、雙鏈表、epitem之間的關係:


深入理解Linux IO復用之epoll - 天天要聞


注:rbr表示rb_root,rbn表示rb_node 上文給出了其在內核中的定義

  • epoll_wait的數據拷貝
常見錯誤觀點:epoll_wait返回時,對於就緒的事件,epoll使用的是共享內存的方式,即用戶態和內核態都指向了就緒鏈表,所以就避免了內存拷貝消耗網上抄來抄去的觀點

關於epoll_wait使用共享內存的方式來加速用戶態和內核態的數據交互,避免內存拷貝的觀點,並沒有得到2.6內核版本代碼的證實,並且關於這次拷貝的實現是這樣的:

深入理解Linux IO復用之epoll - 天天要聞


5.ET模式和LT模式

  • 簡單理解

默認採用LT模式,LT支持阻塞和非阻塞套,ET模式只支持非阻塞套接字,其效率要高於LT模式,並且LT模式更加安全。

LT和ET模式下都可以通過epoll_wait方法來獲取事件,LT模式下將事件拷貝給用戶程序之後,如果沒有被處理或者未處理完,那麼在下次調用時還會反饋給用戶程序,可以認為數據不會丟失會反覆提醒;

ET模式下如果沒有被處理或者未處理完,那麼下次將不再通知到用戶程序,因此避免了反覆被提醒,卻加強了對用戶程序讀寫的要求;

  • 深入理解

上面的簡單理解在網上隨便找一篇都會講到,但是LT和ET真正使用起來,還是存在一定難度的。

  • LT的讀寫操作

LT對於read操作比較簡單,有read事件就讀,讀多讀少都沒有問題,但是write就不那麼容易了,一般來說socket在空閑狀態時發送緩衝區一定是不滿的,假如fd一直在監控中,那麼會一直通知寫事件,不勝其煩。

所以必須保證沒有數據要發送的時候,要把fd的寫事件監控從epoll列表中刪除,需要的時候再加入回去,如此反覆。

天下沒有免費的午餐,總是無代價地提醒是不可能的,對應write的過度提醒,需要使用者隨用隨加,否則將一直被提醒可寫事件。

  • ET的讀寫操作

fd可讀則返回可讀事件,若開發者沒有把所有數據讀取完畢,epoll不會再次通知read事件,也就是說如果沒有全部讀取所有數據,那麼導致epoll不會再通知該socket的read事件,事實上一直讀完很容易做到。

若發送緩衝區未滿,epoll通知write事件,直到開發者填滿發送緩衝區,epoll才會在下次發送緩衝區由滿變成未滿時通知write事件。

ET模式下只有socket的狀態發生變化時才會通知,也就是讀取緩衝區由無數據到有數據時通知read事件,發送緩衝區由滿變成未滿通知write事件。

  • 一道面試題


使用Linux epoll模型的LT水平觸發模式,當socket可寫時,會不停的觸發socket可寫的事件,如何處理?網路流傳的騰訊面試題

這道題目對LT和ET考察比較深入,驗證了前文說的LT模式write問題。

普通做法

當需要向socket寫數據時,將該socket加入到epoll等待可寫事件。接收到socket可寫事件後,調用write()或send()發送數據,當數據全部寫完後, 將socket描述符移出epoll列表,這種做法需要反覆添加和刪除。

改進做法:

向socket寫數據時直接調用send()發送,當send()返回錯誤碼EAGAIN,才將socket加入到epoll,等待可寫事件後再發送數據,全部數據發送完畢,再移出epoll模型,改進的做法相當於認為socket在大部分時候是可寫的,不能寫了再讓epoll幫忙監控。

上面兩種做法是對LT模式下write事件頻繁通知的修復,本質上ET模式就可以直接搞定,並不需要用戶層程序的補丁操作。

  • ET模式的線程飢餓問題

如果某個socket源源不斷地收到非常多的數據,在試圖讀取完所有數據的過程中,有可能會造成其他的socket得不到處理,從而造成飢餓問題。

解決辦法:為每個已經準備好的描述符維護一個隊列,這樣程序就可以知道哪些描述符已經準備好了但是並沒有被讀取完,然後程序定時或定量的讀取,如果讀完則移除,直到隊列為空,這樣就保證了每個fd都被讀到並且不會丟失數據,流程如圖:

深入理解Linux IO復用之epoll - 天天要聞


  • EPOLLONESHOT設置

A線程讀完某socket上數據後開始處理這些數據,此時該socket上又有新數據可讀,B線程被喚醒讀新的數據,造成2個線程同時操作一個socket的局面 ,EPOLLONESHOT保證一個socket連接在任一時刻只被一個線程處理。

  • 兩種模式的選擇

通過前面的對比可以看到LT模式比較安全並且代碼編寫也更清晰,但是ET模式屬於高速模式,在處理大高並發場景使用得當效果更好,具體選擇什麼根據自己實際需要和團隊代碼能力來選擇,如果並發很高且團隊水平較高可以選擇ET模式,否則建議LT模式。

6.epoll的驚群問題

在2.6.18內核中accept的驚群問題已經被解決了,但是在epoll中仍然存在驚群問題,表現起來就是當多個進程/線程調用epoll_wait時會阻塞等待,當內核觸發可讀寫事件,所有進程/線程都會進行響應,但是實際上只有一個進程/線程真實處理這些事件。

在epoll官方沒有正式修復這個問題之前,Nginx作為知名使用者採用全局鎖來限制每次可監聽fd的進程數量,每次只有1個可監聽的進程,後來在Linux 3.9內核中增加了SO_REUSEPORT選項實現了內核級的負載均衡,Nginx1.9.1版本支持了reuseport這個新特性,從而解決驚群問題。

EPOLLEXCLUSIVE是在2016年Linux 4.5內核新添加的一個 epoll 的標識,Ngnix 在 1.11.3 之後添加了NGX_EXCLUSIVE_EVENT選項對該特性進行支持。EPOLLEXCLUSIVE標識會保證一個事件發生時候只有一個線程會被喚醒,以避免多偵聽下的驚群問題。

科技分類資訊推薦

新款長安逸動申報圖曝光!加裝電動擾流板 - 天天要聞

新款長安逸動申報圖曝光!加裝電動擾流板

汽車市場的競爭從未停歇,各車企不斷推陳出新以吸引消費者目光。近日,新款長安逸動的申報圖在網路上曝光,其中最引人注目的當屬新增的電動擾流板,這一設計不僅為車輛增添了運動氣息,更展現出長安汽車在產品創新上的不懈追求。
別克昂科威家族第180萬、君越第130萬整車下線 - 天天要聞

別克昂科威家族第180萬、君越第130萬整車下線

2025年5月26日,別克品牌迎來重要時刻,旗下SUV昂科威家族第180萬台整車在東嶽工廠下線,旗艦轎車君越車型第130萬台整車在浦東金橋基地下線。作為別克品牌布局SUV及轎車市場的兩大戰略車型,昂科威家族和君越多年來攜手共進,不斷鞏固別克在主流合資品牌中的領
風雲T10旗艦全能,以超值價格引領SUV新風尚 - 天天要聞

風雲T10旗艦全能,以超值價格引領SUV新風尚

在新能源汽車的浪潮中,奇瑞汽車再次以科技實力和創新精神,推出了備受矚目的旗艦車型——風雲T10。這款超長續航旗艦電混SUV,不僅集超強性能、超長續航、豪華配置與極致安全於一身,更有著令人難以置信的置換廠補一口價16.79萬元起的超值價格,它重新定義了旗艦SUV
中國一汽最強電混技術賦能,奔騰悅意07 9.98萬起全球上市 - 天天要聞

中國一汽最強電混技術賦能,奔騰悅意07 9.98萬起全球上市

5月26日,「生活從此 日新悅意——中國一汽奔騰悅意電混之夜暨奔騰悅意07全球上市」發布會盛大召開,中國一汽逐日動力BMP超級電混首款力作、超大大大電混SUV——奔騰悅意07正式發布。延續悅意序列「時空光影」美學,奔騰悅意07以「超大續航、超大性能、超大可靠」
榮膺四項殊榮!安凱客車以創新實力引領商用車高質量發展 - 天天要聞

榮膺四項殊榮!安凱客車以創新實力引領商用車高質量發展

5月25日,「運輸新生態高質量發展論壇暨2025中國商用車品牌營銷盛典」在河北雄安新區盛大啟幕。安凱客車以卓越表現攬獲「重大賽事交通服務突出貢獻單位」與「服務金口碑稱號」兩項殊榮;同時,旗下N12、E12S雙層觀光巴士,也憑藉出色性能與市場表現,分別摘得「公路
汽車級電芯!比亞迪兩輪、三輪車電池來了,終生不用再更換電池 - 天天要聞

汽車級電芯!比亞迪兩輪、三輪車電池來了,終生不用再更換電池

2025年5月,兩輪電動車電池市場發生了翻天覆地的變化,因為比亞迪兩輪車「刀片電池」來了,通過比亞迪刀片電池架構技術,把新能源汽車級的電池技術,下放到兩輪車領域,讓兩輪電動車實現了技術突破與市場革新,電動車終身不用再更換電池,讓我們一起來了解該電池的4大核心優