Linux系統(tǒng)編程—I/O多路復(fù)用

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é)果如下:


image.png

另一個終端往有名管道寫的程序代碼:

#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é)果如下:


image.png

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é)省不少的開銷。

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

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