Linux I/O複用:select ,poll,epoll

I/O複用:一種進程預先告知內核的能力,使得內核一旦發現進程指定的一個或多個I/O條件就緒,它就通知進程。

一、5種I/O模型

Linux下有5種I/O模型,分別爲:

  • 阻塞式I/O;
  • 非阻塞式I/O;
  • I/O複用;
  • 信號驅動式I/O;
  • 異步I/O;

      阻塞式I/O,即應用程序調用IO函數,導致程序阻塞(當前進程被掛起,暫停運行直到函數返回),等待數據準備好,如果數據沒有準備好,進程就一直處於等待狀態,只有數據準備好了且返回到進程緩衝區或發生錯誤才返回。在網絡通信中,socket函數創建套接字時,所有的套接字默認都是阻塞的。
      
      非阻塞式I/O,進程反覆調用一個IO函數,若沒有數據準備好,返回一個錯誤,即不進入阻塞狀態,而是不斷的進行函數調用。 把一個SOCKET接口設置爲非阻塞就是告訴內核,當所請求的I/O操作無法完成時,不要將進程睡眠,而是返回一個錯誤。這樣我們的I/O操作函數將不斷的測試數據是否已經準備好,如果沒有準備好,繼續測試,直到數據準備好爲止。在這個不斷測試的過程中,會大量的佔用CPU的時間。
      
      I/O複用,主要通過select和poll等函數來實現,阻塞在這兩個系統調用中的某一個之上,而不是阻塞在真正的IO系統調用上,比阻塞IO並沒有什麼優越性;關鍵是能實現同時對多個IO端口進行監聽(select和poll等函數可以看作是一種代理,由它們來代替IO函數進行監聽等待)。
      
      信號驅動式I/O,通過使用信號,讓內核在描述符就緒時發送SIGIO信號給進程。當程序運行到IO時,進程繼續運行並不阻塞。當數據準備好時,進程會收到一個SIGIO信號,可以在信號處理函數中調用I/O操作函數處理數據。
      
      異步I/O,提前告知內核執行某個操作,且在內核完成整個操作之後通過我們,在等待IO執行期間,進程不會阻塞。
      
      總結:同步IO引起進程阻塞,直至IO操作完成。
         異步IO不會引起進程阻塞。
         IO複用是先通過select調用阻塞。

二、select 函數

  select函數允許進程指示內核等待多個事件中的一個或多個發生,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒它。我們調用select告知內核對哪些描述字(就讀、寫或異常條件)感興趣以及等待多長時間。我們感興趣的描述字不侷限於套接口,任何描述字都可以使用select來測試。

/* 函數
 * 返回值:若有就緒描述符則爲其數目,超時則爲0,出錯爲-1;
 * 函數原型:
 */
 #include <sys/select.h>
 #include <sys/time.h>
 int select (int maxfdpl , fd_set *readset ,fd_set *writeset, fd_set *exceptset , const struct timeval *timeout);
/* 說明:
 * maxfdpl表示待測試的描述符個數,其值爲待測試的最大描述符+1;
 * 中間的readset,writeset,exceptset指定我們要讓內核測試讀、寫、異常條件的描述符;
 * timeout告知內核等待所指定描述字中的任何一個就緒可花多長時間;
 */

/* timeval 結構
 * 結構定義:
 */
 struct timeval {
     long tv_sec; //seconds
     long tv_usec ; //microseconds
 }
/* 說明:
 * 這個結構參數有3種可能值:
 * 1、NULL:代表永遠等待下去,僅在有一個描述符準備好時才返回;
 * 2、一個固定的值:代表等待一段固定的時間,期間有描述符準備好則返回;
 * 3、0:表示根本不等待,檢查描述字之後立即返回,這稱爲輪詢;
 */

/* fd_set
 * fd_set 結構表示一個描述符集,通常是一個整數數組,每個整數中每一位對應一個描述符。
 * 關於fd_set所有實現細節隱藏在數據類型和以下四個宏當中:
 */
