select、poll和epoll

select

select最早於1983年出現在4.2BSD中,它通過一個select()系統調用來監視多個文件描述符的數組,當select()返回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程可以獲得這些文件描述符從而進行後續的讀寫操作。

select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點,事實上從現在看來,這也是它所剩不多的優點之一。

select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般爲1024,不過可以通過修改宏定義甚至重新編譯內核的方式提升這一限制。

另外,select()所維護的存儲大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增長。同時,由於網絡響應時間的延遲使得大量TCP連接處於非活躍狀態,但調用select()會對所有socket進行一次線性掃描,所以這也浪費了一定的開銷。

poll

poll在1986年誕生於System V Release 3,它和select在本質上沒有多大差別,但是poll沒有最大文件描述符數量的限制。

poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體複製於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增加而線性增大。

另外,select()和poll()將就緒的文件描述符告訴進程後,如果進程沒有對其進行IO操作,那麼下次調用select()和poll()的時候將再次報告這些文件描述符,所以它們一般不會丟失就緒的消息,這種方式稱爲水平觸發(Level Triggered)。

epoll

直到Linux2.6纔出現了由內核直接支持的實現方法,那就是epoll,它幾乎具備了之前所說的一切優點,被公認爲Linux2.6下性能最好的多路I/O就緒通知方法。

epoll可以同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變爲就緒狀態,它只說一遍,如果我們沒有採取行動,那麼它將不會再次告知,這種方式稱爲邊緣觸發),理論上邊緣觸發的性能要更高一些,但是代碼實現相當複雜。

epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這裏也使用了內存映射(mmap)技術,這樣便徹底省掉了這些文件描述符在系統調用時複製的開銷。

另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。

區別

本質而言,poll和select的共同點就是,對全部指定設備做一次poll,當然這往往都是還沒有就緒的,那就會通過回調函數把當前進程註冊到設備的等待隊列,如果所有設備返回的掩碼都沒有顯示任何的事件觸發,就去掉回調函數的函數指針,進入有限時的睡眠狀態,再恢復和不斷做poll,再作有限時的睡眠,直到其中一個設備有事件觸發爲止。只要有事件觸發,系統調用返回,回到用戶態,用戶就可以對相關的fd作進一步的讀或者寫操作了。當然,這個時候還不是所有的設備都就緒的喔,那就得不斷地poll或者select了,而做一次這樣的系統調用都得輪詢所有的設備,次數是設備數*(睡眠次數-1),也就是時間複雜度是O(n),還得做幾次O(n)呢。可見,對於現在普遍的服務器程序,需要同時併發監聽數千個連接,並且連接需要重複使用的情況,poll和select就存在這樣的性能瓶頸。另外,數千個設備fd在每次調用時,都需要將其從用戶空間複製到內核空間,這裏的開銷不可忽略。

 

poll和select放在一起,是因爲其機制一致,而參數和數據結構就略有不同。select一次性傳入三組作用於不同信道的設備fd,分別是輸入,輸出和錯誤異常。各組的fd期待各組所特有的,由代碼指定的一組事件,如輸入信道期待輸入就緒,輸入掛起和錯誤等事件。 然後,select就挑選調用者關心的fd做poll文件操作,檢測返回的掩碼,看看是否有fd所屬信道感興趣的事件,比如看看這個屬於輸出信道的fd有沒有輸出就緒等一系列的事件發生,一樣地,如果有一個fd發生感興趣事件就返回調用了。select,爲了同時處理三組使用不同的事件判斷規則的fd,採用了位圖的方式表示,一組一個位圖,位長度是當中最大的fd值,上限是1024,三組就是3072,而且這還只是傳入的位圖,還有一樣大小的傳出的位圖。當fd數越來越多時,所需的存儲開銷比較大。

 

既然,一組fd處理起來比較粗放,那就各個fd自己準備好了。poll()系統調用是System V的多元I/O解決方案。它有三個參數,第一個是pollfd結構的數組指針,也就是指向一組fd及其相關信息的指針,因爲這個結構包含的除了fd,還有期待的事件掩碼和返回的事件掩碼,實質上就是將select的中的fd,傳入和傳出參數歸到一個結構之下,也不再把fd分爲三組,也不再硬性規定fd感興趣的事件,這由調用者自己設定。這樣,不使用位圖來組織數據,也就不需要位圖的全部遍歷了。按照一般隊列地遍歷,每個fd做poll文件操作,檢查返回的掩碼是否有期待的事件,以及做是否有掛起和錯誤的必要性檢查,如果有事件觸發,就可以返回調用了。

 

