天天看點

面試官:select、poll、epoll有何差別?我:阿巴阿巴...

前言

很多朋友對 select、poll、epoll很頭疼,因為搞不清三者的差別和内在邏輯,是以今天就來給大家詳細說說,一家之言,不對之處還請指正。

大家可以進群973961276一起聊聊技術吹吹牛,每周都會有幾次抽獎送專業書籍的活動,獎品不甚值錢,但也算個彩頭。

缺乏項目實戰經驗和想跳槽漲薪或是自我提升的朋友看這裡>>c/c++ 項目實戰/背景伺服器開發進階架構師

(1)、select==>時間複雜度O(n)

它僅僅知道了,有I/O事件發生了,卻并不知道是哪那幾個流(可能有一個,多個,甚至全部),我們隻能無差别輪詢所有流,找出能讀出資料,或者寫入資料的流,對他們進行操作。是以select具有O(n)的無差别輪詢複雜度,同時處理的流越多,無差别輪詢時間就越長。

(2)、poll==>時間複雜度O(n)

poll本質上和select沒有差別,它将使用者傳入的數組拷貝到核心空間,然後查詢每個fd對應的裝置狀态, 但是它沒有最大連接配接數的限制,原因是它是基于連結清單來存儲的.

(3)、epoll==>時間複雜度O(1)

epoll可以了解為event poll,不同于忙輪詢和無差别輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。是以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(複雜度降低到了O(1))

select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實作會負責把資料從核心拷貝到使用者空間。  

epoll跟select都能提供多路I/O複用的解決方案。在現在的Linux核心裡有都能夠支援,其中epoll是Linux所特有,而select則應該是POSIX所規定,一般作業系統均有實作

select:

select本質上是通過設定或者檢查存放fd标志位的資料結構來進行下一步處理。這樣所帶來的缺點是:

1、 單個程序可監視的fd數量被限制,即能監聽端口的大小有限。

      一般來說這個數目和系統記憶體關系很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機預設是1024個。64位機預設是2048.

2、 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低:

       當套接字比較多的時候,每次select()都要通過周遊FD_SETSIZE個Socket來完成排程,不管哪個Socket是活躍的,都周遊一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。

3、需要維護一個用來存放大量fd的資料結構,這樣會使得使用者空間和核心空間在傳遞該結構時複制開銷大

poll:

poll本質上和select沒有差別,它将使用者傳入的數組拷貝到核心空間,然後查詢每個fd對應的裝置狀态,如果裝置就緒則在裝置等待隊列中加入一項并繼續周遊,如果周遊完所有fd後沒有發現就緒裝置,則挂起目前程序,直到裝置就緒或者主動逾時,被喚醒後它又要再次周遊fd。這個過程經曆了多次無謂的周遊。

它沒有最大連接配接數的限制,原因是它是基于連結清單來存儲的,但是同樣有一個缺點:

1、大量的fd的數組被整體複制于使用者态和核心位址空間之間,而不管這樣的複制是不是有意義。                   

2、poll還有一個特點是“水準觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。

epoll:

epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是預設的模式,ET是“高速”模式。LT模式下,隻要這個fd還有資料可讀,每次 epoll_wait都會傳回它的事件,提醒使用者程式去操作,而在ET(邊緣觸發)模式中,它隻會提示一次,直到下次再有資料流入之前都不會再提示了,無 論fd中是否還有資料可讀。是以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的傳回值小于請求值,或者 遇到EAGAIN錯誤。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,核心就會采用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。

如果采用EPOLLLT模式的話,系統中一旦有大量你不需要讀寫的就緒檔案描述符,它們每次調用epoll_wait都會傳回,這樣會大大降低處理程式檢索自己關心的就緒檔案描述符的效率.。而采用EPOLLET這種邊沿觸發模式的話,當被監控的檔案描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把資料全部讀寫完(如讀寫緩沖區太小),那麼下次調用epoll_wait()時,它不會通知你,也就是它隻會通知你一次,直到該檔案描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水準觸發效率高,系統不會充斥大量你不關心的就緒檔案描述符

epoll的優點:

1、沒有最大并發連接配接的限制,能打開的FD的上限遠大于1024(1G的記憶體上能監聽約10萬個端口);

