(五十五)高併發服務器——多路IO轉接機制epoll模型

本文部分轉載於:
http://blog.csdn.net/wqx521/article/details/53783029
http://blog.csdn.net/orz415678659/article/details/8958415

   epoll是什麼?按照man手冊的說法:是爲處理大批量句柄而作了改進的poll。當然,這不是2.6內核纔有的,它是在2.5.44內核中被引進的(epoll(4) is a new API introduced in Linuxkernel 2.5.44),它幾乎具備了之前所說的一切優點,被公認爲Linux2.6下性能最好的多路I/O就緒通知方法。
  epoll是Linux下多路複用IO接口select/poll的增強版本,它能顯著提高程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率,因爲它會複用文件描述符集合來傳遞結果而不用迫使開發者每次等待事件之前都必須重新準備要被偵聽的文件描述符集合,另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。
  目前epell是linux大規模併發網絡程序中的熱門首選模型。
  epoll除了提供select/poll那種IO事件的電平觸發(Level Triggered)外,還提供了邊沿觸發(Edge Triggered只告訴進程哪些文件描述符剛剛變爲就緒狀態,它只說一遍,如果我們沒有採取行動,那麼它將不會再次告知,這種方式稱爲邊緣觸發),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。
  epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這裏也使用了內存映射(mmap)技術,這樣便徹底省掉了這些文件描述符在系統調用時複製的開銷。
  另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。
  
epoll有2種工作方式:LT和ET:
  LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。
  ET (edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核通過epoll告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再爲那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再爲就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。
  
  ET和LT的區別就在這裏體現,LT事件不會丟棄,而是隻要讀buffer裏面有數據可以讓用戶讀,則不斷的通知你。而ET則只在事件發生之時通知。可以簡單理解爲LT是水平觸發,而ET則爲邊緣觸發。LT模式只要有事件未處理就會觸發,而ET則只在高低電平變換時(即狀態從1到0或者0到1)觸發。
  


相關API:
1、創建一個epoll句柄,參數size用來告訴內核監聽的文件描述符個數

#include <sys/epoll.h>

int epoll_create(int size)
                     size:告訴內核監聽的數目

2、控制某個epoll監控的文件描述符上的事件:註冊、修改、刪除。

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epfd:爲epoll_creat的句柄
op:  表示動作,用3個宏來表示:
        EPOLL_CTL_ADD (註冊新的fd到epfd),
        EPOLL_CTL_MOD (修改已經註冊的fd的監聽事件),
        EPOLL_CTL_DEL (從epfd刪除一個fd);
event:告訴內核需要監聽的事件
        struct epoll_event {
            __uint32_t events; /* Epoll events */
            epoll_data_t data; /* User data variable */
        };
        其中上面的結構體中的events可選參數是:
            EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);  
            EPOLLOUT:表示對應的文件描述符可以寫;  
            EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);  
            EPOLLERR:表示對應的文件描述符發生錯誤;  
            EPOLLHUP:表示對應的文件描述符被掛斷;  
            EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(LevelTriggered)來說的。  
            EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏  
        其中上面結構體中的epoll_data_t結構體爲:
            typedef union epoll_data{    
                    void *ptr;    
                    int fd;    
                    uint32_t u32;    
                    uint64_t u64;  
            } epoll_data_t;  
        一般會把註冊的文件描述符放進去,這樣當事件被激活時,返回epoll_event結構體,這樣在epoll_event的data中就可以知道是哪一個被激活了。

3、等待所監控文件描述符上有事件的產生,類似於select()調用。

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

events:用來從內核得到事件的集合,
maxevents:告之內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size,
timeout:是超時時間
        -1:阻塞
        0:立即返回,非阻塞
        >0:指定微秒

返回值:成功返回有多少文件描述符就緒,時間到時返回0,出錯返回-1

  
  
  查看一個進程打開大數目的socket描述符:

cat /proc/sys/fs/file-max

  設置最大打開文件描述符限制

sudo vi /etc/security/limits.conf
寫入以下配置,soft軟限制,hard硬限制
*     soft    nofile    65536
*     hard   nofile    100000


C/S模型舉例:
注:其中wrap.h和wrap.c請查看第五十篇博客

