I/O多路復用select、poll、epoll的區(qū)別使用

采用單線程的方式處理高并發(fā)

在單線程模型中,可以采用I/O復用來提高單線程處理多個請求的能力,然后再采用事件驅(qū)動模型,基于異步回調(diào)來處理事件來。

I/O 多路復用技術(shù)是為了解決進程或線程阻塞到某個 I/O 系統(tǒng)調(diào)用而出現(xiàn)的技術(shù),使進程不阻塞于某個特定的 I/O 系統(tǒng)調(diào)用。

select(),poll(),epoll()都是I/O多路復用的機制。I/O多路復用通過一種機制,可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒,就是這個文件描述符進行讀寫操作之前),能夠通知程序進行相應(yīng)的讀寫操作。
他們本質(zhì)上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現(xiàn)會負責把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。

與多線程和多進程相比,I/O 多路復用的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不需要建立新的進程或者線程,也不必維護這些線程和進程。

select():

監(jiān)視并等待多個文件描述符的屬性變化(可讀、可寫或錯誤異常)。
select()函數(shù)監(jiān)視的文件描述符分 3 類,分別是writefds、readfds、和 exceptfds。
調(diào)用后 select() 函數(shù)會阻塞,直到有描述符就緒(有數(shù)據(jù)可讀、可寫、或者有錯誤異常),或者超時( timeout 指定等待時間),函數(shù)才返回。
當 select()函數(shù)返回后,可以通過遍歷 fdset,來找到就緒的描述符。

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

//成功:就緒描述符的數(shù)目,超時返回 0,
//出錯:-1
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//nfds: 要監(jiān)視的文件描述符的范圍,一般取監(jiān)視的描述符數(shù)的最大值+1,如這里寫 10, 這樣的話,描述符 
//0,1, 2 …… 9 都會被監(jiān)視,在 Linux 上最大值一般為1024。
//readfd: 監(jiān)視的可讀描述符集合,只要有文件描述符即將進行讀操作,這個文件描述符就存儲到這。
//writefds: 監(jiān)視的可寫描述符集合。
//exceptfds: 監(jiān)視的錯誤異常描述符集合

缺點:

  • 每次調(diào)用 select(),都需要把 fd 集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在 fd 很多時會很大,同時每次調(diào)用 select() 都需要在內(nèi)核遍歷傳遞進來的所有 fd,這個開銷在 fd 很多時也很大。
  • 單個進程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在 Linux 上一般為 1024,可以通過修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制,但是這樣也會造成效率的降低。
poll():

select() 和 poll() 系統(tǒng)調(diào)用的本質(zhì)一樣,管理多個描述符也是進行輪詢,根據(jù)描述符的狀態(tài)進行處理。
但是 poll() 沒有最大文件描述符數(shù)量的限制(但是數(shù)量過大后性能也是會下降)。

缺點與select()一樣:包含大量文件描述符的數(shù)組被整體復制于用戶態(tài)和內(nèi)核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數(shù)量的增加而線性增大。

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);//監(jiān)視并等待多個文件描述符的屬性變化。
//fds: 不同與 select() 使用三個位圖來表示三個 fdset 的方式,poll() 使用一個 pollfd 的指針實現(xiàn)。
//一個 pollfd 結(jié)構(gòu)體數(shù)組,其中包括了你想測試的文件描述符和事件, 事件由結(jié)構(gòu)中事件域 events 來確定,
//調(diào)用后實際發(fā)生的時間將被填寫在結(jié)構(gòu)體的 revents 域。

//nfds: 用來指定第一個參數(shù)數(shù)組元素個數(shù)。

//timeout: 指定等待的毫秒數(shù),無論 I/O 是否準備好,poll() 都會返回。

struct pollfd{
    int fd;         /* 文件描述符 */
    short events;   /* 等待的事件 */
    short revents;  /* 實際發(fā)生了的事件 */
}; 

