Linux網絡編程 - epoll

下面有一張圖,這張圖來自 The Linux Programming Interface(No Starch Press)。它直觀地爲我們展示了 select、poll、epoll 幾種不同的 I/O 複用技術在面對不同文件描述符大小時的表現差異。

                  

從圖中可以明顯地看到,epoll 的性能是最好的,即使在多達 10000 個文件描述的情況下,其性能的下降和有 10 個文件描述符的情況相比,差別也不是很大。而隨着文件描述符的增大,常規的 select 和 poll 方法性能逐漸變得很差。

epoll 的用法

epoll 可以說是和 poll 非常相似的一種 I/O 多路複用技術,有些朋友將 epoll 歸爲異步 I/O,我覺得這是不正確的。本質上 epoll 還是一種 I/O 多路複用技術, epoll 通過監控註冊的多個描述字,來進行 I/O 事件的分發處理。不同於 poll 的是,epoll 不僅提供了默認的 level-triggered(條件觸發)機制,還提供了性能更爲強勁的 edge-triggered(邊緣觸發)機制。

使用 epoll 進行網絡程序的編寫,需要三個步驟,分別是 epoll_create,epoll_ctl 和 epoll_wait。

epoll_create:

int epoll_create(int size);
int epoll_create1(int flags);
        返回值: 若成功返回一個大於0的值,表示epoll實例;若返回-1表示出錯

epoll_create() 方法創建了一個 epoll 實例,從 Linux 2.6.8 開始,參數 size 被自動忽略,但是該值仍需要一個大於 0 的整數。這個 epoll 實例被用來調用 epoll_ctl 和 epoll_wait,如果這個 epoll 實例不再需要,比如服務器正常關機,需要調用 close() 方法釋放 epoll 實例,這樣系統內核可以回收 epoll 實例所分配使用的內核資源。

參數 size,在一開始的 epoll_create 實現中,是用來告知內核期望監控的文件描述字大小,然後內核使用這部分的信息來初始化內核數據結構。在新的實現中,這個參數不再被需要,因爲內核可以動態分配需要的內核數據結構。我們只需要注意,每次將 size 設置成一個大於 0 的整數就可以了。

epoll_create1() 的用法和 epoll_create() 基本一致,如果 epoll_create1() 的輸入 flags 爲 0,則和 epoll_create() 一樣,內核自動忽略。可以增加如 EPOLL_CLOEXEC 的額外選項,如果你有興趣的話,可以研究一下這個選項有什麼意義。

epoll_ctl:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
        返回值: 若成功返回0;若返回-1表示出錯

在創建完 epoll 實例之後,可以通過調用 epoll_ctl 往這個 epoll 實例增加或刪除監控的事件。

第一個參數 epfd 是剛剛調用 epoll_create 創建的 epoll 實例描述字,可以簡單理解成是 epoll 句柄。

第二個參數表示增加還是刪除一個監控事件,它有三個選項可供選擇:

  • EPOLL_CTL_ADD: 向 epoll 實例註冊文件描述符對應的事件。
  • EPOLL_CTL_DEL:向 epoll 實例刪除文件描述符對應的事件。
  • EPOLL_CTL_MOD: 修改文件描述符對應的事件。

第三個參數是註冊的事件的文件描述符,比如一個監聽套接字。

第四個參數表示的是註冊的事件類型,並且可以在這個結構體裏設置用戶需要的數據,其中最爲常見的是使用聯合結構裏的 fd 字段,表示事件所對應的文件描述符。

typedef union epoll_data {
     void        *ptr;
     int          fd;
     uint32_t     u32;
     uint64_t     u64;
 } epoll_data_t;

 struct epoll_event {
     uint32_t     events;      /* Epoll events */
     epoll_data_t data;        /* User data variable */
 };

我們在前面介紹 poll 的時候已經接觸過基於 mask 的事件類型了,這裏 epoll 仍舊使用了同樣的機制,我們重點看一下這幾種事件類型:

  • EPOLLIN:表示對應的文件描述字可以讀;
  • EPOLLOUT:表示對應的文件描述字可以寫;
  • EPOLLRDHUP:表示套接字的一端已經關閉,或者半關閉;
  • EPOLLHUP:表示對應的文件描述字被掛起;
  • EPOLLET:設置爲 edge-triggered,默認爲 level-triggered。

epoll_wait:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  返回值: 成功返回的是一個大於0的數,表示事件的個數;返回0表示的是超時時間到;若出錯返回-1.

epoll_wait() 函數類似之前的 poll 和 select 函數,調用者進程被掛起,在等待內核 I/O 事件的分發。

第一個參數是 epoll 實例描述字,也就是 epoll 句柄。