回到poll和select的共同點,面對高併發多連接的應用情境,它們顯現出原來沒有考慮到的不足,雖然poll比起select又有所改進了。除了上述的關於每次調用都需要做一次從用戶空間到內核空間的拷貝,還有這樣的問題,就是當處於這樣的應用情境時,poll和select會不得不多次操作,並且每次操作都很有可能需要多次進入睡眠狀態,也就是多次全部輪詢fd,我們應該怎麼處理一些會出現重複而無意義的操作。

 

這些重複而無意義的操作有:1、從用戶到內核空間拷貝,既然長期監視這幾個fd,甚至連期待的事件也不會改變,那拷貝無疑就是重複而無意義的,我們可以讓內核長期保存所有需要監視的fd甚至期待事件,或者可以再需要時對部分期待事件進行修改;2、將當前線程輪流加入到每個fd對應設備的等待隊列,這樣做無非是哪一個設備就緒時能夠通知進程退出調用,聰明的開發者想到,那就找個“代理”的回調函數,代替當前進程加入fd的等待隊列好了(這也是我後來才總結出來,Linux的等待隊列,實質上是回調函數隊列吧,也可以使用宏來將當前進程“加入”等待隊列,其實就是將喚醒當前進程的回調函數加入隊列)。這樣,像poll系統調用一樣,做poll文件操作發現尚未就緒時,它就調用傳入的一個回調函數,這是epoll指定的回調函數,它不再像以前的poll系統調用指定的回調函數那樣,而是就將那個“代理”的回調函數加入設備的等待隊列就好了,這個代理的回調函數就自己乖乖地等待設備就緒時將它喚醒,然後它就把這個設備fd放到一個指定的地方,同時喚醒可能在等待的進程,到這個指定的地方取fd就好了。我們把1和2結合起來就可以這樣做了,只拷貝一次fd,一旦確定了fd就可以做poll文件操作,如果有事件當然好啦,馬上就把fd放到指定的地方,而通常都是沒有的,那就給這個fd的等待隊列加一個回調函數,有事件就自動把fd放到指定的地方,當前進程不需要再一個個poll和睡眠等待了。

 

epoll機制就是這樣改進的了。誠然,fd少的時候,當前進程一個個地等問題不大,可是現在和尚多了,方丈就不好管了。以前設備事件觸發時,只負責喚醒當前進程就好了,而當前進程也只能傻傻地在poll裏面等待或者循環,再來一次poll,也不知道這個由設備提供的poll性能如何,能不能檢查出當前進程已經在等待了就立即返回,當然,我也不明白爲什麼做了一遍的poll之後,去掉回調函數指針了,還得再做,不是說好了會去喚醒進程的嗎?

 

現在就讓事件觸發回調函數多做一步。本來設備還沒就緒就調用一個回調函數了,現在再在這個回調函數裏面做一個註冊另一個回調函數的操作,目的就是使得設備事件觸發多走一步,不僅僅是喚醒當前進程,還要把自己的fd放到指定的地方。就像收本子的班長,以前得一個個學生地去問有沒有本子,如果沒有,它還得等待一段時間而後又繼續問,現在好了,只走一次,如果沒有本子,班長就告訴大家去那裏交本子,當班長想起要取本子,就去那裏看看或者等待一定時間後離開,有本子到了就叫醒他,然後取走。這個道理很簡單,就是老師和班幹們常說的,大家多做一點工作,我的工作就輕鬆很多了,尤其是需要管理的東西越來越多時。

epoll由三個系統調用組成,分別是epoll_create,epoll_ctl和epoll_wait。epoll_create用於創建和初始化一些內部使用的數據結構;epoll_ctl用於添加,刪除或者修改指定的fd及其期待的事件,epoll_wait就是用於等待任何先前指定的fd事件。


select,poll,epoll簡介

select

select本質上是通過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是:

1 單個進程可監視的fd數量被限制

2 需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大

3 對socket進行掃描時是線性掃描

poll

poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。

它沒有最大連接數的限制,原因是它是基於鏈表來存儲的,但是同樣有一個缺點:

大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣的複製是不是有意義。

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

epoll

epoll支持水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴進程哪些fd剛剛變爲就需態,並且只會通知一次。

在前面說到的複製問題上,epoll使用mmap減少複製開銷。

還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,內核就會採用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知

1 支持一個進程所能打開的最大連接數

select

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

poll

poll本質上和select沒有區別,但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的

epoll

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

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

select

因爲每次調用時都會對連接進行線性遍歷,所以隨着FD的增加會造成遍歷速度慢的“線性下降性能問題”。

poll

同上

epoll

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

3 消息傳遞方式

select

內核需要將消息傳遞到用戶空間,都需要內核拷貝動作

poll

同上

epoll

epoll通過內核和用戶空間共享一塊內存來實現的。

綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。表面上看epoll的性能最好,但是在連接數少並且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調




發佈了121 篇原創文章 · 獲贊 34 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章