IO多路轉接—select 、poll、epoll

多路(多個文件描述符)I/O(輸入輸出)轉接(將多個文件描述符交給select監控)

調用select函數,直到描述符表中有一個描述符準備好進入I/O時,該函數才返回,通過select的返回值告知進程哪些描述符已經準備好進入I/O。

三種模型性能分析 

select

1.select能監聽的文件描述符個數受限於FD_SETSIZE,一般爲1024,單純改變進程打開的文件描述符個數並不能改變select監聽文件個數
2.解決1024以下客戶端時使用select是很合適的,但如果鏈接客戶端過多,select採用的是輪詢模型,會大大降低服務器響應效率,不應在select上投入更多精力

readfds、writefds和exceptfds是指向描述符集的指針。這三個描述符集說明了我們關心的可讀、可寫或處於異常條件的各個描述符。每個描述符集存放在一個fd_set數據類型中。這種數據類型爲每一個可能的描述符保持了一位。

三者中任意一個或全部都可以是空指針,這表示對相應狀態並不關心。 

理解select模型:

理解select模型的關鍵在於理解fd_set,爲說明方便,取fd_set長度爲1字節,fd_set中的每一bit可以對應一個文件描述符fd。則1字節長的fd_set最大可以對應8個fd。 
(1)執行fd_set set; FD_ZERO(&set);則set用位表示是0000,0000。 
(2)若fd=5,執行FD_SET(fd,&set);後set變爲0001,0000(第5位置爲1) 
(3)若再加入fd=2,fd=1,則set變爲0001,0011 
(4)執行select(6,&set,0,0,0)阻塞等待  //其中6=5+1
(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變爲0000,0011。注意:沒有事件發生的fd=5被清空。

 

void FD_CLR(int fd, fd_set *set); 把文件描述符集合裏fd清0

 

返回值:

返回準備就緒的描述符,所以正返回值表示已經準備好的描述符

若超時則返回0,表示沒有描述符準備好

若出錯則返回-1

使用select函數的過程一般是:
1、調用宏FD_ZERO將指定的fd_set清零

  1. 調用宏FD_SET將需要測試的fd加入fd_set
  2. 調用函數select測試fd_set中的所有fd

用宏FD_ISSET檢查某個fd在函數select調用後,相應位是否仍然爲1。
以下是一個測試單個文件描述字可讀性的例子:

poll

不同select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。 

其中,pollfd是一個結構體,裏面包含就緒的文件描述符及其事件:

所以pollfd結構體整體的含義就指的是關心的fd上的event事件

其中fd+events就相當於select接口裏fd_set中的內容。

timeout是一個定時器,單位是ms,不同的值有不同的含義,如下表:

timeout

含義

-1

IO不阻塞,此時poll可以任意進行IO

大於0

隔一段時間阻塞,此時poll每隔一段時間IO被阻塞一次

0

一直阻塞,此時poll不能進行

 

返回值:
由於poll返回時會將該文件描述符上的就緒事件放入revents中,所以其返回值就是就緒事件fd的個數,如下表所示:

返回值

含義

0

超時(timeout)

<0

出錯

大於0

就緒事件的個數

Code用poll監控輸入輸出

Step1:定義pollfd結構體並將timeout設爲0

Step2:加入主事件循環,調用poll接口監控標準輸入

所以POLLIN 和POLLOUT都是宏定義,對服務器而言,這兩個宏就表示輸入輸出事件。

 

epoll API(函數)

 

  1. 創建一個epoll句柄,參數size用來告訴內核監聽的文件描述符個數,跟內存大小有關
  2. 控制某個epoll監控的文件描述符上的事件:註冊、修改、刪除。
  3. 等待所監控文件描述符上事件的產生,類似於select()調用。
     

注:句柄是一個標識符,是拿來標識對象或者項目的,它就象我們的姓名一樣,每個人都會有一個,不同的人的姓名不一樣,但是,也可能有一個名字和你一樣的人。從數據類型上來看它只是一個16位的無符號整數。應用程序幾乎總是通過調用一個WINDOWS函數來獲得一個句柄,之後其他的WINDOWS函數就可以使用該句柄,以引用相應的對象。

 

events可以是以下幾個宏的集合:

參考:http://kimi.it/515.html

LT模式 和 ET模式:

下面我們用一個快遞員配送快遞的例子來解釋一下ET模式:

假如 : 
1. 我有5個快遞,當一個快遞到的時候,快遞員就打電話讓你取,一直打直到你把這個快遞取走爲止,下一個你的來了依然如此;很顯然這樣的快遞員工作方式效率會很慢。

上面的就是屬於LT模式;

2.. 同樣的,如果你有5個快遞,當一個快遞到的時候,快遞員第一次給你送的的時候打一次電話,你不來他就替你收着(而這個時候,快遞員不會等你),第二個你的來了再給你打一次,你不來他依然替你收着,每次只有快遞數量變化的時候纔會打電話,這個時候只有你哪一次有時間,將所有的快遞都拿走。此種方式效率較高:因爲快遞員並沒有去等

這種模式屬於 ET模式。

下面我來介紹一下兩者之間的特點:

首先了解:

在一個非阻塞的socket上調用read/write函數, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
從字面上看, 意思是:EAGAIN: 再試一次,EWOULDBLOCK: 如果這是一個阻塞socket, 操作將被block,perror輸出: Resource temporarily unavailable

總結:
這個錯誤表示資源暫時不夠,能read時,讀緩衝區沒有數據,或者write時,寫緩衝區滿了。遇到這種情況,如果是阻塞socket,read/write就要阻塞掉。而如果是非阻塞socket,read/write立即返回-1, 同時errno設置爲EAGAIN。
所以,對於阻塞socket,read/write返回-1代表網絡出錯了。但對於非阻塞socket,read/write返回-1不一定網絡真的出錯了。可能是Resource temporarily unavailable。這時你應該再試,直到Resource available。


 
 LT模式下只要某個socket處於readable/writable狀態,無論什麼時候  epoll_wait都會返回該socket(通知);
    1. 當epoll檢測到socket上的事件就緒時,可以不立即處理或者只處理一部分
    (例如:2KB的數據好了,此時可以一次讀1KB,然後剩1KB)
    2. 在第二次調用epoll_wait的時候它依然會立即通知你,並且通知socket的讀事件就緒
       直到緩存區內的數據都讀完了,epoll_wait纔不會立即返回
    3. 支持非阻塞與阻塞

ET模式下只有某個socket從unreadable變爲readable或從unwritable變爲writable時,epoll_wait纔會返回該socket(通知)。

   1. 當epoll檢測到socket上的事件就緒時,必須立即處理
   (例如:2KB的數據好了,此時可以一次讀1KB,然後剩1KB)
   2. 但是在第二次調用epoll_wait的時候,它不再立即返回通知你
      也就是說,ET模式下,數據就緒以後只有一次處理機會,所以要麼不讀,要麼讀完,
      不會有隻讀一部分的情況
      (只有在數據從 無變有 或者 少變多 的時候,纔會通知你)
   3. 性能比LT高
   4. 只能採用非阻塞

 在epoll的ET模式下,正確的讀寫方式爲:
讀:只要可讀,就一直讀,直到返回0,或者 errno = EAGAIN
寫:只要可寫,就一直寫,直到數據發送完,或者 errno = EAGAIN

另外爲什麼ET模式只支持非阻塞讀寫呢?

   因爲: 數據就緒只通知一次,必須在通知後,一次處理完
         也就是說:如果使用ET模式,當數據就緒的時候就要一直讀,直到數據讀完爲止

     1. 但是如果當前的fd是阻塞的,而讀是循環的:那麼在讀完緩存區的時候,
        如果對端還沒有數據寫進來,那麼該read函數就會一直阻塞,
        這不符合邏輯,不能這麼使用 
      2. 那麼就需要將fd設置成非阻塞,當沒有數據的時候,read雖然讀取不到任何的數據,
      但是肯定不會被阻塞住,其會返回-1,並且errno被設置爲EAGAIN.那麼此時說明緩衝區內數據已經讀完,read返回繼續後序的邏輯

下面介紹使用epoll的具體步驟(之前一直困惑的地方):

大致流程:

 首先通過create_epoll(int maxfds)來創建一個epoll的句柄,其中maxfds爲你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,之後的所有操作 將通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個創建出來的epoll句柄。之後在你的網絡主循環裏面,每一幀的調用 epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的語法爲:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
    其中kdpfd爲用epoll_create創建之後的句柄,events是一個 epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裏面將儲存所有的讀寫事件。 max_events是當前需要監聽的所有socket句柄數。最後一個timeout是 epoll_wait的超時,爲0的時候表示馬上返回,爲-1的時候表示一直等下去,直到有事件範圍,爲任意正整數的時候表示等這麼長的時間,如果一直沒 有事件,則範圍。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環 的效率。

具體流程以及代碼示例

 Epoll模型主要負責對大量併發用戶的請求進行及時處理,完成服務器與客戶端的數據交互。其具體的實現步驟如下:
(a) 使用epoll_create()函數創建文件描述,設定將可管理的最大socket描述符數目。
(b) 創建與epoll關聯的接收線程,應用程序可以創建多個接收線程來處理epoll上的讀通知事件,線程的數量依賴於程序的具體需要。
(c) 創建一個偵聽socket描述符ListenSock;將該描述符設定爲非阻塞模式,調用Listen()函數在套接字上偵聽有無新的連接請求,在 epoll_event結構中設置要處理的事件類型EPOLLIN,工作方式爲 epoll_ET,以提高工作效率,同時使用epoll_ctl()註冊事件,最後啓動網絡監視線程。
(d) 網絡監視線程啓動循環,epoll_wait()等待epoll事件發生。
(e) 如果epoll事件表明有新的連接請求,則調用accept()函數,將用戶socket描述符添加到epoll_data聯合體,同時設定該描述符爲非 阻塞,並在epoll_event結構中設置要處理的事件類型爲讀和寫,工作方式爲epoll_ET.
(f) 如果epoll事件表明socket描述符上有數據可讀,則將該socket描述符加入可讀隊列,通知接收線程讀入數據,並將接收到的數據放入到接收數據 的鏈表中,經邏輯處理後,將反饋的數據包放入到發送數據鏈表中,等待由發送線程發送。

int main()
{
   int sock = socket(AF_INET, SOCK_STREAM, 0);
	if( sock < 0 ){
		perror("socket");
		exit(1);
	}

	//Address already in use
	int opt = 1;
	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_addr.s_addr = inet_addr(ip);
	local.sin_port = htons(port);

	if( bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0 ){
		perror("bind");
		exit(2);
	}

	if( listen(sock, 5) < 0 ){
		perror("listen");
		exit(3);
	}
    
    int listen_sock=sock;
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
        int client_sock=-1;

          // I/O多路複用
          struct epoll_event ev;
          struct epoll_event events[20];
          int epfd;
          int nfds=0;// 用來接收epoll_wait的返回值,表示非阻塞的文件描述符的數量
          epfd=epoll_create(256);
          ev.data.fd=listen_sock;
          ev.events=EPOLLIN;// 當綁定的那個socket文件描述符可讀的時候,就觸發事件(默認水平觸發)
       epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev); // 把綁定的n個socket文件描述符添加到內核的紅黑樹裏面
while (1) {
nfds = epoll_wait(epfd, events, 20, 0);
//epoll_wait範圍之後應該是一個循環,遍利所有的事件:
for (n = 0; n < nfds; ++n)
{
        if (events[n].data.fd == listen_sock)
        {//如果是主socket的事件的話,則表示有新連接進入了,進行新連接的處理。
                client_sock = accept (listen_sock, (struct sockaddr *) &client, &len);
                if (client_sock < 0)
                {
                        perror ("accept");
                        continue;
                }
                setnonblocking (client);        // 將新連接置於非阻塞模式
                ev.events = EPOLLIN | EPOLLET; // 並且將新連接也加入EPOLL的監聽隊列。
                //注意,這裏的參數EPOLLIN | EPOLLET並沒有設置對寫socket的監聽,
                //如果有寫操作的話,這個時候epoll是不會返回事件的,
               //如果要對寫操作也監聽的話,應該是EPOLLIN | EPOLLOUT | EPOLLET
                ev.data.fd = client_sock;
                if (epoll_ctl (epfd, EPOLL_CTL_ADD, client_sock, &ev) < 0)
                {
                        /*
                                設置好event之後,將這個新的event通過epoll_ctl加入到epoll的監聽隊列裏面,
                                這裏用EPOLL_CTL_ADD來加一個新的epoll事件,通過EPOLL_CTL_DEL來減少一個epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式.
                        */
                        fprintf (stderr, "epoll set insertion error: fd=%d", client_sock);
                         return -1;
                 }
        }
        else          
 {                                                  
   // 如果不是主socket的事件的話,則代表是一個用戶socket的事件,
   do_use_fd (events[n].data.fd); 
   //則來處理這個用戶socket的事情,比如說 read(fd,xxx)之類的,或者一些其他的處理。
 }
   close(epfd);
   close(listen_sock);
   return 0;
 }

關於內存映射技術mmap:

mmap()系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以向訪問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。

注:實際上,mmap()系統調用並不是完全爲了用於共享內存而設計的。它本身提供了不同於一般對普通文件的訪問方式,進程可以像讀寫內存一樣對普通文件的操作。而Posix或系統V的共享內存IPC則純粹用於共享目的,當然mmap()實現共享內存也是其主要應用之一。

epoll模型分析:

(1)調用epoll_create創建epoll模型的時候,實際上是在內核區創建了一棵空的紅黑樹和一個空的隊列;

(2)調用epoll_ctl的時候,實際上是在往紅黑樹中添加結點,結點描述的是文件描述符及其上的對應事件;

(3)當某文件描述符上的某事件就緒的時候操作系統會創造一個結點放在隊列中(此結點表示此文件描述符上的此事件就緒),這個隊列通過內存映射機制讓用戶看到。

epoll相對於select和poll的改進:

  1. 當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這裏也使用了內存映射(mmap)技術共享同一塊存儲,避免了fd從內核賦值到用戶空間。這樣便徹底省掉了這些文件描述符在系統調用時複製的開銷。
  2. epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。

Select、Poll、Epoll的特點:

Select、Poll、Epoll區別:

 基於epoll的簡單服務器

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

void handler_events(int epfd,struct epoll_event revs[],int num,int listen_sock)
{
    struct epoll_event ev;
    int i = 0;
    for( ; i < num; i++ )
    {
    int fd = revs[i].data.fd;

    // 如果是監聽文件描述符,則調用accept接受新連接

    if( fd == listen_sock && (revs[i].events & EPOLLIN) )
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int new_sock = accept(fd,(struct sockaddr *)&client,&len);

        if( new_sock < 0 )
        {
        perror("accept fail ... \n");
        continue;
        }

       printf("get a new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));

       //因爲只是一個http協議:連接成功後,下面就是要 請求和響應 
       // 而服務器端響應之前:要先去讀客戶端要請求的內容

       ev.events = EPOLLIN;
       ev.data.fd = new_sock;
       epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);

       continue;
    }

    // 如果是普通文件描述符,則調用read提供讀取數據的服務

    if(revs[i].events & EPOLLIN)
    {
        char buf[10240];
        ssize_t s = read(fd,buf,sizeof(buf)-1);
        if( s > 0 )// 讀成功了
        {
        buf[s] = 0;
        printf(" %s ",buf);

        // 讀成功後,就是要給服務端響應了
        // 而這裏的事件是隻讀事件,所以要進行修改

        ev.events = EPOLLOUT;// 只寫事件
        ev.data.fd = fd;
        epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);// 其中EPOLL_CTL_MOD 表示修改

        }

        else if( s == 0 )
        {
        printf(" client quit...\n ");
        close(fd);// 這裏的fd 就是 revs[i].fd
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);// 連接關閉,那麼就要把描述該連接的描述符關閉
        }
        else// s = -1 失敗了
        {   
        printf("read fai ...\n");
        close(fd);// 這裏的fd 就是 revs[i].fd
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);// 連接關閉,那麼就要把描述該連接的描述符關閉
        }
        continue;
    }

    // 服務器端給客戶端響應: 寫

    if( revs[i].events & EPOLLOUT )
    {
        const char* echo = "HTTP/1.1 200 ok \r\n\r\n<html>hello epoll server!!!</html>\r\n";
        write(fd,echo,strlen(echo));
        close(fd);
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
    }
    }
}

