select, poll和epoll的區(qū)別(轉(zhuǎn)載)
select()系統(tǒng)調(diào)用提供一個(gè)機(jī)制來(lái)實(shí)現(xiàn)同步多元I/O:
#include
#include
#include
int select (int n,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
調(diào)用select()將阻塞,直到指定的文件描述符準(zhǔn)備好執(zhí)行I/O,或者可選參數(shù)timeout指定的時(shí)間已經(jīng)過(guò)去。
監(jiān)視的文件描述符分為三類(lèi)set,每一種對(duì)應(yīng)等待不同的事件。readfds中列出的文件描述符被監(jiān)視是否有數(shù)據(jù)可供讀取(如果讀取操作完成則不會(huì)阻塞)。writefds中列出的文件描述符則被監(jiān)視是否寫(xiě)入操作完成而不阻塞。最后,exceptfds中列出的文件描述符則被監(jiān)視是否發(fā)生異常,或者無(wú)法控制的數(shù)據(jù)是否可用(這些狀態(tài)僅僅應(yīng)用于套接字)。這三類(lèi)set可以是NULL,這種情況下select()不監(jiān)視這一類(lèi)事件。
select()成功返回時(shí),每組set都被修改以使它只包含準(zhǔn)備好I/O的文件描述符。例如,假設(shè)有兩個(gè)文件描述符,值分別是7和9,被放在readfds中。當(dāng)select()返回時(shí),如果7仍然在set中,則這個(gè)文件描述符已經(jīng)準(zhǔn)備好被讀取而不會(huì)阻塞。如果9已經(jīng)不在set中,則讀取它將可能會(huì)阻塞(我說(shuō)可能是因?yàn)閿?shù)據(jù)可能正好在select返回后就可用,這種情況下,下一次調(diào)用select()將返回文件描述符準(zhǔn)備好讀取)。
第一個(gè)參數(shù)n,等于所有set中最大的那個(gè)文件描述符的值加1。因此,select()的調(diào)用者負(fù)責(zé)檢查哪個(gè)文件描述符擁有最大值,并且把這個(gè)值加1再傳遞給第一個(gè)參數(shù)。
timeout參數(shù)是一個(gè)指向timeval結(jié)構(gòu)體的指針,timeval定義如下:
#include
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* 10E-6 second */
};
如果這個(gè)參數(shù)不是NULL,則即使沒(méi)有文件描述符準(zhǔn)備好I/O,select()也會(huì)在經(jīng)過(guò)tv_sec秒和tv_usec微秒后返回。當(dāng)select()返回時(shí),timeout參數(shù)的狀態(tài)在不同的系統(tǒng)中是未定義的,因此每次調(diào)用select()之前必須重新初始化timeout和文件描述符set。實(shí)際上,當(dāng)前版本的Linux會(huì)自動(dòng)修改timeout參數(shù),設(shè)置它的值為剩余時(shí)間。因此,如果timeout被設(shè)置為5秒,然后在文件描述符準(zhǔn)備好之前經(jīng)過(guò)了3秒,則這一次調(diào)用select()返回時(shí)tv_sec將變?yōu)?。
如果timeout中的兩個(gè)值都設(shè)置為0,則調(diào)用select()將立即返回,報(bào)告調(diào)用時(shí)所有未決的事件,但不等待任何隨后的事件。
文件描述符set不會(huì)直接操作,一般使用幾個(gè)助手宏來(lái)管理。這允許Unix系統(tǒng)以自己喜歡的方式來(lái)實(shí)現(xiàn)文件描述符set。但大多數(shù)系統(tǒng)都簡(jiǎn)單地實(shí)現(xiàn)set為位數(shù)組。FD_ZERO移除指定set中的所有文件描述符。每一次調(diào)用select()之前都應(yīng)該先調(diào)用它。
fd_set writefds;
FD_ZERO(&writefds);
FD_SET添加一個(gè)文件描述符到指定的set中,F(xiàn)D_CLR則從指定的set中移除一個(gè)文件描述符:
FD_SET(fd, &writefds); /* add 'fd' to the set */
FD_CLR(fd, &writefds); /* oops, remove 'fd' from the set */
設(shè)計(jì)良好的代碼應(yīng)該永遠(yuǎn)不使用FD_CLR,而且實(shí)際情況中它也確實(shí)很少被使用。
FD_ISSET測(cè)試一個(gè)文件描述符是否指定set的一部分。如果文件描述符在set中則返回一個(gè)非0整數(shù),不在則返回0。FD_ISSET在調(diào)用select()返回之后使用,測(cè)試指定的文件描述符是否準(zhǔn)備好相關(guān)動(dòng)作:
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */
因?yàn)槲募枋龇鹲et是靜態(tài)創(chuàng)建的,它們對(duì)文件描述符的最大數(shù)目強(qiáng)加了一個(gè)限制,能夠放進(jìn)set中的最大文件描述符的值由FD_SETSIZE指定。在Linux中,這個(gè)值是1024。本章后面我們還將看到這個(gè)限制的衍生物。
返回值和錯(cuò)誤代碼
select()成功時(shí)返回準(zhǔn)備好I/O的文件描述符數(shù)目,包括所有三個(gè)set。如果提供了timeout,返回值可能是0;錯(cuò)誤時(shí)返回-1,并且設(shè)置errno為下面幾個(gè)值之一:
EBADF
給某個(gè)set提供了無(wú)效文件描述符。
EINTR
等待時(shí)捕獲到信號(hào),可以重新發(fā)起調(diào)用。
EINVAL
參數(shù)n為負(fù)數(shù),或者指定的timeout非法。
ENOMEM
不夠可用內(nèi)存來(lái)完成請(qǐng)求。
--------------------------------------------------------------------------------------------------------------
poll()系統(tǒng)調(diào)用是System V的多元I/O解決方案。它解決了select()的幾個(gè)不足,盡管select()仍然經(jīng)常使用(多數(shù)還是出于習(xí)慣,或者打著可移植的名義):
#include
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
和select()不一樣,poll()沒(méi)有使用低效的三個(gè)基于位的文件描述符set,而是采用了一個(gè)單獨(dú)的結(jié)構(gòu)體pollfd數(shù)組,由fds指針指向這個(gè)組。pollfd結(jié)構(gòu)體定義如下:
#include
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
每一個(gè)pollfd結(jié)構(gòu)體指定了一個(gè)被監(jiān)視的文件描述符,可以傳遞多個(gè)結(jié)構(gòu)體,指示poll()監(jiān)視多個(gè)文件描述符。每個(gè)結(jié)構(gòu)體的events域是監(jiān)視該文件描述符的事件掩碼,由用戶(hù)來(lái)設(shè)置這個(gè)域。revents域是文件描述符的操作結(jié)果事件掩碼。內(nèi)核在調(diào)用返回時(shí)設(shè)置這個(gè)域。events域中請(qǐng)求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN
有數(shù)據(jù)可讀。
POLLRDNORM
有普通數(shù)據(jù)可讀。
POLLRDBAND
有優(yōu)先數(shù)據(jù)可讀。
POLLPRI
有緊迫數(shù)據(jù)可讀。
POLLOUT
寫(xiě)數(shù)據(jù)不會(huì)導(dǎo)致阻塞。
POLLWRNORM
寫(xiě)普通數(shù)據(jù)不會(huì)導(dǎo)致阻塞。
POLLWRBAND
寫(xiě)優(yōu)先數(shù)據(jù)不會(huì)導(dǎo)致阻塞。
POLLMSG
SIGPOLL消息可用。
此外,revents域中還可能返回下列事件:
POLLER
指定的文件描述符發(fā)生錯(cuò)誤。
POLLHUP
指定的文件描述符掛起事件。
POLLNVAL
指定的文件描述符非法。
這些事件在events域中無(wú)意義,因?yàn)樗鼈冊(cè)诤线m的時(shí)候總是會(huì)從revents中返回。使用poll()和select()不一樣,你不需要顯式地請(qǐng)求異常情況報(bào)告。
POLLIN | POLLPRI等價(jià)于select()的讀事件,POLLOUT | POLLWRBAND等價(jià)于select()的寫(xiě)事件。POLLIN等價(jià)于POLLRDNORM | POLLRDBAND,而POLLOUT則等價(jià)于POLLWRNORM。
例如,要同時(shí)監(jiān)視一個(gè)文件描述符是否可讀和可寫(xiě),我們可以設(shè)置events為POLLIN | POLLOUT。在poll返回時(shí),我們可以檢查revents中的標(biāo)志,對(duì)應(yīng)于文件描述符請(qǐng)求的events結(jié)構(gòu)體。如果POLLIN事件被設(shè)置,則文件描述符可以被讀取而不阻塞。如果POLLOUT被設(shè)置,則文件描述符可以寫(xiě)入而不導(dǎo)致阻塞。這些標(biāo)志并不是互斥的:它們可能被同時(shí)設(shè)置,表示這個(gè)文件描述符的讀取和寫(xiě)入操作都會(huì)正常返回而不阻塞。
timeout參數(shù)指定等待的毫秒數(shù),無(wú)論I/O是否準(zhǔn)備好,poll都會(huì)返回。timeout指定為負(fù)數(shù)值表示無(wú)限超時(shí);timeout為0指示poll調(diào)用立即返回并列出準(zhǔn)備好I/O的文件描述符,但并不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來(lái),立即返回。
返回值和錯(cuò)誤代碼
成功時(shí),poll()返回結(jié)構(gòu)體中revents域不為0的文件描述符個(gè)數(shù);如果在超時(shí)前沒(méi)有任何事件發(fā)生,poll()返回0;失敗時(shí),poll()返回-1,并設(shè)置errno為下列值之一:
EBADF
一個(gè)或多個(gè)結(jié)構(gòu)體中指定的文件描述符無(wú)效。
EFAULT
fds指針指向的地址超出進(jìn)程的地址空間。
EINTR
請(qǐng)求的事件之前產(chǎn)生一個(gè)信號(hào),調(diào)用可以重新發(fā)起。
EINVAL
nfds參數(shù)超出PLIMIT_NOFILE值。
ENOMEM
可用內(nèi)存不足,無(wú)法完成請(qǐng)求。
--------------------------------------------------------------------------------------------------------------
以上內(nèi)容來(lái)自《OReilly.Linux.System.Programming - Talking.Directly.to.the.Kernel.and.C.Library.2007》
--------------------------------------------------------------------------------------------------------------
epoll的優(yōu)點(diǎn):
1.支持一個(gè)進(jìn)程打開(kāi)大數(shù)目的socket描述符(FD)
select 最不能忍受的是一個(gè)進(jìn)程所打開(kāi)的FD是有一定限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是2048。對(duì)于那些需要支持的上萬(wàn)連接數(shù)目的IM服務(wù)器來(lái)說(shuō)顯然太少了。這時(shí)候你一是可以選擇修改這個(gè)宏然后重新編譯內(nèi)核,不過(guò)資料也同時(shí)指出這樣會(huì)帶來(lái)網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache方案),不過(guò)雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案。不過(guò) epoll則沒(méi)有這個(gè)限制,它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬(wàn)左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
2.IO效率不隨FD數(shù)目增加而線性下降
傳統(tǒng)的select/poll另一個(gè)致命弱點(diǎn)就是當(dāng)你擁有一個(gè)很大的socket集合,不過(guò)由于網(wǎng)絡(luò)延時(shí),任一時(shí)間只有部分的socket是"活躍"的,但是select/poll每次調(diào)用都會(huì)線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個(gè)問(wèn)題,它只會(huì)對(duì)"活躍"的socket進(jìn)行操作---這是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中epoll是根據(jù)每個(gè)fd上面的callback函數(shù)實(shí)現(xiàn)的。那么,只有"活躍"的socket才會(huì)主動(dòng)的去調(diào)用 callback函數(shù),其他idle狀態(tài)socket則不會(huì),在這點(diǎn)上,epoll實(shí)現(xiàn)了一個(gè)"偽"AIO,因?yàn)檫@時(shí)候推動(dòng)力在os內(nèi)核。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個(gè)高速LAN環(huán)境,epoll并不比select/poll有什么效率,相反,如果過(guò)多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環(huán)境,epoll的效率就遠(yuǎn)在select/poll之上了。
3.使用mmap加速內(nèi)核與用戶(hù)空間的消息傳遞。
這點(diǎn)實(shí)際上涉及到epoll的具體實(shí)現(xiàn)了。無(wú)論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶(hù)空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點(diǎn)上,epoll是通過(guò)內(nèi)核于用戶(hù)空間mmap同一塊內(nèi)存實(shí)現(xiàn)的。而如果你想我一樣從2.5內(nèi)核就關(guān)注epoll的話,一定不會(huì)忘記手工 mmap這一步的。
4.內(nèi)核微調(diào)
這一點(diǎn)其實(shí)不算epoll的優(yōu)點(diǎn)了,而是整個(gè)linux平臺(tái)的優(yōu)點(diǎn)。也許你可以懷疑linux平臺(tái),但是你無(wú)法回避linux平臺(tái)賦予你微調(diào)內(nèi)核的能力。比如,內(nèi)核TCP/IP協(xié)議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu),那么可以在運(yùn)行時(shí)期動(dòng)態(tài)調(diào)整這個(gè)內(nèi)存pool(skb_head_pool)的大小--- 通過(guò)echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個(gè)參數(shù)(TCP完成3次握手的數(shù)據(jù)包隊(duì)列長(zhǎng)度),也可以根據(jù)你平臺(tái)內(nèi)存大小動(dòng)態(tài)調(diào)整。更甚至在一個(gè)數(shù)據(jù)包面數(shù)目巨大但同時(shí)每個(gè)數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng)卡驅(qū)動(dòng)架構(gòu)。