【Linux系统编程】I/O多路复用select、poll、epoll的区别使用
I/O 多路復(fù)用技術(shù)是為了解決進(jìn)程或線程阻塞到某個 I/O 系統(tǒng)調(diào)用而出現(xiàn)的技術(shù),使進(jìn)程不阻塞于某個特定的 I/O 系統(tǒng)調(diào)用。
select(),poll(),epoll()都是I/O多路復(fù)用的機(jī)制。I/O多路復(fù)用通過一種機(jī)制,可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒,就是這個文件描述符進(jìn)行讀寫操作之前),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。但select(),poll(),epoll()本質(zhì)上都是同步I/O,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫,異步I/O的實(shí)現(xiàn)會負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。什么是同步或異步,詳情請看《同步和異步的區(qū)別》。
與多線程和多進(jìn)程相比,I/O 多路復(fù)用的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不需要建立新的進(jìn)程或者線程,也不必維護(hù)這些線程和進(jìn)程。
select()的使用
所需頭文件:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds,?fd_set *readfds,?fd_set *writefds,?fd_set *exceptfds,?struct timeval *timeout);
功能:
監(jiān)視并等待多個文件描述符的屬性變化(可讀、可寫或錯誤異常)。select()函數(shù)監(jiān)視的文件描述符分 3 類,分別是writefds、readfds、和 exceptfds。調(diào)用后 select() 函數(shù)會阻塞,直到有描述符就緒(有數(shù)據(jù)可讀、可寫、或者有錯誤異常),或者超時( timeout 指定等待時間),函數(shù)才返回。當(dāng) select()函數(shù)返回后,可以通過遍歷 fdset,來找到就緒的描述符。
參數(shù):
nfds:?要監(jiān)視的文件描述符的范圍,一般取監(jiān)視的描述符數(shù)的最大值+1,如這里寫 10, 這樣的話,描述符 0,1, 2 …… 9 都會被監(jiān)視,在 Linux 上最大值一般為1024。
readfd:?監(jiān)視的可讀描述符集合,只要有文件描述符即將進(jìn)行讀操作,這個文件描述符就存儲到這。
writefds:?監(jiān)視的可寫描述符集合。
exceptfds:?監(jiān)視的錯誤異常描述符集合
中間的三個參數(shù) readfds、writefds 和 exceptfds 指定我們要讓內(nèi)核監(jiān)測讀、寫和異常條件的描述字。如果不需要使用某一個的條件,就可以把它設(shè)為空指針( NULL )。集合fd_set?中存放的是文件描述符,可通過以下四個宏進(jìn)行設(shè)置:
//清空集合
void FD_ZERO(fd_set *fdset);?
//將一個給定的文件描述符加入集合之中
void FD_SET(int fd, fd_set *fdset);
//將一個給定的文件描述符從集合中刪除
void FD_CLR(int fd, fd_set *fdset);
?// 檢查集合中指定的文件描述符是否可以讀寫?
int FD_ISSET(int fd, fd_set *fdset);?
timeout:?超時時間,它告知內(nèi)核等待所指定描述字中的任何一個就緒可花多少時間。其 timeval 結(jié)構(gòu)用于指定這段時間的秒數(shù)和微秒數(shù)。
struct timeval
{
time_t tv_sec; ? ? ? /* 秒 */
suseconds_t tv_usec; /* 微秒 */
};
1)永遠(yuǎn)等待下去:僅在有一個描述字準(zhǔn)備好 I/O 時才返回。為此,把該參數(shù)設(shè)置為空指針 NULL。
2)等待固定時間:在指定的固定時間(?timeval 結(jié)構(gòu)中指定的秒數(shù)和微秒數(shù))內(nèi),在有一個描述字準(zhǔn)備好 I/O 時返回,如果時間到了,就算沒有文件描述符發(fā)生變化,這個函數(shù)會返回 0。
3)根本不等待(不阻塞):檢查描述字后立即返回,這稱為輪詢。為此,struct timeval變量的時間值指定為 0 秒 0 微秒,文件描述符屬性無變化返回 0,有變化返回準(zhǔn)備好的描述符數(shù)量。
返回值:
成功:就緒描述符的數(shù)目,超時返回 0,
出錯:-1
我們寫這么一個例子,同時循環(huán)讀取標(biāo)準(zhǔn)輸入的內(nèi)容,讀取有名管道的內(nèi)容,默認(rèn)的情況下,標(biāo)準(zhǔn)輸入沒有內(nèi)容,read()時會阻塞,同樣的,有名管道如果沒有內(nèi)容,read()也會阻塞,我們?nèi)绾螌?shí)現(xiàn)循環(huán)讀取這兩者的內(nèi)容呢?最簡單的方法是,開兩個線程,一個線程循環(huán)讀標(biāo)準(zhǔn)輸入的內(nèi)容,一個線程循環(huán)讀有名管道的內(nèi)容。而在這里,我們通過 select() 函數(shù)實(shí)現(xiàn)這個功能:
#include <sys/select.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h>int main(int argc, char *argv[]) {fd_set rfds;struct timeval tv;int ret;int fd;ret = mkfifo("test_fifo", 0666); // 創(chuàng)建有名管道if(ret != 0){perror("mkfifo:");}fd = open("test_fifo", O_RDWR); // 讀寫方式打開管道if(fd < 0){perror("open fifo");return -1;}ret = 0;while(1){// 這部分內(nèi)容,要放在while(1)里面FD_ZERO(&rfds); // 清空FD_SET(0, &rfds); // 標(biāo)準(zhǔn)輸入描述符 0 加入集合FD_SET(fd, &rfds); // 有名管道描述符 fd 加入集合// 超時設(shè)置tv.tv_sec = 1;tv.tv_usec = 0;// 監(jiān)視并等待多個文件(標(biāo)準(zhǔn)輸入,有名管道)描述符的屬性變化(是否可讀)// 沒有屬性變化,這個函數(shù)會阻塞,直到有變化才往下執(zhí)行,這里沒有設(shè)置超時// FD_SETSIZE 為 <sys/select.h> 的宏定義,值為 1024ret = select(FD_SETSIZE, &rfds, NULL, NULL, NULL);//ret = select(FD_SETSIZE, &rfds, NULL, NULL, &tv);if(ret == -1){ // 出錯perror("select()");}else if(ret > 0){ // 準(zhǔn)備就緒的文件描述符char buf[100] = {0};if( FD_ISSET(0, &rfds) ){ // 標(biāo)準(zhǔn)輸入read(0, buf, sizeof(buf));printf("stdin buf = %s\n", buf);}else if( FD_ISSET(fd, &rfds) ){ // 有名管道read(fd, buf, sizeof(buf));printf("fifo buf = %s\n", buf);}}else if(0 == ret){ // 超時printf("time out\n");}}return 0; }
下面為上面例子的往有名管道寫內(nèi)容的示例代碼:
#include <sys/select.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h>int main(int argc, char *argv[]) {//select_demo(8);fd_set rfds;struct timeval tv;int ret;int fd;ret = mkfifo("test_fifo", 0666); // 創(chuàng)建有名管道if(ret != 0){perror("mkfifo:");}fd = open("test_fifo", O_RDWR); // 讀寫方式打開管道if(fd < 0){perror("open fifo");return -1;}while(1){char *str = "this is for test";write(fd, str, strlen(str)); // 往管道里寫內(nèi)容printf("after write to fifo\n");sleep(5);}return 0; }
運(yùn)行結(jié)果如下:
select()目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優(yōu)點(diǎn)。
select()的缺點(diǎn)在于:
1)每次調(diào)用 select(),都需要把 fd 集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個開銷在 fd 很多時會很大,同時每次調(diào)用 select() 都需要在內(nèi)核遍歷傳遞進(jìn)來的所有 fd,這個開銷在 fd 很多時也很大。
2)單個進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在 Linux 上一般為 1024,可以通過修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制,但是這樣也會造成效率的降低。
poll()的使用
select() 和 poll() 系統(tǒng)調(diào)用的本質(zhì)一樣,前者在 BSD UNIX 中引入的,后者在 System V 中引入的。poll() 的機(jī)制與 select() 類似,與 select() 在本質(zhì)上沒有多大差別,管理多個描述符也是進(jìn)行輪詢,根據(jù)描述符的狀態(tài)進(jìn)行處理,但是 poll() 沒有最大文件描述符數(shù)量的限制(但是數(shù)量過大后性能也是會下降)。poll() 和 select() 同樣存在一個缺點(diǎn)就是,包含大量文件描述符的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數(shù)量的增加而線性增大。
所需頭文件:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:
監(jiān)視并等待多個文件描述符的屬性變化。
參數(shù):
fds: 不同與 select() 使用三個位圖來表示三個 fdset 的方式,poll() 使用一個 pollfd 的指針實(shí)現(xiàn)。一個 pollfd 結(jié)構(gòu)體數(shù)組,其中包括了你想測試的文件描述符和事件, 事件由結(jié)構(gòu)中事件域 events 來確定,調(diào)用后實(shí)際發(fā)生的時間將被填寫在結(jié)構(gòu)體的 revents 域。
struct pollfd{
int fd; ? ? ? ? /* 文件描述符 */
short events; ? /* 等待的事件 */
short revents; ?/* 實(shí)際發(fā)生了的事件 */
};?
fd:每一個 pollfd 結(jié)構(gòu)體指定了一個被監(jiān)視的文件描述符,可以傳遞多個結(jié)構(gòu)體,指示 poll() 監(jiān)視多個文件描述符。
events:每個結(jié)構(gòu)體的 events 域是監(jiān)視該文件描述符的事件掩碼,由用戶來設(shè)置這個域。events?等待事件的掩碼取值如下:
處理輸入:
POLLIN?普通或優(yōu)先級帶數(shù)據(jù)可讀
POLLRDNORM?普通數(shù)據(jù)可讀
POLLRDBAND?優(yōu)先級帶數(shù)據(jù)可讀
POLLPRI?高優(yōu)先級數(shù)據(jù)可讀
處理輸出:
POLLOUT?普通或優(yōu)先級帶數(shù)據(jù)可寫
POLLWRNORM?普通數(shù)據(jù)可寫
POLLWRBAND?優(yōu)先級帶數(shù)據(jù)可寫
處理錯誤:
POLLERR發(fā)生錯誤
POLLHUP發(fā)生掛起
POLLVAL?描述字不是一個打開的文件
poll() 處理三個級別的數(shù)據(jù),普通 normal,優(yōu)先級帶 priority band,高優(yōu)先級 high priority,這些都是出于流的實(shí)現(xiàn)。
POLLIN | POLLPRI 等價于 select() 的讀事件,POLLOUT | POLLWRBAND 等價于 select() 的寫事件。POLLIN 等價于 POLLRDNORM | POLLRDBAND,而 POLLOUT 則等價于 POLLWRNORM 。例如,要同時監(jiān)視一個文件描述符是否可讀和可寫,我們可以設(shè)置 events 為 POLLIN | POLLOUT。
revents:revents 域是文件描述符的操作結(jié)果事件掩碼,內(nèi)核在調(diào)用返回時設(shè)置這個域。events 域中請求的任何事件都可能在 revents 域中返回。
每個結(jié)構(gòu)體的 events 域是由用戶來設(shè)置,告訴內(nèi)核我們關(guān)注的是什么,而 revents 域是返回時內(nèi)核設(shè)置的,以說明對該描述符發(fā)生了什么事件。
nfds:?用來指定第一個參數(shù)數(shù)組元素個數(shù)。
timeout:?指定等待的毫秒數(shù),無論 I/O 是否準(zhǔn)備好,poll() 都會返回。當(dāng)?shù)却龝r間為 0 時,poll() 函數(shù)立即返回,為 -1 則使 poll() 一直阻塞直到一個指定事件發(fā)生。
返回值:
成功時,poll() 返回結(jié)構(gòu)體中 revents 域不為 0 的文件描述符個數(shù);如果在超時前沒有任何事件發(fā)生,poll()返回 0;
失敗時,poll() 返回 -1,并設(shè)置 errno 為下列值之一:
EBADF:一個或多個結(jié)構(gòu)體中指定的文件描述符無效。
EFAULT:fds?指針指向的地址超出進(jìn)程的地址空間。
EINTR:請求的事件之前產(chǎn)生一個信號,調(diào)用可以重新發(fā)起。
EINVAL:nfds?參數(shù)超出 PLIMIT_NOFILE 值。
ENOMEM:可用內(nèi)存不足,無法完成請求。
我們將上面的例子,改為用 poll() 實(shí)現(xiàn):
#include <poll.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h>int main(int argc, char *argv[]) {int ret;int fd;struct pollfd fds[2]; // 監(jiān)視文件描述符結(jié)構(gòu)體,2 個元素ret = mkfifo("test_fifo", 0666); // 創(chuàng)建有名管道if(ret != 0){perror("mkfifo:");}fd = open("test_fifo", O_RDWR); // 讀寫方式打開管道if(fd < 0){perror("open fifo");return -1;}ret = 0;fds[0].fd = 0; // 標(biāo)準(zhǔn)輸入fds[1].fd = fd; // 有名管道fds[0].events = POLLIN; // 普通或優(yōu)先級帶數(shù)據(jù)可讀fds[1].events = POLLIN; // 普通或優(yōu)先級帶數(shù)據(jù)可讀while(1){// 監(jiān)視并等待多個文件(標(biāo)準(zhǔn)輸入,有名管道)描述符的屬性變化(是否可讀)// 沒有屬性變化,這個函數(shù)會阻塞,直到有變化才往下執(zhí)行,這里沒有設(shè)置超時ret = poll(fds, 2, -1);//ret = poll(&fd, 2, 1000);if(ret == -1){ // 出錯perror("poll()");}else if(ret > 0){ // 準(zhǔn)備就緒的文件描述符char buf[100] = {0};if( ( fds[0].revents & POLLIN ) == POLLIN ){ // 標(biāo)準(zhǔn)輸入read(0, buf, sizeof(buf));printf("stdin buf = %s\n", buf);}else if( ( fds[1].revents & POLLIN ) == POLLIN ){ // 有名管道read(fd, buf, sizeof(buf));printf("fifo buf = %s\n", buf);}}else if(0 == ret){ // 超時printf("time out\n");}}return 0; }
poll() 的實(shí)現(xiàn)和 select() 非常相似,只是描述 fd 集合的方式不同,poll() 使用 pollfd 結(jié)構(gòu)而不是 select() 的 fd_set 結(jié)構(gòu),其他的都差不多。
epoll的使用
epoll?是在 2.6 內(nèi)核中提出的,是之前的 select() 和 poll() 的增強(qiáng)版本。相對于 select() 和 poll() 來說,epoll 更加靈活,沒有描述符限制。epoll 使用一個文件描述符管理多個描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的 copy 只需一次。
epoll 操作過程需要三個接口,分別如下:
#include <sys/epoll.h> int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct
int epoll_create(int size);
功能:
該函數(shù)生成一個 epoll 專用的文件描述符(創(chuàng)建一個 epoll 的句柄)。
參數(shù):
size: 用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大,參數(shù) size 并不是限制了 epoll 所能監(jiān)聽的描述符最大個數(shù),只是對內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個建議。自從 linux 2.6.8 之后,size 參數(shù)是被忽略的,也就是說可以填只有大于 0 的任意值。需要注意的是,當(dāng)創(chuàng)建好 epoll 句柄后,它就是會占用一個 fd 值,在 linux 下如果查看 /proc/ 進(jìn)程 id/fd/,是能夠看到這個 fd 的,所以在使用完 epoll 后,必須調(diào)用 close() 關(guān)閉,否則可能導(dǎo)致 fd 被耗盡。
返回值:
成功:epoll 專用的文件描述符
失敗:-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)聽的事件類型。
參數(shù):
epfd:?epoll 專用的文件描述符,epoll_create()的返回值
op:?表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件;
EPOLL_CTL_DEL:從 epfd 中刪除一個 fd;
fd:?需要監(jiān)聽的文件描述符
event:?告訴內(nèi)核要監(jiān)聽什么事件,struct epoll_event 結(jié)構(gòu)如下:
// 保存觸發(fā)事件的某個文件描述符相關(guān)的數(shù)據(jù)(與具體使用方式有關(guān)) typedef union epoll_data {void *ptr;int fd;__uint32_t u32;__uint64_t u64; } epoll_data_t;// 感興趣的事件和被觸發(fā)的事件 struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */ };
events 可以是以下幾個宏的集合:
EPOLLIN?:表示對應(yīng)的文件描述符可以讀(包括對端 SOCKET 正常關(guān)閉);
EPOLLOUT:表示對應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對應(yīng)的文件描述符發(fā)生錯誤;
EPOLLHUP:表示對應(yīng)的文件描述符被掛斷;
EPOLLET?:將 EPOLL 設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對于水平觸發(fā)(Level Triggered)來說的。
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個 socket 的話,需要再次把這個 socket 加入到 EPOLL 隊(duì)列里
返回值:
成功: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)用。
參數(shù):
epfd:?epoll 專用的文件描述符,epoll_create()的返回值
events:?分配好的 epoll_event 結(jié)構(gòu)體數(shù)組,epoll 將會把發(fā)生的事件賦值到events 數(shù)組中(events 不可以是空指針,內(nèi)核只負(fù)責(zé)把數(shù)據(jù)復(fù)制到這個 events 數(shù)組中,不會去幫助我們在用戶態(tài)中分配內(nèi)存)。
maxevents:?maxevents 告之內(nèi)核這個 events 有多大 。
timeout:?超時時間,單位為毫秒,為 -1 時,函數(shù)為阻塞
返回值:
成功:返回需要處理的事件數(shù)目,如返回 0 表示已超時。
失敗:-1
LT 模式:當(dāng) epoll_wait 檢測到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序可以不立即處理該事件。下次調(diào)用 epoll_wait 時,會再次響應(yīng)應(yīng)用程序并通知此事件。
ET 模式:當(dāng) epoll_wait 檢測到描述符事件發(fā)生并將此事件通知應(yīng)用程序,應(yīng)用程序必須立即處理該事件。如果不處理,下次調(diào)用 epoll_wait 時,不會再次響應(yīng)應(yīng)用程序并通知此事件。
ET 模式在很大程度上減少了 epoll 事件被重復(fù)觸發(fā)的次數(shù),因此效率要比 LT 模式高。epoll 工作在 ET 模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務(wù)餓死。
接下來,我們將上面的例子,改為用 epoll 實(shí)現(xiàn):
#include <sys/epoll.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h>int main(int argc, char *argv[]) {int ret;int fd;ret = mkfifo("test_fifo", 0666); // 創(chuàng)建有名管道if(ret != 0){perror("mkfifo:");}fd = open("test_fifo", O_RDWR); // 讀寫方式打開管道if(fd < 0){perror("open fifo");return -1;}ret = 0;struct epoll_event event; // 告訴內(nèi)核要監(jiān)聽什么事件struct epoll_event wait_event;int epfd = epoll_create(10); // 創(chuàng)建一個 epoll 的句柄,參數(shù)要大于 0, 沒有太大意義if( -1 == epfd ){perror ("epoll_create");return -1;}event.data.fd = 0; // 標(biāo)準(zhǔn)輸入event.events = EPOLLIN; // 表示對應(yīng)的文件描述符可以讀// 事件注冊函數(shù),將標(biāo)準(zhǔn)輸入描述符 0 加入監(jiān)聽事件ret = epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);if(-1 == ret){perror("epoll_ctl");return -1;}event.data.fd = fd; // 有名管道event.events = EPOLLIN; // 表示對應(yīng)的文件描述符可以讀// 事件注冊函數(shù),將有名管道描述符 fd 加入監(jiān)聽事件ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);if(-1 == ret){perror("epoll_ctl");return -1;}ret = 0;while(1){// 監(jiān)視并等待多個文件(標(biāo)準(zhǔn)輸入,有名管道)描述符的屬性變化(是否可讀)// 沒有屬性變化,這個函數(shù)會阻塞,直到有變化才往下執(zhí)行,這里沒有設(shè)置超時ret = epoll_wait(epfd, &wait_event, 2, -1);//ret = epoll_wait(epfd, &wait_event, 2, 1000);if(ret == -1){ // 出錯close(epfd);perror("epoll");}else if(ret > 0){ // 準(zhǔn)備就緒的文件描述符char buf[100] = {0};if( ( 0 == wait_event.data.fd ) && ( EPOLLIN == wait_event.events & EPOLLIN ) ){ // 標(biāo)準(zhǔn)輸入read(0, buf, sizeof(buf));printf("stdin buf = %s\n", buf);}else if( ( fd == wait_event.data.fd ) && ( EPOLLIN == wait_event.events & EPOLLIN ) ){ // 有名管道read(fd, buf, sizeof(buf));printf("fifo buf = %s\n", buf);}}else if(0 == ret){ // 超時printf("time out\n");}}close(epfd);return 0; }
在 select/poll中,進(jìn)程只有在調(diào)用一定的方法后,內(nèi)核才對所有監(jiān)視的文件描述符進(jìn)行掃描,而 epoll() 事先通過 epoll_ctl() 來注冊一個文件描述符,一旦基于某個文件描述符就緒時,內(nèi)核會采用類似 callback 的回調(diào)機(jī)制(軟件中斷?),迅速激活這個文件描述符,當(dāng)進(jìn)程調(diào)用 epoll_wait() 時便得到通知。
epoll 的優(yōu)點(diǎn)主要是一下幾個方面:
1)監(jiān)視的描述符數(shù)量不受限制,它所支持的 FD 上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠(yuǎn)大于 2048,舉個例子,在 1GB 內(nèi)存的機(jī)器上大約是 10 萬左右,具體數(shù)目可以 cat /proc/sys/fs/file-max 察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。select() 的最大缺點(diǎn)就是進(jìn)程打開的 fd 是有數(shù)量限制的。這對于連接數(shù)量比較大的服務(wù)器來說根本不能滿足。雖然也可以選擇多進(jìn)程的解決方案( Apache 就是這樣實(shí)現(xiàn)的),不過雖然 Linux 上面創(chuàng)建進(jìn)程的代價比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案。
2)I/O 的效率不會隨著監(jiān)視 fd 的數(shù)量的增長而下降。select(),poll() 實(shí)現(xiàn)需要自己不斷輪詢所有 fd 集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而 epoll 其實(shí)也需要調(diào)用 epoll_wait() 不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時,調(diào)用回調(diào)函數(shù),把就緒 fd 放入就緒鏈表中,并喚醒在 epoll_wait() 中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是 select() 和 poll() 在“醒著”的時候要遍歷整個 fd 集合,而 epoll 在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的 CPU 時間。這就是回調(diào)機(jī)制帶來的性能提升。
3)select(),poll() 每次調(diào)用都要把 fd 集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,而 epoll 只要一次拷貝,這也能節(jié)省不少的開銷。
總結(jié)
以上是生活随笔為你收集整理的【Linux系统编程】I/O多路复用select、poll、epoll的区别使用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Linux系统编程】可重入函数和不可重
- 下一篇: 【Linux网络编程】套接字的介绍