void FD_ZERO(fd_set *fdset); /* clear all bits in fdset */
void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset */
void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset */
int  FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset ? */
/* 說明:
 * select函數修改由指針參數所指向的描述字集,因而這三個參數都是值-結果參數;
 * 在select函數執行過程中,會修改指針參數(readset...)其中的值;
 * 函數返回時,結果指示哪些描述符已就緒,該函數返回後,
 * 我們使用FD_ISSET來測試fd_set數據類型中的描述符,
 * 描述符集中任何與未就緒的描述符對應的位返回時均清爲0,
 * 爲此,每次重新調用select函數中,我們都得再次把所有描述字集合中的所關心的位置爲1;
 */

部分參考:Linux五種IO模型性能分析

三、poll 函數

  poll函數提供的功能與select類似,不過在處理流設備時,它能夠額外的信息。

/* 返回值:若有就緒描述符則爲其數目,超時則爲0,出錯爲-1;
 * 函數定義:
 */
#include <poll.h>
int poll(struct polldf *fdarray, unsigned long nfds, int timeout);
/*
 * 說明:
 * 第一個參數爲指向結構數組的指針,每一個pollfd結構用於指定測試某個給定描述符fg的條件;
 * nfds表示結構數組中元素的個數;
 * timeout指定poll函數返回前等待多長時間(單位毫秒);可能取值:
 * 1、-1:永遠等待(較新的系統中貌似沒有定義INFTIM);
 * 2、0:立即返回,不阻塞進程(較新的系統中貌似沒有0的說明);
 * 3、>0:等待指定數值的毫秒數;
 */
struct pollfd{
    int fd;         /*descriptor to check*/
    short events;   /*events of interest on fd*/
    short revents;  /*events that occurred on fd*/
}
/*
 * fd 成員表示感興趣的,且打開了的文件描述符;
 * events  成員是位掩碼,用於指定針對這個文件描述符感興趣的事件;
 * revents  成員是位掩碼,用於指定當 poll 返回時,在該文件描述符上已經發生了哪些事情。
 */ 

  pollfd結構作用與select函數中的fd_set是相似的,pollfd每個描述符有兩個變量,一個爲調用值,另一個爲返回結果,fd_set則使用的是值-結果參數,但都是通過參數變化來返回結果。通過一些定義的宏來指定一些事件:
  POLLIN : 普通或優先級帶數據可讀;
  POLLOUT : 普通數據可寫;
  POLLPRI : 高優先級數據可讀;
  POLLERR :發生錯誤;
  POLLUP :發生掛起;
  …
  詳細宏說明可參考:poll()函數詳解

四、epoll

epoll是linux中獨有的函數,功能也是用於IO複用,但是是select和poll的改進。epoll包括3個函數。

int epoll_create(int size);
/*創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。
當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,
是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*epoll的事件註冊函數,它不同與select()是在監聽事件時告訴內核要監聽什麼類型的事件,
而是在這裏先註冊要監聽的事件類型。
第一個參數是epoll_create()的返回值,第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd,第四個參數是告訴內核需要監聽什麼事,
struct epoll_event結構如下:
struct epoll_event {
  __uint32_t events;  // Epoll events 
  epoll_data_t data;  // User data variable 
};
typedef union epoll_data
{
  void        *ptr;
  int          fd;
  __uint32_t   u32;
  __uint64_t   u64;
} epoll_data_t;

events可以是以下幾個宏的集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,
需要再次把這個socket加入到EPOLL隊列裏

關於epoll工作模式ET,LT
LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket,
在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。
如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。
傳統的select/poll都是這種模型的代表。
ET (edge-triggered)是高速工作方式,只支持no-block socket。
在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll告訴你。
然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,
直到你做了某些操作導致那個文件描述符不再爲就緒狀態了,但是請注意,
如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)

EPOLLLT——水平觸發(電平一直在1上就一直提醒)
EPOLLET——邊緣觸發(電平變化時提醒一次)
epoll有EPOLLLT和EPOLLET兩種觸發模式,LT是默認的模式,ET是“高速”模式。
LT模式下,只要這個fd還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作;
而在ET(邊緣觸發)模式中,它只會提示一次,直到下次再有數據流入之前都不會再提示了,無 論fd中是否還有數據可讀。
所以在ET模式下,read一個fd的時候一定要把它的buffer讀光,也就是說一直讀到read的返回值小於請求值,或者遇到EAGAIN錯誤。
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
/*等待事件的產生,類似於select()調用。
參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,
這個maxevents的值不能大於創建epoll_create()時的size,
參數timeout是超時時間(毫秒,0會立即返回,-1永久阻塞)。
該函數返回需要處理的事件數目,如返回0表示已超時。*/

