歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Linux IO多路復用 epoll 心得

Linux IO多路復用 epoll 心得

日期:2017/3/1 9:05:59   编辑:Linux編程

一、epoll簡介

epoll是Linux內核為處理大批量文件描述符而作了改進的poll, 是Linux下多路復用IO接口select/poll的增強版本, 它能顯著提高程序在大量並發連接中只有少量活躍的情況下的系統CPU利用率。另一點原因就是獲取事件的時候, 它無須遍歷整個被偵聽的描述符集, 只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。

相關文件到Linux公社資源站下載:

------------------------------------------分割線------------------------------------------

免費下載地址在 http://linux.linuxidc.com/

用戶名與密碼都是www.linuxidc.com

具體下載目錄在 /2017年資料/1月/31日/Linux IO多路復用 epoll 心得/

下載方法見 http://www.linuxidc.com/Linux/2013-07/87684.htm

------------------------------------------分割線------------------------------------------

二、epoll的API函數

1. 句柄創建函數

int epoll_create(int size);

創建一個epoll的句柄, size用來告訴內核這個監聽的數目一共有多大。

int epoll_create1(int flag);

這個函數是在linux 2.6.27中加入的, 其實它和epoll_create差不多, 不同的是epoll_create1函數的參數是flag。

當flag是0時, 表示和epoll_create函數完全一樣, 不需要size的提示了。

當flag = EPOLL_CLOEXEC, 創建的epfd會設置FD_CLOEXEC, 它是fd的一個標識說明, 用來設置文件close-on-exec狀態的。

當flag = EPOLL_NONBLOCK, 創建的epfd會設置為非阻塞。

2. 事件操作函數

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

第一個參數epfd, 為epoll_create返回的的epoll文件描述符。

第二個參數op表示操作值。有三個操作類型:

EPOLL_CTL_ADD //注冊目標fd到epfd中, 同時關聯內部event到fd上

EPOLL_CTL_MOD //修改已經注冊到fd的監聽事件

EPOLL_CTL_DEL //從epfd中刪除/移除已注冊的fd, event可以被忽略, 也可以為NULL

第三個參數fd表示需要監聽的fd。

第四個參數event表示需要監聽的事件。

event參數是一個枚舉的集合, 可以用“|”來增加事件類型, 枚舉如下:

// EPOLLIN: 表示關聯的fd可以進行讀操作了。

// EPOLLOUT: 表示關聯的fd可以進行寫操作了。

// EPOLLRDHUP(since Linux 2.6.17): 表示套接字關閉了連接, 或者關閉了正寫一半的連接。

// EPOLLPRI: 表示關聯的fd有緊急優先事件可以進行讀操作了。

// EPOLLERR: 表示關聯的fd發生了錯誤, epoll_wait會一直等待這個事件, 所以一般沒必要設置這個屬性。

// EPOLLHUP: 表示關聯的fd掛起了, epoll_wait會一直等待這個事件, 所以一般沒必要設置這個屬性。

// EPOLLET: 設置關聯的fd為ET的工作方式, epoll的默認工作方式是LT。

// EPOLLONESHOT(since Linux 2.6.2): 設置關聯的fd為one-shot的工作方式。表示只監聽一次事件, 如果要再次監聽, 需要把socket放入到epoll隊列中。

3. 事件等待函數

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

int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);

上面兩個函數的參數含義:

第一個參數:表示epoll_wait等待epfd上的事件。

第二個參數:events指針攜帶有epoll_data_t數據。

第三個參數:maxevents告訴內核events有多大, 該值必須大於0。

第四個參數:timeout表示超時時間(單位: 毫秒), 為0的時候表示馬上返回, 為-1的時候表示一直等下去, 直到有事件返回, 為任意正整數的時候表示等這麼長的時間, 如果一直沒有事件, 則返回。一般情況下, 如果網絡主循環是單獨的線程的話, 可以用-1來等, 這樣可以保證一些效率, 如果是和主邏輯在同一個線程的話, 則可以用0來保證主循環的效率。

epoll_pwait(since linux 2.6.19)允許一個應用程序安全的等待, 直到fd設備准備就緒, 或者捕獲到一個信號量。其中sigmask表示要捕獲的信號量。

