1. 程式人生 > >Linux網路程式設計---I/O複用模型之epoll

Linux網路程式設計---I/O複用模型之epoll

Linux網路程式設計—I/O複用模型之epoll

1. epoll模型簡介

epoll是Linux多路服用IO介面select/poll的加強版,e對應的英文單詞就是enhancement,中文翻譯為增強,加強,提高,充實的意思。所以epoll模型會顯著提高程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率。

  • epoll把使用者關心的檔案描述符上的時間放在核心的一個事件表中,無需像select和poll那樣每次呼叫都重複傳入檔案描述符集。
  • epoll在獲取事件的時候,無需遍歷整個被監聽的檔案描述符集合,而是遍歷那些被核心IO事件非同步喚醒而加入ready佇列的描述符集合。

所以,epoll是Linux大規模高併發網路程式的首選模型。

2.epoll模型的API

epoll使用一組函式來完成任務

2.1 函式epoll_create

建立一個epoll控制代碼,控制代碼的英文是handle,相通的意思是把手,把柄。

#include <sys/epoll.h>

int epoll_create(int size);
//返回值:若成功,返回一個非負的檔案描述符,若出錯,返回-1。
  • 該函式返回一個檔案描述符,用來唯一標示核心中這個事件表,sizeof引數提示核心要監聽的檔案描述符個數,這與記憶體大小有關。
  • 返回的檔案描述符將是其他所有epoll系統呼叫的第一個引數,以指定要訪問的核心時間表,所以用該返回的檔案描述符相當與其他epoll呼叫的把手、把柄一樣。

檢視程序能夠開啟的最大數目的檔案描述符

➜  ~ cat /proc/sys/fs/file-max
1215126
//該值與記憶體大小有關

修改最大檔案描述符限制

➜  ~ sudo vim /etc/security/limits.conf
//重啟生效

2.2 函式epoll_ctl

該函式用來操作epoll的核心事件表

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//返回值:若成功,返回0,若出錯返回-1。
  • epfd就是函式epoll_create建立的控制代碼。
  • op是指定操作型別,有一下三種
    • EPOLL_CTL_ADD,向epfd註冊fd的上的event
    • EPOLL_CTL_MOD,修改fd已註冊的event
    • EPOLL_CTL_DEL,從epfd上刪除fd的event
      1. fd是操作的檔案描述符
      2. event指定核心要監聽事件,它是struct epoll_event結構型別的指標。epoll_event定義如下:
struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};
  • events成員描述事件型別,將以下巨集定義通過位或方式組合

    • EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉)
    • POLLOUT:表示對應的檔案描述符可以寫
    • EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來)
    • EPOLLERR:表示對應的檔案描述符發生錯誤
    • EPOLLHUP:表示對應的檔案描述符被結束通話;
    • EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的
    • EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡
  • data用於儲存使用者資料,是epoll_data_t結構型別,該結構定義如下:

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;
  • epoll_data_t是一個聯合體,fd指定事件所從屬的目標檔案描述符。ptr可以用來指定fd相關的使用者資料,但兩者不能同時使用。

2.3 函式epoll_wait

函式epoll_wait用來等待所監聽檔案描述符上有事件發生

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//返回值:若成功,返回就緒的檔案描述符個數,若出錯,返回-1,時間超時返回0
  • epfd就是函式epoll_create建立的控制代碼
  • timeout是超時事件,-1為阻塞,0為立即返回,非阻塞,大於0是指定的微妙
  • events是一個 傳入傳出引數,它是epoll_event結構的指標,用來從核心得到事件的集合
  • maxevents告知核心events的大小,但不能大於epoll_create()時建立的size

3. LT和ET模式

  • LT(Level Triggered,電平觸發):LT模式是epoll預設的工作模式,也是select和poll的工作模式,在LT模式下,epoll相當於一個效率較高的poll。
    • 採用LT模式的檔案描述符,當epoll_wait檢測到其上有事件發生並將此事件通知應用程式後,應用程式可以不立即處理此事件,當下一次呼叫epoll_wait是,epoll_wait還會將此事件通告應用程式。
  • ET(Edge Triggered,邊沿觸發):當呼叫epoll_ctl,向引數event註冊EPOLLET事件時,epoll將以ET模式來操作該檔案描述符,ET模式是epoll的高效工作模式.
    • 對於採用ET模式的檔案描述符,當epoll_wait檢測到其上有事件發生並將此通知應用程式後,應用程式必須立即處理該事件,因為後續的epoll_wait呼叫將不在嚮應用程式通知這一事件。ET模式降低了同意epoll事件被觸發的次數,效率比LT模式高。

4. LT和ET的服務端和客戶端程式碼

4.1 伺服器端

#include <sys/epoll.h>
#include <fcntl.h>
#include "wrap.h"

