1. 程式人生 > >I/O多路轉接之select、poll、epoll

I/O多路轉接之select、poll、epoll

I/O多路轉接之select

系統提供select函式來實現多路複用輸入/輸出模型。select系統呼叫是用來讓我們的程式監視多個檔案控制代碼的狀態變化的。程式會停在select這裡等待,直到被監視的檔案控制代碼有一個或多個發生了狀態改變。關於檔案控制代碼,其實就是一個整數,我們最熟悉的控制代碼是0、1、2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。0、1、2是整數表示的,對應的FILE *結構的表示就是stdin、stdout、stderr。

這裡寫圖片描述

引數nfds是需要監視的最大的檔案描述符值+1;
rdset,wrset,exset分別對應於需要檢測的可讀檔案描述符的集合,可寫檔案描述符的集 合及異常檔案描述符的集合。
struct timeval結構用於描述一段時間長度,如果在這個時間內,需要監視的描述符沒有事件發生則函式返回,返回值為0。

下面的巨集提供了處理這三種描述片語的方式:

(1)FD_CLR(inr fd,fd_set* set);用來清除描述片語set中相關fd 的位
(2)FD_ISSET(int fd,fd_set *set);用來測試描述片語set中相關fd 的位是否為真
(3)FD_SET(int fd,fd_set*set);用來設定描述片語set中相關fd的位
(4)FD_ZERO(fd_set *set);用來清除描述片語set的全部位

引數timeout為結構timeval,用來設定select()的等待時間,其結構定義如下:

這裡寫圖片描述

如果引數timeout設為:
(1)NULL:則表示select()沒有timeout,select將一直被阻塞,直到某個檔案描述符上發生了事件。
(2)0:僅檢測描述符集合的狀態,然後立即返回,並不等待外部事件的發生。
(3)特定的時間值:如果在指定的時間段裡沒有事件發生,select將超時返回。

函式返回值:

(1)執行成功則返回檔案描述詞狀態已改變的個數
(2)如果返回0代表在描述詞狀態改變前已超過timeout時間,沒有返回;
(3)當有錯誤發生時則返回-1,錯誤原因存於errno,此時引數readfds,writefds,exceptfds和timeout的值變成不可預測。錯誤值可能為:
a)EBADF 檔案描述詞為無效的或該檔案已關閉
b)EINTR 此呼叫被訊號所中斷
c)EINVAL 引數n 為負值。
d)ENOMEM 核心記憶體不足

常見的程式片段如下:

fs_set readset; //定義讀檔案描述符的集合
FD_SET(fd,&readset); //將fd加入讀檔案描述符集合中
select(fd+1,&readset,NULL,NULL,NULL);//監視多個檔案控制代碼的狀態變化
if(FD_ISSET(fd,readset)){…⋯…⋯}//判斷fd是否在讀檔案描述符集合中

理解select模型:

理解select模型的關鍵在於理解fd_set,為說明⽅方便,取fd_set長度為1位元組,fd_set中的每一bit可以對應一個檔案描述符fd。則1位元組長的fd_set最大可以對應8個fd。
(1)執行fd_set set; FD_ZERO(&set);則set用位表示是0000,0000。
(2)若fd=5,執行FD_SET(fd,&set);後set變為0001,0000(第5位置為1)
(3)若再加入fd=2,fd=1,則set變為0001,0011
(4)執行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空。

基於上面的討論,可以輕鬆得出select模型的特點:

(1)可監控的檔案描述符個數取決與sizeof(fd_set)的值。我這邊服務 器上sizeof(fd_set)=512,每bit表示一個檔案描述符,則我伺服器上支援的最大檔案描述符是512*8=4096。據說可調,另有說雖 然可調,但調整上限受於編譯核心時的變數值。本人對調整fd_set的大小不太感興趣,參考http://www.cppblog.com/CppExplore/archive/2008/03/21/45061.html中的模型2
(1)可以有效突破select可監控的檔案描述符上限。
(2)將fd加入select監控集的同時,還要再使用一個數據結構array儲存放到select監控集中的fd,一是用於再select 返回後,array作為源資料和fd_set進行FD_ISSET判斷。二是select返回後會把以前加入的但並無事件發生的fd清空,則每次開始 select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用於select的第一個 引數。
(3)可見select模型必須在select前迴圈array(加fd,取maxfd),select返回後迴圈array(FD_ISSET判斷是否有時間發生)。

select使用事例一:檢測標準輸入輸出

這裡寫圖片描述
這裡寫圖片描述

說明:
1. 當只檢測檔案描述符0(標準輸入)時,因為輸入條件只有在你有輸入資訊的時候,才成立,所以如果一直不輸入,就會產生超時資訊。
2. 當只檢測檔案描述符1(標準輸出)時,因為輸出條件始終成熟,所以不會超時而是直接返回,讓write_fd寫入條件就緒,直接輸出資訊。
3. 當兩者同時檢測的時候,只要有任何一個滿足,就會直接返回。