2、效率提升,不是輪詢的方式,不會随着FD數目的增加效率下降。隻有活躍可用的FD才會調用callback函數;

即Epoll最大的優點就在于它隻管你“活躍”的連接配接,而跟連接配接總數無關,是以在實際的網絡環境中,Epoll的效率就會遠遠高于select和poll。

3、 記憶體拷貝,利用mmap()檔案映射記憶體加速與核心空間的消息傳遞;即epoll使用mmap減少複制開銷。

select、poll、epoll 差別總結:

1、支援一個程序所能打開的最大連接配接數

select

單個程序所能打開的最大連接配接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是3232,同理64位機器上FD_SETSIZE為3264),當然我們可以對進行修改,然後重新編譯核心,但是性能可能會受到影響,這需要進一步的測試。

poll

poll本質上和select沒有差別,但是它沒有最大連接配接數的限制,原因是它是基于連結清單來存儲的

epoll

雖然連接配接數有上限,但是很大,1G記憶體的機器上可以打開10萬左右的連接配接,2G記憶體的機器可以打開20萬左右的連接配接

2、FD劇增後帶來的IO效率問題

因為每次調用時都會對連接配接進行線性周遊,是以随着FD的增加會造成周遊速度慢的“線性下降性能問題”。

同上

因為epoll核心中實作是根據每個fd上的callback函數來實作的,隻有活躍的socket才會主動調用callback,是以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。

3、 消息傳遞方式

核心需要将消息傳遞到使用者空間,都需要核心拷貝動作

epoll通過核心和使用者空間共享一塊記憶體來實作的。

總結:

綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。

1、表面上看epoll的性能最好,但是在連接配接數少并且連接配接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。

2、select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善 

  今天對這三種IO多路複用進行對比,參考網上和書上面的資料,整理如下:

1、select實作

select的調用過程如下所示:

面試官:select、poll、epoll有何差別?我:阿巴阿巴...

(1)使用copy_from_user從使用者空間拷貝fd_set到核心空間

(2)注冊回調函數__pollwait

(3)周遊所有fd,調用其對應的poll方法(對于socket,這個poll方法是sock_poll,sock_poll根據情況會調用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll為例,其核心實作就是__pollwait,也就是上面注冊的回調函數。

(5)__pollwait的主要工作就是把current(目前程序)挂到裝置的等待隊列中,不同的裝置有不同的等待隊列,對于tcp_poll來說,其等待隊列是sk->sk_sleep(注意把程序挂到等待隊列中并不代表程序已經睡眠了)。在裝置收到一條消息(網絡裝置)或填寫完檔案資料(磁盤裝置)後,會喚醒裝置等待隊列上睡眠的程序,這時current便被喚醒了。

(6)poll方法傳回時會傳回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set指派。

(7)如果周遊完所有的fd,還沒有傳回一個可讀寫的mask掩碼,則會調用schedule_timeout是調用select的程序(也就是current)進入睡眠。當裝置驅動發生自身資源可讀寫後,會喚醒其等待隊列上睡眠的程序。如果超過一定的逾時時間(schedule_timeout指定),還是沒人喚醒,則調用select的程序會重新被喚醒獲得CPU,進而重新周遊fd,判斷有沒有就緒的fd。

(8)把fd_set從核心空間拷貝到使用者空間。

select的幾大缺點:

(1)每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大

(2)同時每次調用select都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大

(3)select支援的檔案描述符數量太小了,預設是1024

2 poll實作

  poll的實作和select非常相似,隻是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多,管理多個描述符也是進行輪詢,根據描述符的狀态進行處理,但是poll沒有最大檔案描述符數量的限制。poll和select同樣存在一個缺點就是,包含大量檔案描述符的數組被整體複制于使用者态和核心的位址空間之間,而不論這些檔案描述符是否就緒,它的開銷随着檔案描述符數量的增加而線性增大。

3、epoll

  epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都隻提供了一個函數——select或者poll函數。而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll句柄;epoll_ctl是注冊要監聽的事件類型;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内部定義的等待隊列)。這也能節省不少的開銷。 

繼續閱讀