poll() 的實現(xiàn)和 select() 非常相似,只是描述 fd 集合的方式不同,poll() 使用 pollfd 結(jié)構(gòu)而不是 select() 的 fd_set 結(jié)構(gòu),其他的都差不多。

epoll():

epoll 使用一個文件描述符管理多個描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的 copy 只需一次。

#include <sys/epoll.h>

//成功:epoll 專用的文件描述符
//失?。?1
int epoll_create(int size);//生成一個 epoll 專用的文件描述符(創(chuàng)建一個 epoll 的句柄)。會占用一個 fd 值。
//size:參數(shù) size 并不是限制了 epoll 所能監(jiān)聽的描述符最大個數(shù),只是對內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個建議。
//linux 2.6.8 之后,size 參數(shù)是被忽略的,也就是說可以填只有大于 0 的任意值。


//成功:0
//失?。?1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//功能:epoll 的事件注冊函數(shù),它不同于 select() 是在監(jiān)聽事件時告訴內(nèi)核要監(jiān)聽什么類型的事件,而是在這里先注冊要監(jiān)聽的事件類型。
//epfd: epoll 專用的文件描述符,epoll_create()的返回值
//op: 表示動作,用三個宏來表示:EPOLL_CTL_ADD(注冊新的 fd 到 epfd 中)、EPOLL_CTL_MOD(修改fd監(jiān)聽事件)、EPOLL_CTL_DEL(刪除fd)
//fd: 需要監(jiān)聽的文件描述符
//event: 告訴內(nèi)核要監(jiān)聽什么事件,也是幾個宏的集合


//成功:返回需要處理的事件數(shù)目,如返回 0 表示已超時。
//失敗:-1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//功能:等待事件的產(chǎn)生,收集在 epoll 監(jiān)控的事件中已經(jīng)發(fā)送的事件,類似于 select() 調(diào)用。
//epfd: epoll 專用的文件描述符,epoll_create()的返回值
//events: 分配好的 epoll_event 結(jié)構(gòu)體數(shù)組,epoll 將會把發(fā)生的事件賦值到events 數(shù)組中
//(events 不可以是空指針,內(nèi)核只負責把數(shù)據(jù)復制到這個 events 數(shù)組中,不會去幫助我們在用戶態(tài)中分配內(nèi)存)。
//maxevents: maxevents 告之內(nèi)核這個 events 有多大 。
//timeout: 超時時間,單位為毫秒,為 -1 時,函數(shù)為阻塞

epoll 對文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)。
LT 模式:
.socket接收緩沖區(qū)不為空 有數(shù)據(jù)可讀 讀事件一直觸發(fā)
.socket發(fā)送緩沖區(qū)不滿 可以繼續(xù)寫入數(shù)據(jù) 寫事件一直觸發(fā)
符合思維習慣,epoll_wait返回的事件就是socket的狀態(tài)
只要可讀,就一直觸發(fā)讀事件,只要可寫,就一直觸發(fā)寫事件。事件循環(huán)處理比較簡單,無需關(guān)注應(yīng)用層是否有緩沖或緩沖區(qū)是否滿,只管上報事件。缺點是:可能經(jīng)常上報,可能影響性能。
ET 模式:
.socket的接收緩沖區(qū)狀態(tài)變化時觸發(fā)讀事件,即空的接收緩沖區(qū)剛接收到數(shù)據(jù)時觸發(fā)讀事件
.socket的發(fā)送緩沖區(qū)狀態(tài)變化時觸發(fā)寫事件,即滿的緩沖區(qū)剛空出空間時觸發(fā)讀事件
僅在狀態(tài)變化時觸發(fā)事件。
從不可讀變?yōu)榭勺x,從可讀變?yōu)椴豢勺x,從不可寫變?yōu)榭蓪?,從可寫變?yōu)椴豢蓪?,都只觸發(fā)一次。

