1、epoll 系列函數簡介html
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
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);
linux
* epoll_create(2) creates an epoll instance and returns a file descriptor referring to that instance. (The more recent
epoll_create1(2) extends the functionality of epoll_create(2).)
* Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently
registered on an epoll instance is sometimes called an epoll set.
* epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.
ios
一、epoll_create1 產生一個epoll 實例,返回的是實例的句柄。flag 能夠設置爲0 或者EPOLL_CLOEXEC,爲0時函數表現與epoll_create一致,EPOLL_CLOEXEC標誌與open 時的O_CLOEXEC 標誌相似,即進程被替換時會關閉打開的文件描述符。c++
二、epoll_ctl :程序員
(1)epfd:epoll 實例句柄;編程
(2)op:對文件描述符fd 的操做,主要有EPOLL_CTL_ADD、 EPOLL_CTL_DEL等;ubuntu
(3)fd:須要操做的目標文件描述符;數組
(4)event:結構體指針安全
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
服務器
events 參數主要有EPOLLIN、EPOLLOUT、EPOLLET、EPOLLLT等;通常data 共同體咱們設置其成員fd便可,也就是epoll_ctl 函數的第三個參數。
三、epoll_wait:
(1)epfd:epoll 實例句柄;
(2)events:結構體指針
(3)maxevents:事件的最大個數
(4)timeout:超時時間,設爲-1表示永不超時
下面咱們使用c++ 來實現一個服務器端程序:
C++ Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
|
#include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/epoll.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <vector> #include <algorithm> #include "read_write.h" #include "sysutil.h" typedef std::vector<struct epoll_event> EventList; /* 相比於select與poll,epoll最大的好處是不會隨着關心的fd數目的增多而下降效率 */ int main(void) { int count = 0; int listenfd; if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) ERR_EXIT("setsockopt"); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind"); if (listen(listenfd, SOMAXCONN) < 0) ERR_EXIT("listen"); std::vector<int> clients; int epollfd; epollfd = epoll_create1(EPOLL_CLOEXEC); //epoll實例句柄 struct epoll_event event; event.data.fd = listenfd; event.events = EPOLLIN | EPOLLET; //邊沿觸發 epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event); EventList events(16); struct sockaddr_in peeraddr; socklen_t peerlen; int conn; int i; int nready; while (1) { nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1); if (nready == -1) { if (errno == EINTR) continue; ERR_EXIT("epoll_wait"); } if (nready == 0) continue; if ((size_t)nready == events.size()) events.resize(events.size() * 2); for (i = 0; i < nready; i++) { if (events[i].data.fd == listenfd) { peerlen = sizeof(peeraddr); conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen); if (conn == -1) ERR_EXIT("accept"); printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); printf("count = %d\n", ++count); clients.push_back(conn); activate_nonblock(conn); event.data.fd = conn; event.events = EPOLLIN | EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, conn, &event); } else if (events[i].events & EPOLLIN) { conn = events[i].data.fd; if (conn < 0) continue; char recvbuf[1024] = {0}; int ret = readline(conn, recvbuf, 1024); if (ret == -1) ERR_EXIT("readline"); if (ret == 0) { printf("client close\n"); close(conn); event = events[i]; epoll_ctl(epollfd, EPOLL_CTL_DEL, conn, &event); clients.erase(std::remove(clients.begin(), clients.end(), conn), clients.end()); } fputs(recvbuf, stdout); writen(conn, recvbuf, strlen(recvbuf)); } } } return 0; }
|
在程序的最開始定義一個新類型EventList,內部裝着struct epoll_event 結構體的容器。
接下面的socket,bind,listen 都跟之前說的同樣,不述。接着使用epoll_create1 建立一個epoll 實例,再來看下面四行代碼:
struct epoll_event event;
event.data.fd = listenfd;
event.events = EPOLLIN | EPOLLET; //邊沿觸發
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);
根據前面的函數分析,這四句意思就是將監聽套接字listenfd 加入關心的套接字序列。
在epoll_wait 函數中的第二個參數,其實events.begin() 是個迭代器,但其具體實現也是struct epoll_event* 類型,雖然 &*events.begin() 獲得的也是struct epoll_event* ,但不能直接使用events.begin() 作參數,由於類型不匹配,編譯會出錯。
EventList events(16); 即初始化容器的大小爲16,當返回的事件個數nready 已經等於16時,須要增大容器的大小,使用events.resize 函數便可,容器能夠動態增大,這也是咱們使用c++實現的其中一個緣由。
當監聽套接字有可讀事件,accept 返回的conn也須要使用epoll_ctl 函數將其加入關心的套接字隊列。
還須要調用 activate_nonblock(conn); 將conn 設置爲非阻塞,man 7 epoll 裏有這樣一句話:
An application that employs the EPOLLET flag should use nonblocking file descriptors to avoid having a blocking read or
write starve a task that is handling multiple file descriptors.
當下次循環回來某個已鏈接套接字有可讀事件,則讀取數據,若read 返回0表示對方關閉,須要使用epoll_ctl 函數將conn 從隊列中清除,咱們使用 std::vector<int> clients; 來保存每次accept 返回的conn,因此如今也須要將其擦除掉,調用clients.erase() 函數。
咱們可使用前面寫的conntest 客戶端程序測試一下,先運行服務器程序,再運行客戶端,輸出以下:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_epoll
................................
count = 1015
ip=127.0.0.1 port=60492
count = 1016
ip=127.0.0.1 port=60493
count = 1017
ip=127.0.0.1 port=60494
count = 1018
ip=127.0.0.1 port=60495
count = 1019
accept: Too many open files
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./conntest
.........................................................
count = 1015
ip=127.0.0.1 port=60492
count = 1016
ip=127.0.0.1 port=60493
count = 1017
ip=127.0.0.1 port=60494
count = 1018
ip=127.0.0.1 port=60495
count = 1019
connect: Connection reset by peer
爲何服務器端的count 只有1019呢,由於除去012,一個監聽套接字還有一個epoll 實例句柄,因此1024 - 5 = 1019。
爲何客戶端的錯誤提示跟這裏的不同呢?這正說明epoll 處理效率比poll和select 都高,由於處理得快,來一個鏈接就accept一個,當服務器端accept 完第1019個鏈接,再次accept 時會由於文件描述符總數超出限制,打印錯誤提示,而此時客戶端雖然已經建立了第1020個sock,但在connect 過程當中發現對等方已經退出了,故打印錯誤提示,鏈接被對等方重置。若是服務器端處理得慢的話,那麼客戶端會connect 成功1021個鏈接,而後在建立第1022個sock 的時候出錯,打印錯誤提示:socket: Too many open files,固然由於文件描述符的限制,服務器端也只能從已完成鏈接隊列中accept 成功1019個鏈接。
2、epoll與select、poll區別
一、相比於select與poll,epoll最大的好處在於它不會隨着監聽fd數目的增加而下降效率。內核中的select與poll的實現是採用輪詢來處理的,輪詢的fd數目越多,天然耗時越多。
二、epoll的實現是基於回調的,若是fd有指望的事件發生就經過回調函數將其加入epoll就緒隊列中,也就是說它只關心「活躍」的fd,與fd數目無關。
三、epoll不只會告訴應用程序有I/0 事件到來,還會告訴應用程序相關的信息,這些信息是應用程序填充的,所以根據這些信息應用程序就能直接定位到事件,而沒必要遍歷整個fd集合。
四、當已鏈接的套接字數量不太大,而且這些套接字都很是活躍,那麼對於epoll 來講一直在調用callback 函數(epoll 內部的實現更復雜,更復雜的代碼邏輯),可能性能沒有poll 和 select 好,由於一次性遍歷對活躍的文件描述符處理,在鏈接數量不大的狀況下,性能更好,但在處理大量鏈接的狀況時,epoll 明顯佔優。
3、epoll 的EPOLLLT (電平觸發,默認)和 EPOLLET(邊沿觸發)模式的區別
一、EPOLLLT:徹底靠kernel epoll驅動,應用程序只須要處理從epoll_wait返回的fds,這些fds咱們認爲它們處於就緒狀態。此時epoll能夠認爲是更快速的poll。
二、EPOLLET:此模式下,系統僅僅通知應用程序哪些fds變成了就緒狀態,一旦fd變成就緒狀態,epoll將再也不關注這個fd的任何狀態信息,(從epoll隊列移除)直到應用程序經過讀寫操做(非阻塞)觸發EAGAIN狀態,epoll認爲這個fd又變爲空閒狀態,那麼epoll又從新關注這個fd的狀態變化(從新加入epoll隊列)。隨着epoll_wait的返回,隊列中的fds是在減小的,因此在大併發的系統中,EPOLLET更有優點,可是對程序員的要求也更高,由於有可能會出現數據讀取不完整的問題,舉例以下:
假設如今對方發送了2k的數據,而咱們先讀取了1k,而後這時調用了epoll_wait,若是是邊沿觸發,那麼這個fd變成就緒狀態就會從epoll 隊列移除,極可能epoll_wait 會一直阻塞,忽略還沒有讀取的1k數據,與此同時對方還在等待着咱們發送一個回覆ack,表示已經接收到數據;若是是電平觸發,那麼epoll_wait 還會檢測到可讀事件而返回,咱們能夠繼續讀取剩下的1k 數據。
注:上述使用 epoll ET 的例子只是個示例,更規範的用法能夠參考這裏。
參考:
《Linux C 編程一站式學習》
《TCP/IP詳解 卷一》
《UNP》
select, poll和epoll的區別
隨着2.6內核對epoll的徹底支持,網絡上不少的文章和示例代碼都提供了這樣一個信息:使用epoll代替傳統的poll能給網絡服務應用帶來性能上的提高。但大多文章裏關於性能提高的緣由解釋的較少,這裏我將試分析一下內核(2.6.21.1)代碼中poll與epoll的工做原理,而後再經過一些測試數據來對比具體效果。
POLL:
先說poll,poll或select爲大部分Unix/Linux程序員所熟悉,這倆個東西原理相似,性能上也不存在明顯差別,但select對所監控的文件描述符數量有限制,因此這裏選用poll作說明。
poll是一個系統調用,其內核入口函數爲sys_poll,sys_poll幾乎不作任何處理直接調用do_sys_poll,do_sys_poll的執行過程能夠分爲三個部分:
1,將用戶傳入的pollfd數組拷貝到內核空間,由於拷貝操做和數組長度相關,時間上這是一個O(n)操做,這一步的代碼在do_sys_poll中包括從函數開始到調用do_poll前的部分。
2,查詢每一個文件描述符對應設備的狀態,若是該設備還沒有就緒,則在該設備的等待隊列中加入一項並繼續查詢下一設備的狀態。查詢完全部設備後若是沒有一個設備就緒,這時則須要掛起當前進程等待,直到設備就緒或者超時,掛起操做是經過調用schedule_timeout執行的。設備就緒後進程被通知繼續運行,這時再次遍歷全部設備,以查找就緒設備。這一步由於兩次遍歷全部設備,時間複雜度也是O(n),這裏面不包括等待時間。相關代碼在do_poll函數中。
3,將得到的數據傳送到用戶空間並執行釋放內存和剝離等待隊列等善後工做,向用戶空間拷貝數據與剝離等待隊列等操做的的時間複雜度一樣是O(n),具體代碼包括do_sys_poll函數中調用do_poll後到結束的部分。
EPOLL:
接下來分析epoll,與poll/select不一樣,epoll再也不是一個單獨的系統調用,而是由epoll_create/epoll_ctl/epoll_wait三個系統調用組成,後面將會看到這樣作的好處。
先來看sys_epoll_create(epoll_create對應的內核函數),這個函數主要是作一些準備工做,好比建立數據結構,初始化數據並最終返回一個文件描述符(表示新建立的虛擬epoll文件),這個操做能夠認爲是一個固定時間的操做。
epoll是作爲一個虛擬文件系統來實現的,這樣作至少有如下兩個好處:
1,能夠在內核裏維護一些信息,這些信息在屢次epoll_wait間是保持的,好比全部受監控的文件描述符。
2, epoll自己也能夠被poll/epoll;
具體epoll的虛擬文件系統的實現和性能分析無關,再也不贅述。
在sys_epoll_create中還能看到一個細節,就是epoll_create的參數size在現階段是沒有意義的,只要大於零就行。
接着是sys_epoll_ctl(epoll_ctl對應的內核函數),須要明確的是每次調用sys_epoll_ctl只處理一個文件描述符,這裏主要描述當op爲EPOLL_CTL_ADD時的執行過程,sys_epoll_ctl作一些安全性檢查後進入ep_insert,ep_insert裏將 ep_poll_callback作爲回掉函數加入設備的等待隊列(假定這時設備還沒有就緒),因爲每次poll_ctl只操做一個文件描述符,所以也能夠認爲這是一個O(1)操做
ep_poll_callback函數很關鍵,它在所等待的設備就緒後被系統回掉,執行兩個操做:
1,將就緒設備加入就緒隊列,這一步避免了像poll那樣在設備就緒後再次輪詢全部設備找就緒者,下降了時間複雜度,由O(n)到O(1);
2,喚醒虛擬的epoll文件;
最後是sys_epoll_wait,這裏實際執行操做的是ep_poll函數。該函數等待將進程自身插入虛擬epoll文件的等待隊列,直到被喚醒(見上面ep_poll_callback函數描述),最後執行ep_events_transfer將結果拷貝到用戶空間。因爲只拷貝就緒設備信息,因此這裏的拷貝是一個O(1)操做。
還有一個讓人關心的問題就是epoll對EPOLLET的處理,即邊沿觸發的處理,粗略看代碼就是把一部分水平觸發模式下內核作的工做交給用戶來處理,直覺上不會對性能有太大影響,感興趣的朋友歡迎討論。
POLL/EPOLL對比:
表面上poll的過程能夠看做是由一次epoll_create/若干次epoll_ctl/一次epoll_wait/一次close等系統調用構成,實際上epoll將poll分紅若干部分實現的緣由正是由於服務器軟件中使用poll的特色(好比Web服務器):
1,須要同時poll大量文件描述符;
2,每次poll完成後就緒的文件描述符只佔全部被poll的描述符的不多一部分。
3,先後屢次poll調用對文件描述符數組(ufds)的修改只是很小;
傳統的poll函數至關於每次調用都重起爐竈,從用戶空間完整讀入ufds,完成後再次徹底拷貝到用戶空間,另外每次poll都須要對全部設備作至少作一次加入和刪除等待隊列操做,這些都是低效的緣由。
epoll將以上狀況都細化考慮,不須要每次都完整讀入輸出ufds,只需使用epoll_ctl調整其中一小部分,不須要每次epoll_wait都執行一次加入刪除等待隊列操做,另外改進後的機制使的沒必要在某個設備就緒後搜索整個設備數組進行查找,這些都能提升效率。另外最明顯的一點,從用戶的使用來講,使用epoll沒必要每次都輪詢全部返回結果已找出其中的就緒部分,O(n)變O(1),對性能也提升很多。
此外這裏還發現一點,是否是將epoll_ctl改爲一次能夠處理多個fd(像semctl那樣)會提升些許性能呢?特別是在假設系統調用比較耗時的基礎上。不過關於系統調用的耗時問題還會在之後分析。
POLL/EPOLL測試數據對比:
測試的環境:我寫了三段代碼來分別模擬服務器,活動的客戶端,僵死的客戶端,服務器運行於一個自編譯的標準2.6.11內核系統上,硬件爲 PIII933,兩個客戶端各自運行在另外的PC上,這兩臺PC比服務器的硬件性能要好,主要是保證能輕易讓服務器滿載,三臺機器間使用一個100M交換機鏈接。
服務器接受並poll全部鏈接,若是有request到達則回覆一個response,而後繼續poll。
活動的客戶端(Active Client)模擬若干併發的活動鏈接,這些鏈接不間斷的發送請求接受回覆。
僵死的客戶端(zombie)模擬一些只鏈接但不發送請求的客戶端,其目的只是佔用服務器的poll描述符資源。
測試過程:保持10個併發活動鏈接,不斷的調整僵併發鏈接數,記錄在不一樣比例下使用poll與epoll的性能差異。僵死併發鏈接數根據比例分別是:0,10,20,40,80,160,320,640,1280,2560,5120,10240。
下圖中橫軸表示僵死併發鏈接與活動併發鏈接之比,縱軸表示完成40000次請求回覆所花費的時間,以秒爲單位。紅色線條表示poll數據,綠色表示 epoll數據。能夠看出,poll在所監控的文件描述符數量增長時,其耗時呈線性增加,而epoll則維持了一個平穩的狀態,幾乎不受描述符個數影響。
在監控的全部客戶端都是活動時,poll的效率會略高於epoll(主要在原點附近,即僵死併發鏈接爲0時,圖上不易看出來),究竟epoll實現比poll複雜,監控少許描述符並不是它的長處。
測試代碼及具體數據能夠從這裏得到,歡迎討論。
select()系統調用提供一個機制來實現同步多元I/O:
#include <sys/time.h> #include <sys/types.h> #include <unistd.h>
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_SET(int fd, fd_set *set); FD_ZERO(fd_set *set);
|
調用select()將阻塞,直到指定的文件描述符準備好執行I/O,或者可選參數timeout指定的時間已通過去。
監視的文件描述符分爲三類set,每一種對應等待不一樣的事件。readfds中列出的文件描述符被監視是否有數據可供讀取(若是讀取操做完成則不會阻塞)。writefds中列出的文件描述符則被監視是否寫入操做完成而不阻塞。最後,exceptfds中列出的文件描述符則被監視是否發生異常,或者沒法控制的數據是否可用(這些狀態僅僅應用於套接字)。這三類set能夠是NULL,這種狀況下select()不監視這一類事件。
select()成功返回時,每組set都被修改以使它只包含準備好I/O的文件描述符。例如,假設有兩個文件描述符,值分別是7和9,被放在readfds中。當select()返回時,若是7仍然在set中,則這個文件描述符已經準備好被讀取而不會阻塞。若是9已經不在set中,則讀取它將可能會阻塞(我說多是由於數據可能正好在select返回後就可用,這種狀況下,下一次調用select()將返回文件描述符準備好讀取)。
第一個參數n,等於全部set中最大的那個文件描述符的值加1。所以,select()的調用者負責檢查哪一個文件描述符擁有最大值,而且把這個值加1再傳遞給第一個參數。
timeout參數是一個指向timeval結構體的指針,timeval定義以下:
#include <sys/time.h> struct timeval { long tv_sec; /* seconds */ long tv_usec; /* 10E-6 second */ }; |
若是這個參數不是NULL,則即便沒有文件描述符準備好I/O,select()也會在通過tv_sec秒和tv_usec微秒後返回。當select()返回時,timeout參數的狀態在不一樣的系統中是未定義的,所以每次調用select()以前必須從新初始化timeout和文件描述符set。實際上,當前版本的Linux會自動修改timeout參數,設置它的值爲剩餘時間。所以,若是timeout被設置爲5秒,而後在文件描述符準備好以前通過了3秒,則這一次調用select()返回時tv_sec將變爲2。
若是timeout中的兩個值都設置爲0,則調用select()將當即返回,報告調用時全部未決的事件,但不等待任何隨後的事件。
文件描述符set不會直接操做,通常使用幾個助手宏來管理。這容許Unix系統以本身喜歡的方式來實現文件描述符set。但大多數系統都簡單地實現set爲位數組。FD_ZERO移除指定set中的全部文件描述符。每一次調用select()以前都應該先調用它。
fd_set writefds;
FD_ZERO(&writefds);
FD_SET添加一個文件描述符到指定的set中,FD_CLR則從指定的set中移除一個文件描述符:
FD_SET(fd, &writefds); /* add 'fd' to the set */
FD_CLR(fd, &writefds); /* oops, remove 'fd' from the set */
設計良好的代碼應該永遠不使用FD_CLR,並且實際狀況中它也確實不多被使用。
FD_ISSET測試一個文件描述符是否指定set的一部分。若是文件描述符在set中則返回一個非0整數,不在則返回0。FD_ISSET在調用select()返回以後使用,測試指定的文件描述符是否準備好相關動做:
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */
由於文件描述符set是靜態建立的,它們對文件描述符的最大數目強加了一個限制,可以放進set中的最大文件描述符的值由FD_SETSIZE指定。在Linux中,這個值是1024。本章後面咱們還將看到這個限制的衍生物。
返回值和錯誤代碼
select()成功時返回準備好I/O的文件描述符數目,包括全部三個set。若是提供了timeout,返回值多是0;錯誤時返回-1,而且設置errno爲下面幾個值之一:
EBADF
給某個set提供了無效文件描述符。
EINTR
等待時捕獲到信號,能夠從新發起調用。
EINVAL
參數n爲負數,或者指定的timeout非法。
ENOMEM
不夠可用內存來完成請求。
--------------------------------------------------------------------------------------------------------------
poll()系統調用是System V的多元I/O解決方案。它解決了select()的幾個不足,儘管select()仍然常用(多數仍是出於習慣,或者打着可移植的名義):
#include <sys/poll.h> int poll (struct pollfd *fds, unsigned int nfds, int timeout); |
和select()不同,poll()沒有使用低效的三個基於位的文件描述符set,而是採用了一個單獨的結構體pollfd數組,由fds指針指向這個組。pollfd結構體定義以下:
#include <sys/poll.h>
struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ }; |
每個pollfd結構體指定了一個被監視的文件描述符,能夠傳遞多個結構體,指示poll()監視多個文件描述符。每一個結構體的events域是監視該文件描述符的事件掩碼,由用戶來設置這個域。revents域是文件描述符的操做結果事件掩碼。內核在調用返回時設置這個域。events域中請求的任何事件均可能在revents域中返回。合法的事件以下:
POLLIN
有數據可讀。
POLLRDNORM
有普通數據可讀。
POLLRDBAND
有優先數據可讀。
POLLPRI
有緊迫數據可讀。
POLLOUT
寫數據不會致使阻塞。
POLLWRNORM
寫普通數據不會致使阻塞。
POLLWRBAND
寫優先數據不會致使阻塞。
POLLMSG
SIGPOLL消息可用。
此外,revents域中還可能返回下列事件:
POLLER
指定的文件描述符發生錯誤。
POLLHUP
指定的文件描述符掛起事件。
POLLNVAL
指定的文件描述符非法。
這些事件在events域中無心義,由於它們在合適的時候老是會從revents中返回。使用poll()和select()不同,你不須要顯式地請求異常狀況報告。
POLLIN | POLLPRI等價於select()的讀事件,POLLOUT | POLLWRBAND等價於select()的寫事件。POLLIN等價於POLLRDNORM | POLLRDBAND,而POLLOUT則等價於POLLWRNORM。
例如,要同時監視一個文件描述符是否可讀和可寫,咱們能夠設置events爲POLLIN | POLLOUT。在poll返回時,咱們能夠檢查revents中的標誌,對應於文件描述符請求的events結構體。若是POLLIN事件被設置,則文件描述符能夠被讀取而不阻塞。若是POLLOUT被設置,則文件描述符能夠寫入而不致使阻塞。這些標誌並非互斥的:它們可能被同時設置,表示這個文件描述符的讀取和寫入操做都會正常返回而不阻塞。
timeout參數指定等待的毫秒數,不管I/O是否準備好,poll都會返回。timeout指定爲負數值表示無限超時;timeout爲0指示poll調用當即返回並列出準備好I/O的文件描述符,但並不等待其它的事件。這種狀況下,poll()就像它的名字那樣,一旦選舉出來,當即返回。
返回值和錯誤代碼
成功時,poll()返回結構體中revents域不爲0的文件描述符個數;若是在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1,並設置errno爲下列值之一:
EBADF
一個或多個結構體中指定的文件描述符無效。
EFAULT
fds指針指向的地址超出進程的地址空間。
EINTR
請求的事件以前產生一個信號,調用能夠從新發起。
EINVAL
nfds參數超出PLIMIT_NOFILE值。
ENOMEM
可用內存不足,沒法完成請求。
--------------------------------------------------------------------------------------------------------------
以上內容來自《OReilly.Linux.System.Programming - Talking.Directly.to.the.Kernel.and.C.Library.2007》
--------------------------------------------------------------------------------------------------------------
epoll的優勢:
1.支持一個進程打開大數目的socket描述符(FD)
select 最不能忍受的是一個進程所打開的FD是有必定限制的,由FD_SETSIZE設置,默認值是2048。對於那些須要支持的上萬鏈接數目的IM服務器來講顯然太少了。這時候你一是能夠選擇修改這個宏而後從新編譯內核,不過資料也同時指出這樣會帶來網絡效率的降低,二是能夠選擇多進程的解決方案(傳統的 Apache方案),不過雖然linux上面建立進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,因此也不是一種完美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大能夠打開文件的數目,這個數字通常遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目能夠cat /proc/sys/fs/file-max察看,通常來講這個數目和系統內存關係很大。
2.IO效率不隨FD數目增長而線性降低
傳統的select/poll另外一個致命弱點就是當你擁有一個很大的socket集合,不過因爲網絡延時,任一時間只有部分的socket是"活躍"的,可是select/poll每次調用都會線性掃描所有的集合,致使效率呈現線性降低。可是epoll不存在這個問題,它只會對"活躍"的socket進行操做---這是由於在內核實現中epoll是根據每一個fd上面的callback函數實現的。那麼,只有"活躍"的socket纔會主動的去調用 callback函數,其餘idle狀態socket則不會,在這點上,epoll實現了一個"僞"AIO,由於這時候推進力在os內核。在一些 benchmark中,若是全部的socket基本上都是活躍的---好比一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,若是過多使用epoll_ctl,效率相比還有稍微的降低。可是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。
3.使用mmap加速內核與用戶空間的消息傳遞。
這點實際上涉及到epoll的具體實現了。不管是select,poll仍是epoll都須要內核把FD消息通知給用戶空間,如何避免沒必要要的內存拷貝就很重要,在這點上,epoll是經過內核於用戶空間mmap同一塊內存實現的。而若是你想我同樣從2.5內核就關注epoll的話,必定不會忘記手工 mmap這一步的。
4.內核微調
這一點其實不算epoll的優勢了,而是整個linux平臺的優勢。也許你能夠懷疑linux平臺,可是你沒法迴避linux平臺賦予你微調內核的能力。好比,內核TCP/IP協議棧使用內存池管理sk_buff結構,那麼能夠在運行時期動態調整這個內存pool(skb_head_pool)的大小--- 經過echo XXXX>/proc/sys/net/core/hot_list_length完成。再好比listen函數的第2個參數(TCP完成3次握手的數據包隊列長度),也能夠根據你平臺內存大小動態調整。更甚至在一個數據包面數目巨大但同時每一個數據包自己大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。
epoll學習,epoll與select,pool區別
在linux網絡編程中,很長的時間都是用select來作事件觸發.在linux新內核中,有了一種替換它的機制,就是epoll.
相比於select,epoll最大的好處在於它不會隨着監聽fd數目的增加而下降效率.由於在內核中的select實現中,它是採用輪詢來處理的,輪詢的fd數據數據越多,天然耗時就越多.
epoll的接口三個函數
1) int epoll_create(int size);
建立一個epoll句柄,size用來告訴內核這個監聽的數據一共有多大.
須要注意的是,當建立好epoll句柄後,它會佔用一個fd值,在使用完epoll後,必須調用close關閉,不然可能致使fd被耗盡.
2) int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
epoll註冊函數,第一個參數是epoll_create()的返回值.
第二個參數表示動做,用三個宏來表示
EPOLL_CTL_ADD:註冊新的fd到epfd中
EPOLL_CTL_MOD: 修改已經註冊的fd的監聽事件
EPOLL_CTL_DEL:從epfd中刪除一個fd
第三個參數是須要監聽的fd
第四個參數是告訴內核須要監聽什麼事.
struct_event結構以下:
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events;
events能夠是如下幾個宏的集合:
EPOLLIN:表示對應的文件描述符可讀(包括對端SOCKET正常關閉)
EPOLLOUT:表示對應的文件描述符能夠寫
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀
EPOLLERR表示對應的文件描述符發生錯誤
EPOLLHUP:表示對應的文件描述符被掛斷
EPOLLET:將EPOLL設爲邊緣觸發模式(Edge Triggered).這是相對於水平觸發(Level Triggered)來講的
EPOLLONSHOT:只監聽一次,當監聽玩此次事件以後,若是還須要繼續監聽這個socket的話,須要再次把這個socket加入到WPOLL隊列裏
3) int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
參數events用來從內核獲得事件的集合,maxevents告以內核這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,參數timeout是超時時間(毫秒),0會當即返回,-1將不肯定,也有說法是永久阻塞.該函數返回須要處理的事件數目,如返回0表示已超時.
關於ET.LT兩種工做模式
ET模式僅當狀態發送變化的時候纔得到通知,這裏所謂的狀態的變化並不包括緩衝區中還有未處理的數據,也就是說.若是要採用ET模式,須要一直read/write直到出錯爲止.不少人反映爲何採用ET模式只接收了一部分數據就再也得不到通知了,大可能是由於這樣.
而LT模式就是隻要有數據沒有處理就會一直通知下去.
epoll模型
首先經過create_epoll(int maxfds)來建立一個epoll的句柄,其中maxfds爲你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,以後的全部操做將經過這個句柄來進行操做。在用完以後,記得用close()來關閉這個建立出來的epoll句柄。
以後在你的網絡主循環裏面,每一幀的調用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢全部的網絡接口,看哪個能夠讀,哪個能夠寫了。基本的語法爲:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd爲用epoll_create建立以後的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操做成功以後,epoll_events裏面將儲存全部的讀寫事件。max_events是當前須要監聽的全部socket句柄數。最後一個timeout是 epoll_wait的超時,爲0的時候表示立刻返回,爲-1的時候表示一直等下去,直到有事件範圍,爲任意正整數的時候表示等這麼長的時間,若是一直沒有事件,則範圍。通常若是網絡主循環是單獨的線程的話,能夠用-1來等,這樣能夠保證一些效率,若是是和主邏輯在同一個線程的話,則能夠用0來保證主循環的效率。
epoll_wait範圍以後應該是一個循環,遍利全部的事件。
#include<iostream> #include<sys/socket.h> #include<sys/epoll.h> #include<netinet/in.h> #include<arpa/inet.h> #include<fcntl.h> #include<unistd.h> #include<stdio.h> #include<errno.h> using namespace std; int main(int argc,char *argv[]) { int maxi,listenfd,connfd,sockfd,epfd,nfds; ssize_t n; char line[100]; listenfd = socket(AF_INET,SOCK_STREAM,0);
epoll和select,poll區別
1. 支持一個進程打開大數目的socket描述符(FD)
select最不能忍受的是一個進程所打開的FD是有必定限制的,由FD_SETSIZE設置,默認值是2048。對於那些須要支持的上萬鏈接數目的IM服務器來講顯然太少了。這時候你一是能夠選擇修改這個宏而後從新編譯內核,不過資料也同時指出這樣會帶來網絡效率的降低
2. IO效率不隨FD數目增長而線性降低
傳統的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過因爲網絡延時,任一時間只有部分socket是「活躍」的,可是select/poll每次調用都會線性掃描所有的集合,致使效率呈線性降低。可是epoll不存在這個問題,它只會對「活躍」的socket進行操做——這是由於在內核實現中epoll是根據每一個fd上面的callback函數實現的。
epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先經過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。
3. 使用mmap加速內核與用戶空間的消息傳遞
不管是select,poll仍是epoll都須要內核把FD消息通知給用戶空間,如何避免沒必要要的內存拷貝就很重要,在這點上,epoll是經過內核於用戶空間mmap同一塊內存實現的
4. 內核微調
這一點其實不算epoll的優勢了,而是整個Linux平臺的優勢.
相關文章
select、poll、epoll使用小結
Linux上可使用不一樣的I/O模型,咱們能夠經過下圖瞭解經常使用的I/O模型:同步和異步模型,以及阻塞和非阻塞模型,本文主要分析其中的異步阻塞模型。
1、select使用
這個模型中配置的是非阻塞I/O,而後使用阻塞select系統調用來肯定一個I/O描述符什麼時候有操做。使用select調用能夠爲多個描述符提供通知,對於每一個提示符,咱們能夠請求描述符的可寫,可讀以及是否發生錯誤。異步阻塞I/O的系統流程以下圖所示:
使用select經常使用的幾個函數以下:
- FD_ZERO(int fd, fd_set* fds)
- FD_SET(int fd, fd_set* fds)
- FD_ISSET(int fd, fd_set* fds)
- FD_CLR(int fd, fd_set* fds)
- int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
fd_set類型能夠簡單的理解爲按bit位標記句柄的隊列。具體的置位、驗證可使用FD_SET,FD_ISSET等宏實現。在select函數中,readfds、writefds和exceptfds同時做爲輸入參數和輸出參數,若是readfds標記了一個位置,則,select將檢測到該標記位可讀。timeout爲設置的超時時間。
下面咱們來看如何使用select:
- SOCKADDR_IN addrSrv;
- int reuse = 1;
- SOCKET sockSrv,connsock;
- SOCKADDR_IN addrClient;
- pool pool;
- int len=sizeof(SOCKADDR);
- sockSrv=socket(AF_INET,SOCK_STREAM,0);
-
- addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
- addrSrv.sin_family=AF_INET;
- addrSrv.sin_port=htons(port);
-
- if(bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR))<0)
- {
- fprintf(stderr,"Failed to bind");
- return ;
- }
-
- if(listen(sockSrv,5)<0)
- {
- fprintf(stderr,"Failed to listen socket");
- return ;
- }
- setsockopt(sockSrv,SOL_SOCKET,SO_REUSEADDR,(const char*)&reuse,sizeof(reuse));
- init_pool(sockSrv,&pool);
- while(1)
- {
-
- pool.ready_set=pool.read_set;
- pool.nready=select(pool.maxfd+1,&pool.ready_set,NULL,NULL,NULL);
- if(FD_ISSET(sockSrv,&pool.ready_set))
- {
- connsock=accept(sockSrv,(SOCKADDR *)&addrClient,&len);
-
-
- add_client(connsock,&pool);
- }
-
- check_client(&pool);
- }
上面是一個服務器代碼的關鍵部分,設置爲異步的模式,而後接受到鏈接將其添加到鏈接池中。監聽描述符上使用select,接受客戶端的鏈接請求,在check_client函數中,遍歷鏈接池中的描述符,檢查是否有事件發生。
2、poll使用
poll函數相似於select,可是其調用形式不一樣。poll不是爲每一個條件構造一個描述符集,而是構造一個pollfd結構體數組,每一個數組元素指定一個描述符標號及其所關心的條件。定義以下:
- #include <sys/poll.h>
- int poll (struct pollfd *fds, unsigned int nfds, int timeout);
- struct pollfd {
- int fd;
- short events;
- short revents;
- };
每一個結構體的events域是由用戶來設置,告訴內核咱們關注的是什麼,而revents域是返回時內核設置的,以說明對該描述符發生了什麼事件。這點與select不一樣,select修改其參數以指示哪個描述符準備好了。在《unix環境高級編程》中有一張events取值的表,以下:
POLLIN :可讀除高優級外的數據,不阻塞
POLLRDNORM:可讀普通數據,不阻塞
POLLRDBAND:可讀O優先數據,不阻塞
POLLPRI:可讀高優先數據,不阻塞
POLLOUT :可寫普數據,不阻塞
POLLWRNORM:與POLLOUT相同
POLLWRBAND:寫非0優先數據,不阻塞
其次revents還有下面取值
POLLERR :已出錯
POLLHUP:已掛起,當以描述符被掛起後,就不能再寫向該描述符,可是仍能夠從該描述符讀取到數據。
POLLNVAL:此描述符並不引用一打開文件
對poll函數,nfds表示fds中的元素數,timeout爲超時設置,單位爲毫秒若爲0,表示不等待,爲-1表示描述符中一個已經準備好或捕捉到一個信號返回,大於0表示描述符準備好,或超時返回。函數返回值返回值若爲0,表示沒有事件發生,-1表示錯誤,並設置errno,大於0表示有幾個描述符有事件。
poll的使用和select基本相似。在此再也不介紹。poll相對因而select的優點是監聽的描述符數量沒有限制。
3、epoll學習
epoll有兩種模式,Edge Triggered(簡稱ET) 和 Level Triggered(簡稱LT).在採用這兩種模式時要注意的是,若是採用ET模式,那麼僅當狀態發生變化時纔會通知,而採用LT模式相似於原來的select/poll操做,只要還有沒有處理的事件就會一直通知.
1)epoll數據結構介紹:
- typedef union epoll_data
- {
- void *ptr;
- int fd;
- __uint32_t u32;
- __uint64_t u64;
- } epoll_data_t;
-
- struct epoll_event
- {
- __uint32_t events;
- epoll_data_t data;
- };
常見的事件以下:
EPOLLIN:表示對描述符的能夠讀
EPOLLOUT:表示對描述符的能夠寫
EPOLLPRI:表示對描述符的有緊急數據能夠讀
EPOLLERR:發生錯誤
EPOLLHUP:掛起
EPOLLET:邊緣觸發
EPOLLONESHOT:一次性使用,當監聽完此次事件以後,若是還須要繼續監聽這個socket的話,須要再次把這個socket加入到EPOLL隊列裏
2)函數介紹
epoll的三個函數
- int epoll_creae(int size);
功能:該函數生成一個epoll專用的文件描述符
參數:size爲epoll上能關注的最大描述符數
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:用於控制某個epoll文件描述符時間,能夠註冊、修改、刪除
參數:epfd由epoll_create生成的epoll專用描述符
op操做:EPOLL_CTL_ADD 註冊 EPOLL_CTL_MOD修改 EPOLL_DEL刪除
fd:關聯的文件描述符
evnet告訴內核要監聽什麼事件
- int epoll_wait(int epfd,struct epoll_event*events,int maxevents,int timeout);
功能:該函數等待i/o事件的發生。
參數:epfd要檢測的句柄
events:用於回傳待處理時間的數組
maxevents:告訴內核這個events有多大,不能超過以前的size
timeout:爲超時時間
使用方法參考:https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/epoll-example.c
epoll支持的FD上限是最大能夠打開文件的數目(select面臨這樣的問題),IO效率不隨FD數目增長而線性降低(select、poll面臨的問題)使用mmap加速內核與用戶空間的消息傳遞。如今libevent封裝了幾種的實現,能夠經過使用libevent來實現多路複用。
本文參考:https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/
http://www.ibm.com/developerworks/cn/linux/l-cn-edntwk/index.html?ca=drs-
http://www.ibm.com/developerworks/cn/linux/l-async/