epoll和select區別

五、select,poll,epoll的區別

select的幾大缺點:
(1)每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大;
(2)同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大;
(3)select支持的文件描述符數量太小了,默認是1024;

poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多。

epoll的解決方案在epoll_ctl函數中。每次註冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進內核,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。

epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)併爲每個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。

對於第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048。

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

select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,並且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裏的等待隊列並不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。

epoll的最大好處是不會隨着FD的數目增長而降低效率,在selec中採用輪詢處理,其中的數據結構類似一個數組的數據結構,而epoll是維護一個隊列,直接看隊列是不是空就可以了。epoll只會對”活躍”的socket進行操作—這是因爲在內核實現中epoll是根據每個fd上面的callback函數實現的。那麼,只有”活躍”的socket纔會主動的去調用 callback函數(把這個句柄加入隊列),其他idle狀態句柄則不會,在這點上,epoll實現了一個”僞”AIO。但是如果絕大部分的I/O都是“活躍的”,每個I/O端口使用率很高的話,epoll效率不一定比select高(可能是要維護隊列複雜)。

select、poll、epoll之間的區別總結[整理]

六、select 函數應用於客戶端實例

  在博客:Linux 套接字編程基礎中我們介紹了一個簡單的客戶/服務器程序,但是沒有解決當服務器崩潰或關機時客戶端馬上獲取信號並退出的問題,這裏使用select函數來實現。主要思想:原程序是直接阻塞在IO函數中,不能及時獲取服務器發送的FIN信號。這裏利用select函數對socket端口進行監聽,每當IO條件滿足時,執行相應的讀取操作。修改只限於客戶端,代碼如下:

/*
 * client.c
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>

void str_cli(FILE* fp, int sockfd);  //處理函數聲明

int main(int argc, char** argv)
{
    int sockfd;
    struct sockaddr_in servaddr;
    char* ips = "192.168.110.128";
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    if(inet_aton(ips, &servaddr.sin_addr) <= 0)
    {
        printf("inet_aton error!\n");
        exit(0);
    }
    if(connect(sockfd, (struct sockaddr*)& servaddr, sizeof(servaddr)) < 0)
    {
        printf("connect error!\n");
        exit(0);
    }
    str_cli(stdin, sockfd);
    exit(0);
}

void str_cli(FILE* fp, int sockfd)
{
    char sendline[256], recvline[256] = "";
    int maxfdpl, stdineof;
    int n;     //記錄讀取字節數
    fd_set rset;//select para

    stdineof = 0;
    FD_ZERO(&rset);
    printf("Input message to be send:\n");
    for(;;)
    {
        if(stdineof == 0)
        {
            FD_SET(fileno(fp), &rset);
        }
        FD_SET(sockfd, &rset);   //參數設置
        maxfdpl = (fileno(fp) > maxfdpl ? fileno(fp) : maxfdpl) + 1;
        select(maxfdpl, &rset, NULL, NULL, NULL);//調用select
        if(FD_ISSET(sockfd, &rset))  //socket is readable
        {//處理socket口
            if((n = read(sockfd, recvline, 256)) == 0)
            {
                if(stdineof == 1)
                {
                    printf("terminate normally\n");
                    return;
                }
                else
                {//服務器異常退出
                    printf("read error, server collapse!\n");//server collapse
                    exit(1);
                }
            }
            write(fileno(stdout), recvline, n);
            printf("Input message to be send:\n");
        }
        if(FD_ISSET(fileno(fp), &rset)) //input is readable
        {//處理標準輸入口
            if((n = read(fileno(fp), sendline, 256)) == 0)
            { //客戶端發起終止連接
                stdineof = 1;
                shutdown(sockfd, SHUT_WR);
                printf("client-shutdown\n");
                FD_CLR(fileno(fp), &rset);
                continue;
            }
             write(sockfd, sendline, n);
        }
    }
}

說明:套接字連接這裏不再解釋,主要說明str_cli 函數。
函數str_cli 中調用select 對IO口進行監聽,這裏有兩個描述符:sockfd,fileno(fp),一個是套接口,一個是標準輸入口(fileno()用於獲取fp的描述符),分別用於監聽套件字是否有數據要讀和標準輸入是否有數據輸入。
maxfdpl : select函數中描述符個數參數
stdineof : 用於標識標準輸入是否輸入了一個EOF,若輸入了EOF則不再設置rset參數,也就不在對這個IO口進行監聽。
程序正常退出過程: 如果客戶端輸入了EOF(Ctrl+D終止連接),read函數會返回0,那麼就執行shutdown函數關閉寫這一半,當服務器受到因shutdown發送的FIN,會返回一個FIN,客戶端的socket馬上能夠收到這個FIN,read函數返回0,執行return,程序正常退出。
服務器崩潰退出過程:如果服務器突然崩潰或關機,會發送一個FIN給客戶端,客戶端會馬上受到FIN,read函數返回0,這是不是客戶端發起的連接終止,stdineof不爲1,因此輸出錯誤信息,退出程序。

/* 測試
 * 客戶端:
 */
