溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

三種多路復(fù)用IO實現(xiàn)方式:select,poll,epoll的區(qū)別

發(fā)布時間:2020-07-13 15:15:23 來源:網(wǎng)絡(luò) 閱讀:2690 作者:小止1995 欄目:編程語言

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

此時需知道兩個概念:

所謂阻塞方式block,顧名思義,就是進程或是線程執(zhí)行到這些函數(shù)時必須等待某個事件的發(fā)生,如果事件沒有發(fā)生,進程或線程就被阻塞,函數(shù)不能立即返回。

所謂非阻塞方式non-block,就是進程或線程執(zhí)行此函數(shù)時不必非要等待事件的發(fā)生,一旦執(zhí)行肯定返回,以返回值的不同來反映函數(shù)的執(zhí)行情況,如果事件發(fā)生則與阻塞方式相同,若事件沒有發(fā)生,則返回一個代碼來告知事件未發(fā)生,而進程或線程繼續(xù)執(zhí)行,所以效率較高。

一.select()的機制中提供一fd_set的數(shù)據(jù)結(jié)構(gòu),實際上是一long類型的數(shù)組, 每一個數(shù)組元素都能與一打開的文件句柄(不管是Socket句柄,還是其他 文件或命名管道或設(shè)備句柄)建立聯(lián)系,建立聯(lián)系的工作由程序員完成, 當調(diào)用select()時,由內(nèi)核根據(jù)IO狀態(tài)修改fd_set的內(nèi)容,由此來通知執(zhí)行了select()的進程哪一Socket或文件可讀或可寫。主要用于Socket通信當中。

select使用:它能夠監(jiān)視我們需要監(jiān)視的文件描述符的變化情況——讀寫或是異常。準備就緒的描述符數(shù),若超時則返回0,若出錯則返回-1。

1.如果一個發(fā)現(xiàn)I/O有輸入,讀取的過程中,另外一個也有了輸入,這時候不會產(chǎn)生任何反應(yīng).這就需要你的程序語句去用到select函數(shù)的時候才知道有數(shù)據(jù)輸入。

2.程序去select的時候,如果沒有數(shù)據(jù)輸入,程序會一直等待(阻塞時),直到有數(shù)據(jù)為止,也就是程序中無需循環(huán)和sleep。

#include <sys/types.h>

#include <sys/times.h>

#include <sys/select.h>

int select(int nfds, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * timeout)

函數(shù)返回結(jié)果:當readfds或writefds中映象的文件可讀或可寫或超時,本次select()就結(jié)束返回。程序員利用一組系統(tǒng)提供的宏在select()結(jié)束時便可判斷哪一文件可讀或可寫,對Socket編程特別有用的就是readfds。

注:不同的timeval設(shè)置使select()表現(xiàn)出超時結(jié)束、無超時阻塞和輪詢?nèi)N特性(timeval可精確至百萬分之一秒)。

select詳細執(zhí)行步驟:

三種多路復(fù)用IO實現(xiàn)方式:select,poll,epoll的區(qū)別

(1)使用copy_from_user從用戶空間拷貝fd_set到內(nèi)核空間

(2)注冊回調(diào)函數(shù)__pollwait