第二個參數返回給用戶空間需要處理的 I/O 事件,這是一個數組,數組的大小由 epoll_wait 的返回值決定,這個數組的每個元素都是一個需要待處理的 I/O 事件,其中 events 表示具體的事件類型,事件類型取值和 epoll_ctl 可設置的值一樣,這個 epoll_event 結構體裏的 data 值就是在 epoll_ctl 那裏設置的 data,也就是用戶空間和內核空間調用時需要的數據。

第三個參數是一個大於 0 的整數,表示 epoll_wait 可以返回的最大事件值。

epoll 例子

#define MAXEVENTS 128

char rot13_char(char c) {
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

int main(int argc, char **argv) {
    int listen_fd, socket_fd;
    int n, i;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;

    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(listen_fd, F_SETFL, O_NONBLOCK);
 
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);
 
    int on = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    int rt1 = bind(listen_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        printf("bind failed \n");
        exit(0);
    }
 
    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        printf("listen failed \n");
        exit(0);
    }
    signal(SIGPIPE, SIG_IGN);

    efd = epoll_create1(0);
    if (efd == -1) {
        printf("epoll create failed\n");
        return 0;
    }
    event.data.fd = listen_fd;
    event.events = EPOLLIN | EPOLLET;
    /*調用 epoll_ctl 將監聽套接字對應的 I/O 事件進行了註冊,這樣在有新的連接建立之後,就可以感知到。注意這裏使用的是 edge-triggered(邊緣觸發)*/
    if (epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1) {
        printf("epoll_ctl add listen fd failed\n");
    }

    /* Buffer where events are returned */
    events = calloc(MAXEVENTS, sizeof(event));

    /*主循環調用 epoll_wait 函數分發 I/O 事件,當 epoll_wait 成功返回時,通過遍歷返回的 event 數組,就直接可以知道發生的 I/O 事件*/
    while (1) {
        n = epoll_wait(efd, events, MAXEVENTS, -1);
        printf("epoll_wait wakeup\n");
        for (i = 0; i < n; i++) {
            if ((events[i].events & EPOLLERR) ||
                (events[i].events & EPOLLHUP) ||
                (!(events[i].events & EPOLLIN))) {
                fprintf(stderr, "epoll error\n");
                close(events[i].data.fd);
                continue;
            } else if (listen_fd == events[i].data.fd) {
                struct sockaddr_storage ss;
                socklen_t slen = sizeof(ss);
                int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);
                if (fd < 0) {
                    printf("accept failed\n");
                } else {
                    fcntl(fd, F_SETFL, O_NONBLOCK);
                    event.data.fd = fd;
                    event.events = EPOLLIN | EPOLLET; //edge-triggered
                    /*調用 epoll_ctl 把已連接套接字對應的可讀事件註冊到 epoll 實例中*/
                    if (epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1) {
                        printf("epoll_ctl add connection fd failed\n");
                    }
                }
                continue;
            } else {//處理了已連接套接字上的可讀事件,讀取字節流,編碼後再回應給客戶端
                socket_fd = events[i].data.fd;
                printf("get event on socket fd == %d \n", socket_fd);
                while (1) {
                    char buf[512];
                    if ((n = read(socket_fd, buf, sizeof(buf))) < 0) {
                        if (errno != EAGAIN) {
                            printf("read error\n");
                            close(socket_fd);
                        }
                        break;
                    } else if (n == 0) {
                        close(socket_fd);
                        break;
                    } else {
                        for (i = 0; i < n; ++i) {
                            buf[i] = rot13_char(buf[i]);
                        }
                        if (write(socket_fd, buf, n) < 0) {
                            printf("write error\n");
                        }
                    }
                }
            }
        }
    }

    free(events);
    close(listen_fd);
}

edge-triggered VS level-triggered

兩者的區別,條件觸發的意思是隻要滿足事件的條件,比如有數據需要讀,就一直不斷地把這個事件傳遞給用戶;而邊緣觸發的意思是隻有第一次滿足條件的時候才觸發,之後就不會再傳遞同樣的事件了。一般我們認爲,邊緣觸發的效率比條件觸發的效率要高,這一點也是 epoll 的殺手鐗之一。

 

總之, Linux 中 epoll 的出現,爲高性能網絡編程補齊了最後一塊拼圖。epoll 通過改進的接口設計,避免了用戶態 - 內核態頻繁的數據拷貝,大大提高了系統性能。在使用 epoll 的時候,我們一定要理解條件觸發和邊緣觸發兩種模式。條件觸發的意思是隻要滿足事件的條件,比如有數據需要讀,就一直不斷地把這個事件傳遞給用戶;而邊緣觸發的意思是隻有第一次滿足條件的時候才觸發,之後就不會再傳遞同樣的事件了。

 

溫故而知新 !

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