linux IO複用之epoll總結

前言

《UNIX網絡編程》裏並無提到epoll,不知道爲啥,如下的內容是根據linux manual總結的。java

API介紹

epoll是在linux上提供的實現IO複用的機制。epoll與poll相似,能夠同時監聽多個描述符;epoll新增了邊緣觸發和水平觸發的概念,並且在處理大量描述符時更有優點。linux

epoll API中核心概念就是epoll實例,它是一個內核內的數據結構,從用戶角度來看它能夠簡單的看作包含了兩個list:編程

  • interest list(或者叫epoll set):用戶註冊的感興趣的描述符集合
  • ready list:就緒的描述符集合,當有IO就緒時內核會自動將就緒的描述符加到ready list中

epoll API包含三個系統調用:網絡

epoll_create數據結構

int epoll_create(int size);
int epoll_create1(int flags);
複製代碼

epoll_create建立一個epoll實例,函數會返回一個指向epoll實例的描述符,在使用完畢後應該調用close關閉epoll實例。size參數相似map的capacity,標識epoll實例維護的描述符的數量。併發

epoll_create1epoll_create相類似,但參數變成了flags,size則被忽略。這裏的flags有一個可選項:EPOLL_CLOEXECEPOLL_CLOEXEC表示在建立的描述符上設置FD_CLOEXEC標誌。socket

epoll_ctl函數

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

/* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */
#define EPOLL_CTL_ADD 1 /* Add a file decriptor to the interface. */
#define EPOLL_CTL_DEL 2 /* Remove a file decriptor from the interface. */
#define EPOLL_CTL_MOD 3 /* Change file decriptor epoll_event structure. */

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

struct epoll_event {
    uint32_t events;   /* Epoll events */
    epoll_data_t data; /* User data variable */
}
複製代碼

epoll_ctl將描述符和感興趣的事件註冊到epoll實例,這個函數至關於把描述符添加到epoll實例的interest list中。函數操做成功時返回0,不然返回-1並設置errno。oop

epoll_wait性能

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
複製代碼

epoll_wait會阻塞等待IO事件,能夠理解爲從ready list裏獲取描述符。函數返回就緒描述的個數,並會將就緒的描述符存儲到events參數中,經過timeout能夠設置以毫秒爲單位的超時時間,-1表示永不超時。

邊緣觸發和水平觸發

關於邊緣觸發和水平觸發的介紹有不少,這裏就翻譯一下man手冊裏的內容好了。

epoll提供兩種觸發機制:edge-triggered (ET) 和 level-triggered (LT),它們的區別能夠經過如下的例子來講明:

  1. 假設咱們已有一個描述符rfd,咱們將從它讀取一個pipe輸出,咱們將其註冊到epoll實例中,感興趣的事件爲可讀
  2. pipe的寫入端寫入了2KB的數據到pipe
  3. 進程調用了epoll_wait,這時rfd會被放到ready list中而後成功返回
  4. pipe的讀取端從pipe讀取了1KB的數據
  5. 進程又一次調用epoll_wait

若是rfd在註冊到epoll實例時使用了EPOLLET選項,那麼上述第5步調用epoll_wait可能會發生阻塞,儘管這時讀取緩衝區裏仍有可讀取的數據;而同時pipe的另外一端可能在等待着相應的響應,因而陷入了無盡的互相等待。而出現這種現象的緣由在於ET僅在描述符發生變化時纔會返回事件。在上面的例子當中,第2步會產生一個事件,而第3步會消費這個事件。由於第4步沒有讀取完全部的數據,因此第5步可能會陷入無限期的阻塞。

而linux manual建議的邊緣觸發的使用方式以下:

  1. 配合非阻塞描述符使用
  2. 直到每次read或者write返回EAGAIN時才繼續等待下一次事件

與邊緣觸發不一樣,當使用水平觸發選項時,epoll就至關於poll的升級版, 能夠簡單地替換poll。

總的來講,ET和LT的區別在於觸發事件的條件不一樣,LT比較符合編程思惟(有知足條件的就觸發),ET觸發的條件更苛刻一些(僅在發生變化時才觸發),對使用者的要求也更高,理論效率更高。值得一提的是java nio的selector會根據操做系統不一樣採用不一樣的實現,在linux 2.6及之後的版本中使用的就是epoll,採用的是水平觸發;而netty中提供的額外的EpollEventLoop則採用了邊緣觸發。