(3)遍歷所有fd,調(diào)用其對應(yīng)的poll方法(對于socket,這個poll方法是sock_poll,sock_poll根據(jù)情況會調(diào)用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll為例,其核心實現(xiàn)就是__pollwait,也就是上面注冊的回調(diào)函數(shù)。

(5)__pollwait的主要工作就是把current(當前進程)掛到設(shè)備的等待隊列中,不同的設(shè)備有不同的等待隊列,對于tcp_poll來說,其等待隊列是sk->sk_sleep(注意把進程掛到等待隊列中并不代表進程已經(jīng)睡眠了)。在設(shè)備收到一條消息(網(wǎng)絡(luò)設(shè)備)或填寫完文件數(shù)據(jù)(磁盤設(shè)備)后,會喚醒設(shè)備等待隊列上睡眠的進程,這時current便被喚醒了。

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

(7)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會調(diào)用schedule_timeout是調(diào)用select的進程(也就是current)進入睡眠。當設(shè)備驅(qū)動發(fā)生自身資源可讀寫后,會喚醒其等待隊列上睡眠的進程。如果超過一定的超時時間(schedule_timeout指定),還是沒人喚醒,則調(diào)用select的進程會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。

(8)把fd_set從內(nèi)核空間拷貝到用戶空間。

從以上工作流程可得到select特點:

a.所監(jiān)視的每種事件描述符個數(shù)有上限;

printf("%d\n",sizeof(fd_set));

我的linux系統(tǒng)所能關(guān)心事件應(yīng)為128字節(jié)*8=1024個描述符

三種多路復(fù)用IO實現(xiàn)方式:select,poll,epoll的區(qū)別

b.調(diào)用前后輪詢;

使用select函數(shù),必須使用輔助數(shù)組保存關(guān)心的描述符,因為select函數(shù)中描述符集是輸入輸出型參數(shù),故在調(diào)用前應(yīng)輪詢數(shù)組重置描述符集,調(diào)用后得輪詢描述符集判斷關(guān)心事件是否就緒。

c.系統(tǒng)與用戶數(shù)據(jù)拷貝:使用copy_from_user從用戶空間拷貝fd_set到內(nèi)核空間。

d.調(diào)用前需重置(描述符集是輸入輸出型參數(shù))。

二.poll:

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

#include <poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

監(jiān)視描述符事件選項:

三種多路復(fù)用IO實現(xiàn)方式:select,poll,epoll的區(qū)別

fds:是一個struct pollfd結(jié)構(gòu)類型的數(shù)組,用于存放需要檢測其狀態(tài)的Socket描述符;每當調(diào)用這個函數(shù)之后,系統(tǒng)不會清空這個數(shù)組,操作起來比較方便;特別是對于socket連接比較多的情況下,在一定程度上可以提高處理的效率;這一點與select()函數(shù)不同,調(diào)用select()函數(shù)之后,select()函數(shù)會清空它所檢測的socket描述符集合,導(dǎo)致每次調(diào)用select()之前都必須把socket描述符重新加入到待檢測的集合中;因此,select()函數(shù)適合于只檢測一個socket描述符的情況,而poll()函數(shù)適合于大量socket描述符的情況;與select()十分相似,當返回正值時,代表滿足響應(yīng)事件的文件描述符的個數(shù),如果返回0則代表在規(guī)定時間內(nèi)沒有事件發(fā)生。如發(fā)現(xiàn)返回為負則應(yīng)該立即查看 errno,因為這代表有錯誤發(fā)生。

注:如果沒有事件發(fā)生,revents會被清空。

poll特點:

  1. 監(jiān)視描述符個數(shù)無上限;

最大描述符+1,個數(shù)由fds數(shù)組決定。

2.監(jiān)視事件與返回后事件狀態(tài)反生分離,調(diào)用前后不需重置。

3.調(diào)用后輪詢檢測監(jiān)視事件是否發(fā)生。

4.系統(tǒng)與用戶數(shù)據(jù)拷貝:使用copy_from_user從用戶空間拷貝fds到內(nèi)核空間

三.epoll.

epoll是linux內(nèi)核為處理大批量文件描述符而作了改進的poll,是Linux下多路復(fù)用IO接口select/poll的增強版本,它能顯著提高程序在大量并發(fā)連接中中只有少量活躍的情況下的系統(tǒng)CPU利用率。另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。epoll除了提供select/poll那種IO事件的水平觸發(fā)(Level Triggered)外,還提供了邊緣觸發(fā)(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態(tài),減少epoll_wait/epoll_pwait的調(diào)用,提高應(yīng)用程序效率。

epoll特點:

1.epoll和select和poll的調(diào)用接口上的不同。

select和poll都只提供了一個函數(shù)——select或者poll函數(shù)。而epoll提供了三個函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個epoll句柄;epoll_ctl是注冊要監(jiān)聽的事件類型;epoll_wait則是等待事件的產(chǎn)生。

2.使用mmap加速內(nèi)核與用戶空間的消息傳遞。

對于select和poll函數(shù)的系統(tǒng)與內(nèi)核每次調(diào)用時的數(shù)據(jù)拷貝:epoll是通過內(nèi)核與用戶空間mmap同一塊內(nèi)存實現(xiàn)的,在epoll_ctl函數(shù)中:每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進內(nèi)核,而不是在epoll_wait的時候重復(fù)拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。

3.調(diào)用后不需輪詢判斷描述符事件是否就緒。

對于select和poll函數(shù)每次調(diào)用后輪詢檢測事件是否發(fā)生:epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應(yīng)的設(shè)備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)并為每個fd指定一個回調(diào)函數(shù),當設(shè)備就緒,喚醒等待隊列上的等待者時,就會調(diào)用這個回調(diào)函數(shù),而這個回調(diào)函數(shù)會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現(xiàn)睡一會,判斷一會的效果)。