int startup( int port )
{
    // 1. 創建套接字
    int sock = socket(AF_INET,SOCK_STREAM,0);//這裏第二個參數表示TCP
    if( sock < 0 )
    {
    perror("socket fail...\n");
    exit(2);
    }

    // 2. 解決TIME_WAIT時,服務器不能重啓問題;使服務器可以立即重啓
    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);// 地址爲任意類型
    local.sin_port = htons(port);// 這裏的端口號也可以直接指定8080
    // 3. 綁定端口號

    if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0 )
    {
    perror("bind fail...\n");
    exit(3);
    }

    // 4. 獲得監聽套接字
    if( listen(sock,5) < 0 )
    {
    perror("listen fail...\n");
    exit(4);
    }
    return sock;
}

int main(int argc,char* argv[] )
{
    if( argc != 2 )
    {
    printf("Usage:%s port\n ",argv[0]);
    return 1;
    }

    // 1. 創建一個epoll模型: 返回值一個文件描述符

    int epfd = epoll_create(256);
    if( epfd < 0 )
    {
    perror("epoll_create fail...\n");
    return 2;
    }

    // 2. 獲得監聽套接字

   int listen_sock = startup(atoi(argv[1]));//端口號傳入的時候是以字符串的形式傳入的,需要將其轉爲整型


    // 3. 初始化結構體----監聽的結構列表

    struct epoll_event  ev;
    ev.events = EPOLLIN;//關心讀事件
    ev.data.fd = listen_sock;// 關心的描述文件描述符

    // 4. epoll的事件註冊函數---添加要關心的文件描述符的只讀事件

    epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);

    struct epoll_event revs[128];
    int n = sizeof(revs)/sizeof(revs[0]);

    int timeout = 3000;
    int num = 0;


    while(1)
    {

       // 5 . 開始調用epoll等待所關心的文件描述符集就緒

       switch( num = epoll_wait(epfd,revs,n,timeout) )
       {
       case 0:// 表示詞狀態改變前已經超過了timeout的時間
           printf("timeout...\n");
           continue;
       case -1:// 失敗了
           printf("epoll_wait fail...\n");
           continue;
       default: // 成功了

           handler_events(epfd,revs,num,listen_sock);
           break;
       }
    }
    close(epfd);
    close(listen_sock);
    return 0;
}

 

發佈了79 篇原創文章 · 獲贊 110 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章