/* service.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include "wrap.h"

#define MAXLINE 80      //一次讀read的長度
#define SERV_PORT 8000  //端口號
#define OPEN_MAX 1024   //最大epoll監控的數量,即可監控的客戶端

int main(int argc, char *argv[])
{
    int i, j, maxi, listenfd, connfd, sockfd;
    int nready, efd, res;
    ssize_t n;
    char buf[MAXLINE], str[INET_ADDRSTRLEN];//buf爲read到的數據,str爲客戶端IP地址用於打印
    socklen_t clilen;
    int client[OPEN_MAX];
    struct sockaddr_in cliaddr, servaddr;
    struct epoll_event tep, ep[OPEN_MAX];//ep爲活躍的客戶端,即有信息到達

    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(SERV_PORT);
    Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

    Listen(listenfd, 20);

    /* 初始化客戶表,可以不需要使用該client */
    for (i = 0; i < OPEN_MAX; i++)
        client[i] = -1;
    maxi = -1;

    /* 創建一個epoll樹,並將其句柄保存到efd中 */
    efd = epoll_create(OPEN_MAX);
    if (efd == -1)
        perr_exit("epoll_create");

    /******************開始註冊*********************/
    tep.events = EPOLLIN;   //註冊監聽讀事件
    tep.data.fd = listenfd; //當事件激活返回時,通過data知道是哪個文件描述符
    res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);//註冊
    /*****************註冊結束**********************/
    if (res == -1)
        perr_exit("epoll_ctl");

    while(1)
    {
        nready = epoll_wait(efd, ep, OPEN_MAX, -1); /* 阻塞監聽*/
        if (nready == -1)
            perr_exit("epoll_wait");

        for (i = 0; i < nready; i++)
        {
            if (!(ep[i].events & EPOLLIN))
                continue;//該文件沒有讀數據到達 則跳過

            if (ep[i].data.fd == listenfd)
            {//該文件有數據到達,則data.fd中保存的就是該文件的文件描述符
             //如果是listendfd則接收客戶端請求,建立socket鏈接
                clilen = sizeof(cliaddr);
                connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
                printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));

                //將用於和客戶端通信的文件描述符保存到client[]數組中
                for (j = 0; j < OPEN_MAX; j++)
                    if (client[j] < 0)
                    {
                        client[j] = connfd; /* save descriptor */
                        break;
                    }
                if (j == OPEN_MAX)
                    perr_exit("too many clients");
                if (j > maxi)
                    maxi = j; /* max index in client[] array */

                /* 將客戶端的文件描述符也註冊到epoll樹中,加以監聽 */
                tep.events = EPOLLIN;
                tep.data.fd = connfd;
                res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
                if (res == -1)
                    perr_exit("epoll_ctl");
            }
            else
            {//如果客戶端有數據到達
                sockfd = ep[i].data.fd;//從data.fd中取出和客戶端通信的socket

                n = Read(sockfd, buf, MAXLINE);

                if (n == 0)
                {//客戶端斷開
                    for (j = 0; j <= maxi; j++)
                    {//將該處的用戶置爲-1
                        if (client[j] == sockfd)
                        {
                            client[j] = -1;
                            break;
                        }
                    }
                    //刪除監聽該客戶端
                    res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
                    if (res == -1)
                        perr_exit("epoll_ctl");
                    //關閉該文件描述符socket
                    Close(sockfd);
                    printf("client[%d] closed connection\n", j);
                }
                else
                {
                    for (j = 0; j < n; j++)
                        buf[j] = toupper(buf[j]);
                    Writen(sockfd, buf, n);
                }
            }
        }
    }
    close(listenfd);
    close(efd);
    return 0;
}
/* client.c */

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, n;
    sockfd = Socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);
    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    while (fgets(buf, MAXLINE, stdin) != NULL)
    {
        Write(sockfd, buf, strlen(buf));
        n = Read(sockfd, buf, MAXLINE);
        if (n == 0)
            printf("the other side has been closed.\n");
        else
            Write(STDOUT_FILENO, buf, n);
    }
    Close(sockfd);
    return 0;
}
發佈了73 篇原創文章 · 獲贊 17 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章