Linux NIO 系列(04-3) epoll

Linux NIO 系列(04-3) epolllinux

Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)編程

1、why epoll

1.1 select 模型的缺點

  1. 句柄限制:單個進程可以監視的文件描述符的數量存在最大限制,一般是 1024,固然能夠更改數量,但因爲select採用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;(在 linux 內核頭文件中,有這樣的定義: #define __FD_SETSIZE 1024)api

  2. 數據拷貝:內核 / 用戶空間內存拷貝問題,select 須要複製大量的句柄數據結構,產生巨大的開銷;數組

  3. 輪詢機制:select 返回的是含有整個句柄的數組,應用程序須要遍歷整個數組才能發現哪些句柄發生了事件;緩存

select 的觸發方式是水平觸發,應用程序若是沒有完成對一個已經就緒的文件描述符進行 IO 操做,那麼以後每次 select 調用仍是會將這些文件描述符通知進程。服務器

設想一下以下場景:有 100 萬個客戶端同時與一個服務器進程保持着 TCP 鏈接。而每一時刻,一般只有幾百上千個 TCP 鏈接是活躍的(事實上大部分場景都是這種狀況)。如何實現這樣的高併發?網絡

粗略計算一下,一個進程最多有 1024 個文件描述符,那麼咱們須要開 1000 個進程來處理 100 萬個客戶鏈接。若是咱們使用 select 模型,這 1000 個進程裏某一段時間內只有數個客戶鏈接須要數據的接收,那麼咱們就不得不輪詢 1024 個文件描述符以肯定到底是哪一個客戶有數據可讀,想一想若是 1000 個進程都有相似的行爲,那系統資源消耗可有多大啊!數據結構

針對 select 模型的缺點,epoll 模型被提出來了!併發

1.2 epoll 模型優勢

  1. 支持一個進程打開大數目的 socket 描述符
  2. IO 效率不隨 FD 數目增長而線性降低
  3. 使用 mmap 加速內核與用戶空間的消息傳遞
  4. epoll 支持水平觸發和邊沿觸發兩種工做模式

2、epoll API

epoll 在 Linux2.6 內核正式提出,是基於事件驅動的 I/O 方式,相對於 select 來講,epoll 沒有描述符個數限制,使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的 copy 只需一次。

Linux 中提供的 epoll 相關函數以下:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

2.1 epoll_create

函數建立一個 epoll 句柄,參數 size 代表內核要監聽的描述符數量。調用成功時返回一個 epoll 句柄描述符,失敗時返回 -1。

2.2 epoll_ctl

函數註冊要監聽的事件類型。四個參數解釋以下:

  • epfd 表示 epoll 句柄

  • op 表示 fd 操做類型,有以下 3 種

    EPOLL_CTL_ADD   註冊新的fd到epfd中
    EPOLL_CTL_MOD   修改已註冊的fd的監聽事件
    EPOLL_CTL_DEL   從epfd中刪除一個fd
  • fd 是要監聽的描述符

  • event 表示要監聽的事件。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;

2.3 epoll_wait

函數等待事件的就緒,成功時返回就緒的事件數目,調用失敗時返回 -1,等待超時返回 0。

  • epfd 是 epoll 句柄
  • events 表示從內核獲得的就緒事件集合
  • maxevents 告訴內核 events 的大小
  • timeout 表示等待的超時事件

epoll 是 Linux 內核爲處理大批量文件描述符而做了改進的 poll,是 Linux 下多路複用 IO 接口 select/poll 的加強版本,它能顯著提升程序在大量併發鏈接中只有少許活躍的狀況下的系統 CPU 利用率。緣由就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核 IO 事件異步喚醒而加入 Ready 隊列的描述符集合就好了。

3、epoll 工做模式

epoll 除了提供 select/poll 那種 IO 事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存 IO 狀態,減小 epoll_wait/epoll_pwait 的調用,提升應用程序效率。

  • 水平觸發(LT):默認工做模式,即當 epoll_wait 檢測到某描述符事件就緒並通知應用程序時,應用程序能夠不當即處理該事件;下次調用 epoll_wait 時,會再次通知此事件。

    水平觸發同時支持 block 和 non-block socket。在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的 fd 進行 IO 操做。若是你不做任何操做,內核仍是會繼續通知你的,因此,這種模式編程出錯誤可能性要小一點。好比內核通知你其中一個fd能夠讀數據了,你趕忙去讀。你仍是懶懶散散,不去讀這個數據,下一次循環的時候內核發現你還沒讀剛纔的數據,就又通知你趕忙把剛纔的數據讀了。這種機制能夠比較好的保證每一個數據用戶都處理掉了。

  • 邊緣觸發(ET): 當 epoll_wait 檢測到某描述符事件就緒並通知應用程序時,應用程序必須當即處理該事件。若是不處理,下次調用 epoll_wait 時,不會再次通知此事件。(直到你作了某些操做致使該描述符變成未就緒狀態了,也就是說邊緣觸發只在狀態由未就緒變爲就緒時只通知一次)。

    邊緣觸發是高速工做方式,只支持 no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核經過 epoll 告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個文件描述符發送更多的就緒通知,等到下次有新的數據進來的時候纔會再次出發就緒事件。簡而言之,就是內核通知過的事情不會再說第二遍,數據錯過沒讀,你本身負責。這種機制確實速度提升了,可是風險相伴而行。

LT 和 ET 本來應該是用於脈衝信號的,可能用它來解釋更加形象。Level 和 Edge 指的就是觸發點,Level 爲只要處於水平,那麼就一直觸發,而 Edge 則爲上升沿和降低沿的時候觸發。好比:0->1 就是 Edge,1->1 就是 Level。
ET 模式很大程度上減小了 epoll 事件的觸發次數,所以效率比 LT 模式下高。

附1:epoll 網絡編程

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

#define SERVER_PORT 8888
#define OPEN_MAX    3000
#define BACKLOG     10
#define BUF_SIZE    1024

int main() {
    int ret, i;
    int listenfd, connfd, epollfd;
    int nready;
    int recvbytes, sendbytes;

    char* recv_buf;
    struct epoll_event ev;
    struct epoll_event* ep;
    ep = (struct epoll_event*) malloc(sizeof(struct epoll_event) * OPEN_MAX);
    recv_buf = (char*) malloc(sizeof(char) * BUF_SIZE);
    
    struct sockaddr_in seraddr;
    struct sockaddr_in cliaddr;
    int addr_len;

    memset(&seraddr, 0, sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SERVER_PORT);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1) {
        perror("create socket failed.\n");
        return 1;
    }
    ret = bind(listenfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
    if(ret == -1) {
        perror("bind failed.\n");
        return 1;
    }
    ret = listen(listenfd, BACKLOG);
    if(ret == -1) {
        perror("listen failed.\n");
        return 1;
    }

    epollfd = epoll_create(1);
    if(epollfd == -1) {
        perror("epoll_create failed.\n");
        return 1;
    }
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;
    ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
    if(ret == -1) {
        perror("epoll_ctl failed.\n");
        return 1;
    }

    while(1) {
        nready = epoll_wait(epollfd, ep, OPEN_MAX, -1);
        if(nready == -1) {
            perror("epoll_wait failed.\n");
            return 1;
        }
        for(i = 0; i < nready; i++) {
            if(ep[i].data.fd == listenfd) {
                addr_len = sizeof(cliaddr);
                connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &addr_len);
                printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
                if(connfd == -1) {
                    perror("accept failed.\n");
                    return 1;
                }
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev);
                if(ret == -1) {
                    perror("epoll_ctl failed.\n");
                    return 1;
                }
            }
            else {
                recvbytes = recv(ep[i].data.fd, recv_buf, BUF_SIZE, 0);
                if(recvbytes <= 0) {
                    close(ep[i].data.fd);
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, ep[i].data.fd, &ev);
                    continue;
                }
                printf("receive %s\n", recv_buf);
                sendbytes = send(ep[i].data.fd, recv_buf, (size_t)recvbytes, 0);
                if(sendbytes == -1) {
                    perror("send failed.\n");
                }
            }
        } // for each ev
    } // while(1)

    close(epollfd);
    close(listenfd);
    free(ep);
    free(recv_buf);

    return 0;
}

參考:


天天用心記錄一點點。內容也許不重要,但習慣很重要!

相關文章
相關標籤/搜索