4.監(jiān)視描述符沒有個數(shù)上限。

epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠大于2048,注:在1GB內(nèi)存的機器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。

5.IO效率不隨FD數(shù)目增加而線性下降。

傳統(tǒng)的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過由于網(wǎng)絡(luò)延時,任一時間只有部分的socket是“活躍”的,但是select/poll每次調(diào)用都會線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個問題,它只會對“活躍”的socket進行操作---這是因為在內(nèi)核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的。只有“活躍”的socket才會主動的去調(diào)用 callback函數(shù),其他idle狀態(tài)socket則不會。

拓展:系統(tǒng)維護一顆紅黑樹(平衡搜索二叉樹:穩(wěn)定)存儲監(jiān)視描述符,和一張鏈表存儲就緒的描述符。當每次注冊或修改,刪除新的文件描述符到epoll句柄中時,就會增加一個描述符到這課紅黑樹中(增刪改查簡單),當返回時檢測鏈表上是否有節(jié)點,有節(jié)點則拷貝到用戶傳給它的那個描述符數(shù)組中。

epoll對于select和poll相比,顯著優(yōu)點是:

(1)select,poll實現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時,調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時間。這就是回調(diào)機制帶來的性能提升。

(2)select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊列并不是設(shè)備等待隊列,只是一個epoll內(nèi)部定義的等待隊列)。這也能節(jié)省不少的開銷。

總結(jié):

select

  select能監(jiān)控的描述符個數(shù)由內(nèi)核中的FD_SETSIZE限制,僅為1024,這也是select最大的缺點,因   為現(xiàn)在的服務(wù)器并發(fā)量遠遠不止1024。即使能重新編譯內(nèi)核改變FD_SETSIZE的值,但這并不能提高   select的性能。

  • 每次調(diào)用select都會線性掃描所有描述符的狀態(tài),在select結(jié)束后,用戶也要線性掃描fd_set數(shù)組才知道哪些描述符準備就緒,等于說每次調(diào)用復(fù)雜度都是O(n)的,在并發(fā)量大的情況下,每次掃描都是相當耗時的,很有可能有未處理的連接等待超時。

  • 每次調(diào)用select都要在用戶空間和內(nèi)核空間里進行內(nèi)存復(fù)制fd描述符等信息。

poll

  • poll使用pollfd結(jié)構(gòu)來存儲fd,突破了select中描述符數(shù)目的限制。

  • 與select的后兩點類似,poll仍然需要將pollfd數(shù)組拷貝到內(nèi)核空間,之后依次掃描fd的狀態(tài),整體復(fù)雜度依然是O(n)的,在并發(fā)量大的情況下服務(wù)器性能會快速下降。

epoll

  • epoll維護的描述符數(shù)目不受到限制,而且性能不會隨著描述符數(shù)目的增加而下降。

  • 服務(wù)器的特點是經(jīng)常維護著大量連接,但其中某一時刻讀寫的操作符數(shù)量卻不多。epoll先通過epoll_ctl注冊一個描述符到內(nèi)核中,并一直維護著而不像poll每次操作都將所有要監(jiān)控的描述符傳遞給內(nèi)核;在描述符讀寫就緒時,通過回掉函數(shù)將自己加入就緒隊列中,之后epoll_wait返回該就緒隊列。也就是說,epoll基本不做無用的操作,時間復(fù)雜度僅與活躍的客戶端數(shù)有關(guān),而不會隨著描述符數(shù)目的增加而下降。

  • epoll在傳遞內(nèi)核與用戶空間的消息時使用了內(nèi)存共享,而不是內(nèi)存拷貝,這也使得epoll的效率比poll和select更高。



三種多路復(fù)用IO實現(xiàn)方式:select,poll,epoll的區(qū)別

poll和epoll適用于關(guān)心描述符個數(shù)多的應(yīng)用程序。其中epoll對于每次只有很少描述符就緒很有優(yōu)勢(采用回調(diào)機制監(jiān)測描述符就緒)。

綜上:epoll是上面三個函數(shù)中效率最高的。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI