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)核拷貝到用戶空間。
什么是同步或異步?
同步
同步,可以理解為在執(zhí)行完一個函數(shù)或方法之后,一直等待系統(tǒng)返回值或消息,這時程序是出于阻塞的,只有接收到返回的值或消息后才往下執(zhí)行其他的命令。
同步,就是實(shí)時處理(如打電話),比如服務(wù)器一接收客戶端請求,馬上響應(yīng),這樣客戶端可以在最短的時間內(nèi)得到結(jié)果,但是如果多個客戶端,或者一個客戶端發(fā)出的請求很頻繁,服務(wù)器無法同步處理,就會造成涌塞。
同步如打電話,通信雙方不能斷(我們是同時進(jìn)行,同步),你一句我一句,這樣的好處是,對方想表達(dá)的信息我馬上能收到,但是,我在打著電話,我無法做別的事情。
異步
異步,執(zhí)行完函數(shù)或方法后,不必阻塞性地等待返回值或消息,只需要向系統(tǒng)委托一個異步過程,那么當(dāng)系統(tǒng)接收到返回值或消息時,系統(tǒng)會自動觸發(fā)委托的異步過程,從而完成一個完整的流程。
異步,就是分時處理(如收發(fā)短信),服務(wù)器接收到客戶端請求后并不是立即處理,而是等待服務(wù)器比較空閑的時候加以處理,可以避免涌塞。
異步如收發(fā)收短信,對比打電話,打電話我一定要在電話的旁邊聽著,保證雙方都在線,而收發(fā)短信,對方不用保證此刻我一定在手機(jī)旁,同時,我也不用時刻留意手機(jī)有沒有來短信。這樣的話,我看著視頻,然后來了短信,我就處理短信(也可以不處理),接著再看視頻。
總結(jié):
對于寫程序,同步往往會阻塞,沒有數(shù)據(jù)過來,我就等著,異步則不會阻塞,沒數(shù)據(jù)來我干別的事,有數(shù)據(jù)來去處理這些數(shù)據(jù)。
同步在一定程度上可以看做是單線程,這個線程請求一個方法后就待這個方法給他回復(fù),否則他不往下執(zhí)行。
異步在一定程度上可以看做是多線程的,請求一個方法后,就不管了,繼續(xù)執(zhí)行其他的方法。
I/O 多路復(fù)用
與多線程和多進(jìn)程相比,I/O 多路復(fù)用的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不需要建立新的進(jìn)程或者線程,也不必維護(hù)這些線程和進(jìn)程。
select
原型:
#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ù):
1、nfds: 要監(jiān)視的文件描述符的范圍,一般取監(jiān)視的描述符數(shù)的最大值+1,如這里寫 10, 這樣的話,描述符 0,1, 2 …… 9 都會被監(jiān)視,在 Linux 上最大值一般為1024。
2、readfd: 監(jiān)視的可讀描述符集合,只要有文件描述符即將進(jìn)行讀操作,這個文件描述符就存儲到這。
3、writefds: 監(jiān)視的可寫描述符集合。
4、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); // 檢查集合中指定的文件描述符是否可以讀寫
5、timeout: 超時時間,它告知內(nèi)核等待所指定描述字中的任何一個就緒可花多少時間。其 timeval 結(jié)構(gòu)用于指定這段時間的秒數(shù)和微秒數(shù)。
struct timeval {
long tv_sec; /*秒 */
long tv_usec; /* 微妙 */
};
這個參數(shù)有三種可能:
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
select實(shí)例
同時循環(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)容。
#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> 的宏定義,值為 1024
ret = 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;
}
當(dāng)前終端運(yùn)行此程序,另一終端運(yùn)行一個往有名管道寫內(nèi)容的程序,運(yùn)行結(jié)果如下:
另一個終端往有名管道寫的程序代碼:
#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ù):
1、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 等待事件的掩碼取值如下:
1)處理輸入
POLLIN 普通或優(yōu)先級帶數(shù)據(jù)可讀
POLLRDNORM 普通數(shù)據(jù)可讀
POLLRDBAND 優(yōu)先級帶數(shù)據(jù)可讀
POLLPRI 高優(yōu)先級數(shù)據(jù)可讀
2)處理輸出:
POLLOUT 普通或優(yōu)先級帶數(shù)據(jù)可寫
POLLWRNORM 普通數(shù)據(jù)可寫
POLLWRBAND 優(yōu)先級帶數(shù)據(jù)可寫
3)處理錯誤:
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ā)生了什么事件。
2、nfds: 用來指定第一個參數(shù)數(shù)組元素個數(shù)。
3、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 epoll_event * events, int maxevents, int timeout);
epoll_create
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
epoll_ctl
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ù):
1、epfd: epoll 專用的文件描述符,epoll_create()的返回值
2、op: 表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件;
EPOLL_CTL_DEL:從 epfd 中刪除一個 fd;
3、fd: 需要監(jiān)聽的文件描述符
4、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
epoll_wait
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ù):
1、epfd: epoll 專用的文件描述符,epoll_create()的返回值
2、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)存)。
3、maxevents: maxevents 告之內(nèi)核這個 events 有多大 。
4、timeout: 超時時間,單位為毫秒,為 -1 時,函數(shù)為阻塞
返回值:
成功:返回需要處理的事件數(shù)目,如返回 0 表示已超時。
失敗:-1
epoll 對文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)。LT 模式是默認(rèn)模式,LT 模式與 ET 模式的區(qū)別如下:
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)系很大。
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é)省不少的開銷。