select使用事例二:使用select編寫網路伺服器

server:

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

client:

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

I/O多路轉接之poll

不同與select使用三個點陣圖來表示三個fdset的方式,poll使用一個 pollfd的指標實現。

這裡寫圖片描述
這裡寫圖片描述

pollfd結構包含了要監視的event和發生的event,不再使用select“引數-值”傳遞的方式。同時,pollfd並沒有最大數量限制(但是數量過大後效能也是會下降)。 和select函式一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。
從上面看,select和poll都需要在返回後,通過遍歷檔案描述符來獲取已經就緒的socket。事實上,同時連線的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。

事例一,使用poll監控輸入輸出

這裡寫圖片描述
這裡寫圖片描述

事例二,搭建poll伺服器參見select寫法,此處不再詳細講解

I/O多路轉接之epoll

什麼是epoll
epoll是什麼?按照man手冊的說法:是為處理大批量控制代碼而作了改進的poll。被公認為Linux2.6下效能最好的多路I/O就緒通知方法。

epoll的相關係統呼叫
epoll只有epoll_create,epoll_ctl,epoll_wait 3個系統呼叫。

(1)

這裡寫圖片描述
建立一個epoll的控制代碼。自從linux2.6.8之後,size引數是被忽略的。需要注意的是,當建立好epoll控制代碼後,它就是會佔用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close()關閉,否則可能導致fd被耗盡。

(2)

這裡寫圖片描述
epoll的事件註冊函式,它不同於select()是在監聽事件時告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。
第一個引數是epoll_create()的返回值。
第二個引數表示動作,用三個巨集來表示:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個引數是需要監聽的fd。
第四個引數是告訴核心需要監聽什麼事,struct epoll_event結構如下:

這裡寫圖片描述

events可以是以下幾個巨集的集合:
EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被結束通話;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡

(3)

這裡寫圖片描述

收集在epoll監控的事件中已經發送的事件。引數events是分配好的epoll_event結構體陣列,epoll將會把發生的事件賦值到events陣列中(events不可以是空指標,核心只負責把資料複製到這個events陣列中,不會去幫助我們在使用者態中分配記憶體)。maxevents告之核心這個
events有多大,這個 maxevents的值不能大於建立epoll_create()時的size,引數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。如果函式呼叫成功,返回對應I/O上已準備好的檔案描述符數目,如返回0表示已超時。

epoll工作原理

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

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

epoll對檔案描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是預設模式,LT模式與ET模式的區別如下:

  LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程式,應用程式可以不立即處理該事件。下次呼叫epoll_wait時,會再次響應應用程式並通知此事件。

  ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程式,應用程式必須立即處理該事件。如果不處理,下次呼叫epoll_wait時,不會再次響應應用程式並通知此事件。

  ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套介面,以避免由於一個檔案控制代碼的阻塞讀/阻塞寫操作把處理多個檔案描述符的任務餓死。

詳解LT與ET模式:

假如有這樣一個例子:
1. 我們已經把一個用來從管道中讀取資料的檔案控制代碼(RFD)新增到epoll描述符
2. 這個時候從管道的另一端被寫入了2KB的資料
3. 呼叫epoll_wait(2),並且它會返回RFD,說明它已經準備好讀取操作
4. 然後我們讀取了1KB的資料
5. 呼叫epoll_wait(2)……

Edge Triggered工作模式:

如果我們在第1步將RFD新增到epoll描述符的時候使用了EPOLLET標誌,那麼在第5步呼叫epoll_wait(2)之後將有可能會掛起,因為剩餘的資料還存在於檔案的輸入緩衝區內,而且資料發出端還在等待一個針對已經發出資料的反饋資訊。只有在監視的檔案控制代碼上發生了某個事件的時候 ET 工作模式才會彙報事件。因此在第5步的時候,呼叫者可能會放棄等待仍
在存在於檔案輸入緩衝區內的剩餘資料。在上面的例子中,會有一個事件產生在RFD控制代碼上,因為在第2步執行了一個寫操作,然後,事件將會在第3步被銷燬。因為第4步的讀取操作沒有讀空檔案輸入緩衝區內的資料,因此我們在第5步呼叫 epoll_wait(2)完成後,是否掛起是不確定的。epoll工作在ET模式的時候,必須使用非阻塞套介面,以避免由於一個檔案控制代碼的阻塞讀/阻塞寫操作把處理多個檔案描述符的任務餓死。最好以下面的方式呼叫ET模式的epoll介面,在後面會介紹避免可能的缺陷。
i 基於非阻塞檔案控制代碼
ii 只有當read(2)或者write(2)返回EAGAIN時才需要掛起,等待。但這並不是說每次read()時都需要迴圈讀,直到讀到產生一個EAGAIN才認為此次事件處理完成,當read()返回的讀到的資料長度小於請求的資料長度時,就可以確定此時緩衝中已沒有資料了,也就可以認為此事讀事件已處理完成。

