select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。
select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實作會負責把資料從核心拷貝到使用者空間。
select的幾大缺點:
一、select
select本質上是通過設定或者檢查存放fd标志位的資料結構來進行下一步處理。這樣所帶來的缺點是:
1、select檔案描述符數量太小
select最大的缺陷就是單個程序所打開的FD是有一定限制的,它由FD_SETSIZE設定,預設值是1024。
一般來說這個數目和系統記憶體關系很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機預設是1024個,64位機預設是2048。
2、輪詢
對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低。
當套接字比較多的時候,每次select()都要通過周遊FD_SETSIZE個Socket來完成排程,不管哪個Socket是活躍的,都周遊一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
3、複制fd開銷大
需要維護一個用來存放大量fd的資料結構,這樣會使得使用者空間和核心空間在傳遞該結構時複制開銷大。
(1)每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大
(2)同時每次調用select都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大
單個程序能夠監視的檔案描述符的數量存在最大限制,通常是1024,當然可以更改數量,但由于select采用輪詢的方式掃描檔案描述符,檔案描述符數量越多,性能越差;(在linux核心頭檔案中,有這樣的定義:#define __FD_SETSIZE 1024)
核心 / 使用者空間記憶體拷貝問題,select需要複制大量的句柄資料結構,産生巨大的開銷;
select傳回的是含有整個句柄的數組,應用程式需要周遊整個數組才能發現哪些句柄發生了事件;
select的觸發方式是水準觸發,應用程式如果沒有完成對一個已經就緒的檔案描述符進行IO操作,那麼之後每次select調用還是會将這些檔案描述符通知程序。
二、poll
poll的實作和select非常相似,隻是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多。
poll将使用者傳入的數組拷貝到核心空間,然後查詢每個fd對應的裝置狀态,如果裝置就緒則在裝置等待隊列中加入一項并繼續周遊,如果周遊完所有fd後沒有發現就緒裝置,則挂起目前程序,直到裝置就緒或者主動逾時,被喚醒後它又要再次周遊fd。這個過程經曆了多次無謂的周遊。
它沒有最大連接配接數的限制,原因是它是基于連結清單來存儲的,但是同樣有一個缺點:
1)大量的fd的數組被整體複制于使用者态和核心位址空間之間,而不管這樣的複制是不是有意義。
2)poll還有一個特點是“水準觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
三、epoll
epoll是Linux多路服用IO接口select/poll的加強版,e對應的英文單詞就是enhancement,中文翻譯為增強,加強,提高,充實的意思。是以epoll模型會顯著提高程式在大量并發連接配接中隻有少量活躍的情況下的系統CPU使用率。
epoll支援水準觸發和邊緣觸發,最大的特點在于邊緣觸發,它隻告訴程序哪些fd剛剛變為就緒态,并且隻會通知一次。
epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,核心就會采用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。
epoll把使用者關心的檔案描述符上的時間放在核心的一個事件表中,無需像select和poll那樣每次調用都重複傳入檔案描述符集。
epoll在擷取事件的時候,無需周遊整個被監聽的檔案描述符集合,而是周遊那些被核心IO事件異步喚醒而加入ready隊列的描述符集合。
epoll既然是對select和poll的改進,那epoll是怎麼解決的呢?在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都隻提供了一個函數——select或者poll函數。
epoll的設計和實作與select完全不同。epoll通過在Linux核心中申請一個簡易的檔案系統(檔案系統一般用什麼資料結構實作?B+樹)。把原先的select/poll調用分成了3個部分:
1)調用epoll_create()建立一個epoll對象(在epoll檔案系統中為這個句柄對象配置設定資源)
2)調用epoll_ctl向epoll對象中添加這100萬個連接配接的套接字
3)調用epoll_wait收集發生的事件的連接配接
針對複制開銷,epoll的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中隻會拷貝一次。
對于輪詢問題,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待隊列中,而隻在epoll_ctl時把current挂一遍(這一遍必不可少)并為每個fd指定一個回調函數,當裝置就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒連結清單)。epoll_wait的工作實際上就是在這個就緒連結清單中檢視有沒有就緒的fd(利用schedule_timeout()實作睡一會,判斷一會的效果,和select實作中的第7步是類似的)。
針對數量限制,epoll沒有這個限制,它所支援的FD上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。
四、總結
(1)select,poll實作需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒連結清單,期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,調用回調函數,把就緒fd放入就緒連結清單中,并喚醒在epoll_wait中進入睡眠的程序。雖然都要睡眠和交替,但是select和poll在“醒着”的時候要周遊整個fd集合,而epoll在“醒着”的時候隻要判斷一下就緒連結清單是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
(2)select,poll每次調用都要把fd集合從使用者态往核心态拷貝一次,并且要把current往裝置等待隊列中挂一次,而epoll隻要一次拷貝,而且把current往等待隊列上挂也隻挂一次(在epoll_wait的開始,注意這裡的等待隊列并不是裝置等待隊列,隻是一個epoll内部定義的等待隊列)。這也能節省不少的開銷。
如此一來,要實作上面說是的場景,隻需要在程序啟動時建立一個epoll對象,然後在需要的時候向這個epoll對象中添加或者删除連接配接。同時,epoll_wait的效率也非常高,因為調用epoll_wait時,并沒有一股腦的向作業系統複制這100萬個連接配接的句柄資料,核心也不需要去周遊全部的連接配接。
參考
1、select、poll、epoll之間的差別總結[整理]
2、IO多路複用之select、poll、epoll詳解
3、深度了解select、poll和epoll(含有實作代碼)
4、select、poll、epoll的比較-select輪詢+sleep,epoll異步事件驅動高效
5、linux中的輪詢機制select/poll/epoll
6、libevent高性能網絡庫源碼分析——Reactor模式(二)
7、libevent學習筆記 一、基礎知識
8、網絡程式設計中Reactor與Proactor的概念及差別
9、IO設計模式:Reactor和Proactor對比
10、libevent和基于libevent的網絡程式設計
11、兩種高性能I/O設計模式(Reactor/Proactor)的比較
12、兩種高效的事件處理模型:Reactor模式和Proactor模式
13、Linux網絡程式設計---I/O複用模型之epoll
14、epoll模型和使用詳解(精髓)epoll - I/O event notification facility
15、樸素、Select、Poll和Epoll網絡程式設計模型實作和分析——模型比較
參考文獻:
https://www.jianshu.com/p/12c4081365df