一、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。