Level Triggered 工作模式

相反的,以LT方式呼叫epoll介面的時候,它就相當於一個速度比較快的poll(2),並且無論後面的資料是否被使用,因此他們具有同樣的職能。因為即使使用ET模式的epoll,在收到多個chunk的資料的時候仍然會產生多個事件。呼叫者可以設定EPOLLONESHOT標誌,在epoll_wait(2)收到事件後epoll會與事件關聯的檔案控制代碼從epoll描述符中禁止掉。因此當EPOLLONESHOT設定後,使用帶有 EPOLL_CTL_MOD標誌的epoll_ctl(2)處理檔案控制代碼就成為呼叫者必須作的事情。

LT(level triggered)是epoll預設的工作方式,並且同時支援block和no-block socket.在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表.

ET (edge-triggered)是高速工作方式,只支援no-block socket,它效率要比LT更高。ET與LT的區別在於,當一個新的事件到來時,ET模式下當然可以從epoll_wait呼叫中獲取到這個事件,可是如果這次沒有把這個事件對應的套接字緩衝區處理完,在這個套接字中沒有新的事件再次到來時,在ET模式下是無法再次從epoll_wait呼叫中獲取這個事件的。而LT模式正好相反,只要一個事件對應的套接字緩衝區還有資料,就總能從epoll_wait中獲取這個事件。因此,LT模式下開發基於epoll的應用要簡單些,不太容易出錯。而在ET模式下事件發生時,如果沒有徹底地將緩衝區資料處理完,則會導致緩衝區中的使用者請求得不到響應。Nginx預設採用ET模式來使用epoll。

.

事例:使用epoll,完成簡單http訊息回顯,並使用瀏覽器測試

epoll_server:

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

epoll_client:

邏輯和select_client 一致,稍加修改即可
測試執行epoll_server, 開啟瀏覽器,在輸入框輸入本地環回,即可看到結果

select、poll、epoll各自優缺點總結如下:

select缺點

(1)每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
(2)同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
(3)select支援的檔案描述符數量太小了,預設是1024

poll優缺點

(1)(2)點與select一致
(3)相對於select來說,poll可以監視的檔案描述符沒有數量限制,但是隨著監視的描述符數量的增長,其效率也會線性下降。

epoll的優點:

1.支援一個程序開啟大數目的socket描述符(FD)
select 最不能忍受的是一個程序所開啟的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對於那些需要支援的上萬連線數目的IM伺服器來說顯然太少了。這時候你一是可以選擇修改這個巨集然後重新編譯核心,不過資料也同時指出這樣會帶來網路效率的下降,二是可以選擇多程序的解決方案(傳統的 Apache方案),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上執行緒間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目
可以cat /proc/sys/fs/fi゙le-max察看,一般來說這個數目和系統記憶體關係很大。

2.IO效率不隨FD數目增加而線性下降
傳統的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過由於網路延時,任一時間只有部分的socket是”活躍”的,但是select/poll每次呼叫都會線性掃描全部的集合,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對”活躍”的socket進行操作—這是因為在核心實現中epoll是根據每個fd上面的callback函式實現的。那麼,只有”活躍”的socket才會主動的去呼叫 callback函式,其他idle狀態socket則不會,在這點上,epoll實現了一個”偽”AIO,因為這時候推動力在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的—比如一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相⽐比還有稍微的下降。但是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。

3.使用mmap加速核心與使用者空間的訊息傳遞
這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要核心把FD訊息通知給使用者空間,如何避免不必要的記憶體拷貝就很重要,在這點上,epoll是通過核心於使用者空間mmap同一塊記憶體實現的。而如果你想我一樣從2.5核心就關注epoll的話,一定不會忘記手工 mmap這一步的。

4.核心微調
這一點其實不算epoll的優點了,而是整個linux平臺的優點。也許你可以懷疑linux平臺,但是你無法迴避linux平臺賦予你微調核心的能力。比如,核心TCP/IP協議棧使用記憶體池管理sk_buff結構,那麼可以在執行時期動態調整這個記憶體pool(skb_head_pool)的大小— 通過echo
XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函式的第2個引數(TCP完成3次握手的資料包佇列長度),也可以根據你平臺記憶體大小動態調整。更甚至在一個數據包面數目巨大但同時每個資料包本身大小卻很小的特殊系統上嘗試最新的NAPI網絡卡驅動架構。

相比於select、poll,epoll最大的好處在於它不會隨著監聽fd數目的增長而降低效率。因為在核心中的select實現中,它是採用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。並且,在linux/posix_types.h標頭檔案有這樣的宣告:

#define __FD_SETSIZE 1024

表示select最多同時監聽1024個fd,當然,可以通過修改標頭檔案再重編譯核心來擴大這個數目,但這似乎並不治本。

Select、Poll、Epoll簡介:

這裡寫圖片描述

Select、Poll、Epoll區別:

這裡寫圖片描述