函數如果等待成功, 則返回fd的數字; 0表示等待fd超時, 其他錯誤號請查看errno。

4. 句柄關閉函數

int close(int fd);

返回值: 若文件順利關閉則返回0, 發生錯誤時返回-1。

三、epoll的2種觸發模式

1. Level Triggered (LT) 水平觸發

LT是epoll默認的觸發方式, 如下:

socket接收緩沖區不為空, 有數據可讀, 則讀事件一直觸發;

socket發送緩沖區不滿, 可以繼續寫入數據, 則寫事件一直觸發;

LT的處理過程:

accept一個連接, 添加到epoll中監聽EPOLLIN事件;

當EPOLLIN事件到達時, read fd中的數據並處理;

當需要寫入數據時, 先直接把數據write到fd中; 如果數據較大, 無法一次性寫入, 那麼在epoll中監聽EPOLLOUT事件;

當EPOLLOUT事件到達時, 繼續把數據write到fd中; 如果數據寫入完畢, 那麼在epoll中關閉EPOLLOUT事件;

2. Edge Triggered (ET) 邊沿觸發

socket的接收緩沖區狀態變化時觸發讀事件, 即空的接收緩沖區剛接收到數據時觸發讀事件;

socket的發送緩沖區狀態變化時觸發寫事件, 即滿的緩沖區剛空出空間時觸發讀事件;

僅在狀態變化時觸發事件

ET的處理過程:

accept一個連接, 添加到epoll中監聽EPOLLIN|EPOLLOUT事件;

當EPOLLIN事件到達時, read fd中的數據並處理, read需要一直讀, 直到返回EAGAIN為止;

當需要寫出數據時, 把數據write到fd中, 直到數據全部寫完, 或者write返回EAGAIN;

當EPOLLOUT事件到達時, 繼續把數據write到fd中, 直到數據全部寫完, 或者write返回EAGAIN;

ET模式下, 正確的accept要考慮2個問題:

(1) 阻塞模式下, accept存在的問題

考慮這種情況: TCP連接被客戶端夭折, 即在服務器調用accept之前, 客戶端主動發送RST終止連接, 導致剛剛建立的連接從就緒隊列中移出, 如果套接口被設置成阻塞模式, 服務器就會一直阻塞在accept調用上, 直到其他某個客戶建立一個新的連接為止。但是在此期間, 服務器單純地阻塞在accept調用上, 就緒隊列中的其他描述符都得不到處理。

解決辦法是把監聽套接口設置為非阻塞, 當客戶在服務器調用accept之前中止某個連接時, accept調用可以立即返回-1, 這時源自Berkeley的實現會在內核中處理該事件, 並不會將該事件通知給epoll, 而其他實現把errno設置為ECONNABORTED或者EPROTO錯誤,我們應該忽略這兩個錯誤。

(2)ET模式下accept存在的問題

考慮這種情況: 多個連接同時到達, 服務器的TCP就緒隊列瞬間積累多個就緒連接, 由於是邊緣觸發模式, epoll只會通知一次, accept只處理一個連接, 導致TCP就緒隊列中剩下的連接都得不到處理。

解決辦法是用while循環抱住accept調用, 處理完TCP就緒隊列中的所有連接後再退出循環。如何知道是否處理完就緒隊列中的所有連接呢? accept返回-1並且errno設置為EAGAIN就表示所有連接都處理完。

綜合以上兩種情況, 服務器應該使用非阻塞地accept, accept在ET模式下的正確使用方式為:

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {

handle_client(conn_sock);

}

if (conn_sock == -1) {

if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)

perror("accept");

}

3. 總結

從ET的處理過程中可以看到, ET的要求是需要一直讀寫, 直到返回EAGAIN, 否則就會遺漏事件。而LT的處理過程中, 直到返回EAGAIN不是硬性要求, 但通常的處理過程都會讀寫直到返回EAGAIN, 但LT比ET多了一個開關EPOLLOUT事件的步驟。LT的編程與poll/select接近,符合一直以來的習慣,不易出錯。ET的編程可以做到更加簡潔,某些場景下更加高效,但另一方面容易遺漏事件,容易產生bug。

Copyright © Linux教程網 All Rights Reserved