#define MAX_EVENT_NUM           1024
#define BUFFER_SIZE             10
#define true                    1
#define false                   0

int setnonblocking(int fd)
{
        int old_opt = fcntl(fd, F_GETFD);
        int new_opt = old_opt | O_NONBLOCK;
        fcntl(fd, F_SETFD, new_opt);

        return old_opt;
}//將檔案描述符設定為非阻塞的

void addfd(int epollfd, int fd, int enable_et)
{
        struct epoll_event event;
        event.data.fd = fd;
        event.events = EPOLLIN;
        if(enable_et){
                event.events |= EPOLLET;
        }
        epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
//      setnonblocking(fd);
}//將檔案描述符fd的EPOLLIN註冊到epollfd指示的epoll核心事件表中,enable_et表示是否對fd啟用ET模式

void lt(struct epoll_event *events, int num, int epollfd, int listenfd)
{
        char buf[BUFFER_SIZE];
        for(int i = 0; i < num; i++){
                int sockfd = events[i].data.fd;
                if(sockfd == listenfd){
                        struct sockaddr_in clientaddr;
                        socklen_t clilen = sizeof(clientaddr);
                        int connfd = Accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);
                        addfd(epollfd, connfd, false);//對connfd使用預設的lt模式
                }else if(events[i].events & EPOLLIN){//只要socket讀快取中還有未讀的資料,這段程式碼就會觸發
                        printf("event trigger once\n");
                        memset(buf, '\0', BUFFER_SIZE);
                        int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
                        if(ret <= 0){
                                Close(sockfd);
                                continue;
                        }
                        printf("get %d bytes of content:%s\n", ret, buf);
                }else{
                        printf("something else happened\n");
                }
        }
}

void et(struct epoll_event *event, int num, int epollfd, int listenfd)
{
        char buf[BUFFER_SIZE];
        for(int i = 0; i < num; i++){
                int sockfd = event[i].data.fd;
                if(sockfd == listenfd){
                        struct sockaddr_in clientaddr;
                        int clilen = sizeof(clientaddr);
                        int connfd = Accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);
                        addfd(epollfd, connfd, true);//多connfd開啟ET模式
                }else if(event[i].events & EPOLLIN){
                        printf("event trigger once\n");
                        while(1){//這段程式碼不會重複觸發,所以要迴圈讀取資料
                                memset(buf, '\0', BUFFER_SIZE);
                                int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
                                if(ret < 0){
                                        if((errno == EAGAIN) || (errno == EWOULDBLOCK)){
                                                printf("read later\n");
                                                break;
                                        }
                                        Close(sockfd);
                                        break;
                                }else if(ret == 0){
                                        Close(sockfd);
                                }else{
                                        printf("get %d bytes of content:%s\n", ret, buf);
                                }
                        }
                }else{

                        printf("something else happened \n");
                }
        }
}

int start_ser(char *ipaddr, char *port)
{
        int sock = Socket(AF_INET, SOCK_STREAM, 0);

        struct sockaddr_in serveraddr;
        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(port));
        inet_pton(AF_INET, ipaddr, &serveraddr.sin_addr);

        Bind(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

        Listen(sock, 128);

        return sock;
}

int main(int argc, char *argv[])
{
        int listenfd = start_ser(argv[1], argv[2]);

        struct epoll_event events[MAX_EVENT_NUM];
        int epollfd = epoll_create(5);
        if(epollfd < 0){
                perr_exit("epoll_create err");
        }
        addfd(epollfd, listenfd, true);
        while(1){
                int ret = epoll_wait(epollfd, events, MAX_EVENT_NUM, -1);
                if(ret < 0){
                        printf("epoll failure\n");
                        break;
                }

                lt(events, ret, epollfd, listenfd);//lt模式
                //et(events, ret, epollfd, listenfd);//et模式
        }
        Close(listenfd);
        return 0;
}
//warp.h檔案是將socket,bind,listen等函式封裝為第一個字母大寫的標頭檔案

4.2 客戶端

#include "wrap.h"                                                            

int main(int argc, char *argv[])
{
        int connfd;
        struct sockaddr_in serveraddr;
        char buf[1024];

        connfd = Socket(AF_INET, SOCK_STREAM, 0);

        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = htons(atoi(argv[2]));
        inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);

        Connect(connfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

        while(fgets(buf, 1024, stdin) != NULL){
                Write(connfd, buf, strlen(buf));
        }

        Close(connfd);
        return 0;
}

4.3 兩種模式結果對比

ET模式

LT模式
當傳送超過緩衝區大小的資料量,LT會多次呼叫epoll_wait函式接受資料,則列印了多次“event level once”,而ET則是迴圈讀取資料知道讀完,列印了一次“event trigger once”。