[centos@localhost Documents]$ ./a.out    //連接服務器
Input message to be send:
hello  
hello
Input message to be send:
are you ok
are you ok
Input message to be send:      //這裏輸入Ctrl+D,終止連接
client-shutdown
terminate normally
[centos@localhost Documents]$ ./a.out 
Input message to be send:
heiehi
heiehi
Input message to be send:     //這裏將服務器關閉,獲取輸出信息
read error, server collapse!

/* 服務器
 * 與上面對應
 */
receive message: hello
child 5130 terminated!
receive message: heihei
//這裏關閉服務器

代碼主要參考自:UNIX網絡編程卷1:套接字聯網API-chapter-6.7;

七、利用poll實現的服務器程序

  之前的服務器程序實現代碼是通過fork函數創建子進程來實現併發的,這裏利用poll函數來實現同樣的功能,客戶端爲上面的程序;

/* 服務器
 * poll實現併發
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <errno.h>
#include <poll.h>
#include <sys/wait.h>
#include <limits.h>
#define OPEN_MAX 10

int main(int argc, char** argv)
{
    int i, maxi, listenfd, connfd, sockfd;
    int nready;
    ssize_t n;
    socklen_t clilen;
    char buf[256];
    struct pollfd client[OPEN_MAX];
    struct sockaddr_in cliaddr, servaddr;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(6666); //what's is serv_port?

    bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr));

    listen(listenfd, 5); //5 is the number that can deal at the same time;
    client[0].fd = listenfd;
    client[0].events = POLLRDNORM;
    for(i = 1; i < OPEN_MAX; i++)
        client[i].fd = -1;
    maxi = 0;
    for(;;)
    {
        nready = poll(client, maxi+1, -1);
        if(client[0].revents & POLLRDNORM)
        {
            clilen = sizeof(cliaddr);
            connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &clilen);
            for(i = 1; i < OPEN_MAX; i++)
            {
                if(client[i].fd < 0)
                {
                    client[i].fd = connfd;
                    break;
                }
            }
            if(i == OPEN_MAX)
            {
                printf("too many clients");
                exit(1);
            }
            client[i].events = POLLRDNORM;
            if(i>maxi)
                maxi =i;
            if(--nready <= 0)
                continue;
        }
        for(i = 1; i <= maxi; i++)
        {
            if((sockfd = client[i].fd) < 0)
                continue;
            if(client[i].revents & (POLLRDNORM | POLLERR))
            {
                if((n = read(sockfd, buf, 256)) < 0)
                {
                    printf("error");
                    close(sockfd);
                    client[i].fd = -1;
                }
                else if(n == 0)
                {
                    char* sss = "connect closed!";
                    write(fileno(stdout), sss, strlen(sss));
                    close(sockfd);
                    client[i].fd = -1;
                }
                else
                {
                    write(fileno(stdout), buf, n);
                    write(sockfd, buf, n);
                }
                if(nready <= 0)
                    break;
            }
        }
    }
}

說明: maxi爲記錄當前pollfd參數結構中的元素數量-1;nready爲每次poll函數返回的就緒的描述符個數。
參數結構pollfd的第一個元素用於監聽套接字,每次監聽套接字在描述符就緒後,利用accept函數來獲取新的連接描述符並添加到參數結構pollfd中,若是其它元素描述符就緒則依次進行讀寫操作。
測試輸出結果與四中類似。

八、epoll 服務端實現

#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const int SERVER_PORT=1024;
//const char* SERVER_IP="127.0.0.1";
const int LISTEN_MAX = 36;
const size_t MAXEVENTS = 20;

size_t deal_fun(int rfd)
{
  char* buf = (char*)malloc(1024*1024);  //max file size: 1M
  int readnum;
  readnum = read(rfd,buf, 1024*1024);
  if(readnum <=0)
  {
      close(rfd);
      //events[i].data.fd = -1;
      return -1;
  }
  std::cout<<"read: " << buf ;//<< std::endl;
  write(rfd,buf, readnum);
  free(buf);

  return 0;
}
int main()
{
  int listenfd, epollfd;
  struct sockaddr_in cliaddr, servaddr;
  struct epoll_event event;
  struct epoll_event *events;

  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // this machine
  servaddr.sin_port = htons(SERVER_PORT); //

  listenfd = socket(AF_INET, SOCK_STREAM, 0); //ipve4 tcp
  //no block socket
  //int flags = fcntl (listenfd, F_GETFL, 0);
  //flags |= O_NONBLOCK;
  //fcntl (listenfd, F_SETFL, flags);
  //is needed ??
  bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr));
  listen(listenfd, LISTEN_MAX);

  events = (epoll_event *)calloc(MAXEVENTS, sizeof(event));
  event.data.fd = listenfd;
  event.events = EPOLLIN;
  epollfd = epoll_create(3);   //3 is random no use
  epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&event);  //add fd to epool

  while(1)
  {
    int readyfds;
    readyfds = epoll_wait(epollfd, events, MAXEVENTS, -1);

    for(int i=0; i<readyfds; i++)
    {
      if(events[i].data.fd == listenfd)    //new connect
      {
        int connectfd;
        socklen_t addlen;
        connectfd = accept(listenfd, (sockaddr *)&cliaddr, &addlen);
        if(connectfd == -1)
        {
          LOG("connect erro\n");
          break;
        }
        char *str = inet_ntoa(cliaddr.sin_addr);
        std::cout << "accapt a connection from " << str << std::endl;

        event.data.fd = connectfd;
        event.events = EPOLLIN;
        epoll_ctl(epollfd, EPOLL_CTL_ADD, connectfd, &event);
      }
      else    //data to read
      {
        int rfd = events[i].data.fd;
        if(rfd <0)
          continue;

        if(deal_fun(rfd) == -1)
        {
          events[i].data.fd = -1;
        }
      }
    }
  }

  free (events);
  close (epollfd);
}

epoll使用詳解(精髓)
通過完整示例來理解如何使用 epoll

九、水平或邊緣觸發與阻塞與非阻塞的關係

套接字可以設置爲阻塞或非阻塞的,使用epoll監聽套接字可以使用水平觸發模式或邊緣觸發模式。
1. 監聽套接字水平模式:套接字阻塞:正常連接;套接字非阻塞:正常連接
2. 監聽套接字邊緣模式:套件字阻塞:正常連接;套接字非阻塞:正常連接可能會出現連接失敗問題(while循環accept解決)
對於監聽套接字,當使用邊緣模式時,如果是非阻塞,當accept時,可能並不會讀取完所有連接。
3. 連接套接字水平模式(與客戶端的連接):套接字阻塞:正常讀寫;套接字非阻塞:正常讀寫
4. 連接套接字邊緣模式: 套件字阻塞:讀寫會有問題,可能會阻塞程序,套接字非阻塞:正常讀寫

套接字的阻塞表示在對套接字進行處理(讀寫)時,當沒有數據可以操作時,阻塞直到有數據返回。
使用邊緣模式讀取數據時需要進行while循環讀取,不然會導致數據讀取不完全的問題,這裏就只能使用非阻塞的套接字,因爲如果使用阻塞套接字,當while讀完所有數據時,會阻塞在讀操作上無法返回。

一般來說監聽套接字使用水平觸發,阻塞就可以了。
對於連接客戶端的讀寫套接字在水平觸發模式下,阻塞和非阻塞效果都一樣,建議設置非阻塞。邊緣觸發模式下,必須使用非阻塞 IO,並要求一次性地完整讀寫全部數據。

實例淺析epoll的水平觸發和邊緣觸發,以及邊緣觸發爲什麼要使用非阻塞IO
邊緣觸發到底有什麼優勢

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章