在監聽描述符事件時,同一個描述符上可能會連續發生多個事件,這是用戶能夠選擇設置EPOLLONESHOT選項來通知epoll禁用後續的事件。若是設置了EPOLLONESHOT選項,在事件處理完畢後用戶須要從新註冊事件。這個選項在併發環境更加有用。

當多個進程或者線程同時監聽一個epoll實例上的一個描述符時,使用EPOLLET選項能夠保證每次事件只會通知一個進程或者線程,避免相似「驚羣」的問題。

epoll監聽的限制

/proc/sys/fs/epoll/max_user_watches 中的配置限制了同一個用戶在全部epoll實例中能監聽的描述符的總數。

使用邊緣觸發的例子

由於水平觸發和poll的使用方式區別不大,這裏僅展現邊緣觸發的示例:

#define MAX_EVENTS 10
    struct epoll_event ev, events[MAX_EVENTS];
    int listen_sock, conn_sock, nfds, epollfd;

    /* 此處省略調用listen_sock調用socket、bind和listen的過程 */

    //建立epoll實例,程序最後應該調用close關閉epollfd
    epollfd = epoll_create1(0);
    if (epollfd == -1)
    {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    ev.events = EPOLLIN; //感興趣的事件爲讀事件
    ev.data.fd = listen_sock; //註冊fd爲監聽套接字
    
    //註冊event
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1)
    {
        perror("epoll_ctl: listen_sock");
        exit(EXIT_FAILURE);
    }

    for (;;)
    {
        //等待描述符就緒,參數-1表示不超時
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1)
        {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }

        for (n = 0; n < nfds; ++n)
        {
            if (events[n].data.fd == listen_sock)
            {
                //監聽套接字就緒,調用accept創建鏈接
                conn_sock = accept(listen_sock,
                                  (struct sockaddr *)&addr, &addrlen);
                if (conn_sock == -1)
                {
                    perror("accept");
                    exit(EXIT_FAILURE);
                }
                //設置新鏈接爲非阻塞模式(ET下必須設置非阻塞)
                setnonblocking(conn_sock);
                //感興趣的事件爲讀事件,同時設置爲邊緣觸發
                ev.events = EPOLLIN | EPOLLET;
                //註冊fd爲新創建的鏈接描述符
                ev.data.fd = conn_sock;
                //註冊event
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                              &ev) == -1)
                {
                    perror("epoll_ctl: conn_sock");
                    exit(EXIT_FAILURE);
                }
            } else {//新創建的鏈接就緒
                //do_use_fd應該對fd進行read或者write直到EAGAIN,而後記錄當前的read或write進度,等到下次就緒後再繼續
                do_use_fd(events[n].data.fd);
            }
        }
    }
複製代碼

在邊緣觸發模式下,若是但願在事件到來時不馬上進行操做,而是等其餘條件就緒後再進行read或write,這時能夠同時註冊EPOLLIN|EPOLLOUT事件以提升性能,而不是反覆調用epoll_ctl經過EPOLL_CTL_MODEPOLLIN和EPOLLOUT之間來回切換,若是在水平模式下就不能這樣作了,由於感興趣的事件一旦就緒的事件就會持續發生,帶來沒必要要的消耗。

爲何epoll比poll更快

epoll的介紹裏提到epoll比poll更快,根據網上的其餘博客總結了如下幾點緣由:

  1. 等待描述符就緒時,不須要每次都將描述符集合傳遞到內核,而是將描述符註冊到epoll實例,由epoll實例內部維護所有的描述符集合
  2. epoll實例內部使用了紅黑樹和內核cache區維護描述符集合,提升了描述符集合註冊和刪除操做的效率
  3. epoll內部經過回調機制維護ready list。當有描述符就緒時就將其放到ready list中,調用epoll_wait時只須要判斷ready list是否爲空便可,若是不爲空則將ready list複製到用戶空間並清空ready list;不然陷入睡眠
  4. 有描述符就緒時不須要從新遍歷全部描述符,epoll會直接返回就緒的描述符集合

這裏順便提一下LT的實現,epoll_wait在返回就緒描述符前會檢查描述符的觸發類型,若是是水平觸發而且描述符上有未處理的數據,則會將其加入剛纔清空的ready list,這樣下次調用epoll_wait時ready list仍會有該描述符。這也是LT和ET的表現的差異的實際緣由。

相關文章
相關標籤/搜索