本章所有示例代碼>>gtihub
17.1 epoll理解及應用
1. 基於select的I/O複用技術速度慢的原因
兩點不合理:
- 調用select函數後常見的針對所有文件描述符的循環語句;
- 每次調用select函數時都需要向該函數傳遞監視對象信息;
“每次調用select函數時向操作系統傳遞監視對象信息。”
應用程序向操作系統傳遞數據將對程序造成很大負擔,而且無法通過優化代碼解決,因此將成爲性能上的致命弱點。(有些函數不需要操作系統的幫助就能完成功能,而有些則必須藉助於操作系統)
select函數與文件描述符有關,更準確地說,是監視套接字變化的函數。而套接字是由操作系統管理的,所以select函數絕對需要藉助於操作系統才能完成功能。
“僅向操作系統傳遞1次監視對象,監視範圍或內容發生變化時只通知發生變化的事項。”(前提是操作系統支持這種處理方式,Linux的支持方式是epoll,Windows的支持方式是IOCP)
2. select也有優點
大多數操作系統都支持select函數,只要滿足如下兩個條件,可使用select函數:
- 服務器接入者少;
- 程序應具有兼容性;
3. 實現epoll時必要的函數和結構體
epoll函數優點:
- 無需編寫以監視狀態變化爲目的的針對所有文件描述符的循環語句;
- 調用對應於select函數的epoll_wait函數時無需每次傳遞監視對象信息;
epoll服務器端實現中需要的3個函數:
- epoll_create: 創建保存epoll文件描述符的空間;
- epoll_ctl: 向空間註冊並註銷文件描述符;
- epoll_wait: 與select函數類似,等待文件描述符發生變化;
select方式中爲了保存監視對象的文件描述符,直接聲明瞭fd_set變量。但epoll方式下由操作系統負責保存監視對象文件描述符,因此需要向操作系統請求創建保存文件描述符的空間,此時使用的函數就是epoll_create。
select方式中,爲了添加和刪除監視對象文件描述符,需要FD_SET、FD_CLR函數。但在epoll方式中,通過epoll_ctl函數請求操作系統完成。
select方式下調用select函數等待文件描述符的變化,而epoll中調用epoll_wait函數。
select方式中通過fd_set變量查看監視對象的狀態變化(事件發生與否),而epoll方式中通過結構體epoll_event將發生變化的(發生事件的)文件描述符單獨集中到一起。
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
};
typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
聲明足夠大的epoll_event結構體數組後,傳遞給epoll_wait函數時,發生變化的文件描述符信息將被填入該數組。
4. epoll_create
#include<sys/epoll.h>
int epoll_create(int size); //成功時返回epoll文件描述符,失敗-1
-size: epoll實例的大小,僅供操作系統參考,Linux2.6.8內核版本後將完全忽略該參數;
epoll_create函數創建的資源與套接字相同,也由操作系統管理。因此,該函數和創建套接字的情況相同,也會返回文件描述符,主要用於區分epoll例程,終止時也需調用close函數。
5. epoll_ctl
生成epoll例程後,應在其內部註冊監視對象文件描述符,此時使用epoll_ctl函數。
#include<sys/epoll.h>
int epoll_ctl(intepfd, int op, int fd, struct epoll_event *event);
//成功時返回0,失敗時返回-1
-epfd: 用於註冊監視對象的epoll例程;
-op: 用於指定監視對象的添加、刪除和更改等操作;
-fd: 需要註冊的監視對象文件描述符;
-event: 監視對象的事件類
調用:
epoll_ctl(A, EPOLL_CTL_ADD, B, C);
“epoll例程A中註冊文件描述符B,主要目的是監視參數C中的事件。”
epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);
“從epoll例程A中刪除文件描述符B。”
- EPOLL_CTL_ADD : 將文件描述符註冊到epoll例程;
- EPOLL_CTL_DEL: 從epoll例程中刪除文件描述符;
- EPOLL_CTL_MOD: 更改註冊的文件描述符的關注事件發生情況;
epoll_event結構體用於保存發生事件的文件描述符集合。但也可以在epoll例程中註冊文件描述符時,用於註冊關注的事件。
struct epoll_event event;
……
event.events = EPOLLIN; // 發生需要讀取數據的情況(事件)
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
- EPOLLIN: 需要讀取數據的情況;
- EPOLLOUT: 輸出緩衝爲空,可以立即發送數據的情況;
- EPOLLPRI: 收到OOB數據的情況;
- EPOLLRDHUP: 斷開連接或半關閉的情況,這在邊緣觸發方式下非常有用;
- EPOLLERR: 發生錯誤的情況;
- EPOLLLET: 以邊緣觸發的方式得到事件通知;
- EPOLLONESHOT: 發生一次事件後,相應文件描述符不再收到事件通知;
可以通過位或運算同時傳遞多個上述參數。
6. epoll_wait
#include <sys/epoll.h>
int epoll_wait(intepfd, struct epoll_event *events, int maxevents, int timeout); //成功時返回發生事件的文件描述符數,失敗時返回-1
-epfd: 表示事件發生監視範圍的epoll例程的文件描述符;
-events: 保存發生事件的文件描述符集合的結構體地址值;
-maxevents: 第二個參數中可以保存的最大事件數;
-timeout: 以1/1000秒爲單位的等待時間,傳遞-1時,一直等待直到發生事件
int event_cnt;
struct epoll_event*ep_event;
……
ep_events =malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
……
event_cnt =epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
……
調用函數後,返回發生事件的文件描述符數,同時在第二個參數指向的緩衝中保存發生事件的文件描述符集合。
17.2 條件觸發(Level Trigger)和邊緣觸發(EdgeTrigger)
1. 條件觸發和邊緣觸發的區別在於發生事件的時間點
如,
服務器端輸入緩衝收到50字節的數據時,服務器端操作系統將通知該事件(註冊到發生變化的文件描述符)。服務器端讀取20字節後還剩30字節的情況下,仍然會註冊事件。
邊緣觸發中輸入緩衝收到數據時僅註冊1次該事件。即使輸入緩衝中還留有數據,也不會再進行註冊。
2. 掌握條件觸發的事件特性
epoll默認以條件觸發方式工作,因此可以通過22-EchoEPLTServer示例驗證
3. 邊緣觸發的服務器端實現中必知的兩點
- 通過errno變量驗證錯誤原因;
- 爲了完成非阻塞(Non-blocking)I/O,更改套接字特性;
爲了在發生錯誤時提供額外的信息,Linux提供瞭如下全局變量:
#include <errno.h>
int errno;
“read函數發現輸出緩衝中沒有數據可讀時返回-1,同時在error中保存EAGAIN常量。”
將套接字改爲非阻塞方式的方法:
Linux提供更改或讀取文件屬性的如下方法。
#include <fcntl.h>
int fcntl(intfiledes, int cmd, ……); // 成功返回cmd參數相關值,失敗時返回-1
-filedes: 屬性/更改目標的文件描述符;
-cmd: 表示函數調用的目的;
fcntl具有可變參數的形式。如果向第二個參數傳遞F_GETFL,可以獲得第一個參數所指的文件描述符屬性;如果向第二個參數傳遞F_SETFL,可以更改文件描述符屬性。
若希望將文件(套接字)改爲非阻塞模式,需要如下2條語句:
int flag = fcntl(fd,F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
通過第一條語句獲取之前設置的屬性信息,通過第二條語句在此屬性基礎上添加非阻塞O_NONBLOCK標誌。
4. 實現邊緣觸發的回聲服務器端
邊緣觸發方式下,以非阻塞方式工作的read& write函數有可能引起服務器端的長時間停頓。因此,邊緣觸發方式中一定要採用非阻塞read& write函數。
5. 條件觸發和邊緣觸發孰優孰劣
邊緣觸發可以做到如下這點:
“可以分離接收數據和處理數據的時間點。”
條件觸發在輸入緩衝收到數據的情況下,如果不讀取(延遲處理),則每次調用epoll_wait函數時都會產生相應事件,而且事件數也會累加,服務器端不能承受。
從實現模型的角度看,邊緣觸發更有可能帶來高性能。