《UNIX網絡編程》裏並無提到epoll,不知道爲啥,如下的內容是根據linux manual總結的。java
epoll是在linux上提供的實現IO複用的機制。epoll與poll相似,能夠同時監聽多個描述符;epoll新增了邊緣觸發和水平觸發的概念,並且在處理大量描述符時更有優點。linux
epoll API中核心概念就是epoll實例,它是一個內核內的數據結構,從用戶角度來看它能夠簡單的看作包含了兩個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_create1
與epoll_create
相類似,但參數變成了flags,size則被忽略。這裏的flags有一個可選項:EPOLL_CLOEXEC
,EPOLL_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),它們的區別能夠經過如下的例子來講明:
rfd
,咱們將從它讀取一個pipe輸出,咱們將其註冊到epoll實例中,感興趣的事件爲可讀epoll_wait
,這時rfd
會被放到ready list中而後成功返回epoll_wait
若是rfd
在註冊到epoll實例時使用了EPOLLET
選項,那麼上述第5步調用epoll_wait
可能會發生阻塞,儘管這時讀取緩衝區裏仍有可讀取的數據;而同時pipe的另外一端可能在等待着相應的響應,因而陷入了無盡的互相等待。而出現這種現象的緣由在於ET僅在描述符發生變化時纔會返回事件。在上面的例子當中,第2步會產生一個事件,而第3步會消費這個事件。由於第4步沒有讀取完全部的數據,因此第5步可能會陷入無限期的阻塞。
而linux manual建議的邊緣觸發的使用方式以下:
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_MOD
在EPOLLIN和EPOLLOUT
之間來回切換,若是在水平模式下就不能這樣作了,由於感興趣的事件一旦就緒的事件就會持續發生,帶來沒必要要的消耗。
epoll的介紹裏提到epoll比poll更快,根據網上的其餘博客總結了如下幾點緣由:
這裏順便提一下LT的實現,epoll_wait
在返回就緒描述符前會檢查描述符的觸發類型,若是是水平觸發而且描述符上有未處理的數據,則會將其加入剛纔清空的ready list,這樣下次調用epoll_wait
時ready list仍會有該描述符。這也是LT和ET的表現的差異的實際緣由。