LT的編程與poll/select接近,符合一直以來的習慣,不易出錯
ET的編程可以做到更加簡潔,某些場景下更加高效,但另一方面容易遺漏事件,容易產(chǎn)生bug

優(yōu)點:

  • 監(jiān)視的描述符數(shù)量不受限制。
    它所支持的 FD 上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠大于 2048,舉個例子,在 1GB 內(nèi)存的機器上大約是 10 萬左右。雖然也可以選擇多進程的解決方案( Apache 就是這樣實現(xiàn)的),不過雖然 Linux 上面創(chuàng)建進程的代價比較小,但仍舊是不可忽視的,加上進程間數(shù)據(jù)同步遠比不上線程間同步的高效,所以也不是一種完美的方案。
  • I/O 的效率不會隨著監(jiān)視 fd 的數(shù)量的增長而下降。
    輪詢就緒期間可能要睡眠和喚醒多次交替。但是 select() 和 poll() 在“醒著”的時候要遍歷整個 fd 集合,而 epoll 在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的 CPU 時間。這就是回調(diào)機制帶來的性能提升。
  • 節(jié)省拷貝開銷。
    select(),poll() 每次調(diào)用都要把 fd 集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,而 epoll 只要一次拷貝,這也能節(jié)省不少的開銷。

epoll怎么實現(xiàn)的

Linux epoll機制是通過紅黑樹和雙向鏈表實現(xiàn)的。 首先通過epoll_create()系統(tǒng)調(diào)用在內(nèi)核中創(chuàng)建一個eventpoll類型的句柄,其中包括紅黑樹根節(jié)點和雙向鏈表頭節(jié)點。然后通過epoll_ctl()系統(tǒng)調(diào)用,向epoll對象的紅黑樹結(jié)構(gòu)中添加、刪除、修改感興趣的事件,返回0標識成功,返回-1表示失敗。最后通過epoll_wait()系統(tǒng)調(diào)用判斷雙向鏈表是否為空,如果為空則阻塞。當文件描述符狀態(tài)改變,fd上的回調(diào)函數(shù)被調(diào)用,該函數(shù)將fd加入到雙向鏈表中,此時epoll_wait函數(shù)被喚醒,返回就緒好的事件。

具體步驟:

利用sys_epoll_create()創(chuàng)建內(nèi)核事件表,在sys_epoll_creat()里面創(chuàng)建了struct eventpoll結(jié)構(gòu)體,其中包括兩個成員:

  • 就緒雙端隊列struct list_head rdlist,用來存放有就緒事件的描述符;
  • 紅黑樹struct rb_root rbr,作為內(nèi)核事件表,用來收集描述符;

每一個epoll對象都有一個獨立的eventpoll結(jié)構(gòu)體,用于存放通過epoll_ctl方法向epoll對象中添加進來的事件。這些事件都會通過ep_instert掛載到紅黑樹上,這樣重復添加的事件就可以通過紅黑樹而高效的識別出來;

而所有添加到epoll中的事件都會與驅(qū)動程序建立回調(diào)關(guān)系,當相應(yīng)的事件發(fā)生時,會調(diào)用ep_poll_callback這個回調(diào)方法,它會將發(fā)生的事件添加到rdlist雙端隊列中;

在epoll中,對于每一個事件,都會建立一個epitem結(jié)構(gòu)體,它里面包括:

  1. 紅黑樹節(jié)點
  2. Rdlist節(jié)點
  3. 事件句柄信息
  4. 一個指向其所屬的eventpoll對象的指針
  5. 期待發(fā)生的事件類型

當調(diào)用epoll_wait檢查是否有事件發(fā)生時,只需要檢查eventpoll對象中的雙端隊列rdlist中是否有epitem元素即可。如果rdlist不為空,則把事件復制到用戶態(tài),同時將事件數(shù)量返回給用戶;如果為空,就等待直到超時。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內(nèi)容