Epoll,位于頭文件sys/epoll.h,是Linux系統(tǒng)上的I/O事件通知基礎設施。epoll API為Linux系統(tǒng)專有,于內核2.5.44中首次引入,glibc于2.3.2版本加入支持。其它提供類似的功能的系統(tǒng),包括FreeBSD kqueue,Solaris /dev/poll等。
Epoll API
Epoll API實現(xiàn)了與poll類似的功能:監(jiān)測多個文件描述符上是否可以執(zhí)行I/O操作。支持邊緣觸發(fā)ET和水平觸發(fā)LT,相比poll支持監(jiān)測數(shù)量更多的文件描述符。
以下API用于創(chuàng)建和管理epoll實例:
epoll_create:創(chuàng)建Epoll實例,并返回Epoll實例關聯(lián)的文件描述符。(最新的epoll_create1擴展了epoll_create的功能)
create_ctl:注冊關注的文件描述符。注冊于同一epoll實例的一組文件描述符被稱為epoll set,可以通過進程對應的/proc/[pid]/fdinfo目錄查看。
epoll_wait:等待I/O事件,如果當前沒有任何注冊事件處于可用狀態(tài),調用線程會被阻塞。
水平觸發(fā)LT與邊緣觸發(fā)ET
Epoll事件分發(fā)接口可以使用ET和LT兩種模式。兩種模式的差別描述如下。
典型場景:
1 管道(pipe)讀端的文件描述符(rfd)注冊于Epoll實例。
2 寫者(Writer)向管道(pipe)寫端寫2KB的數(shù)據(jù)。
3 epoll_wait調用結束,返回rfd作為就緒的文件描述符。
4 管道讀者(pipe reader) 從rfd讀1KB的數(shù)據(jù)。
5 下一次epoll_wait調用。
如果rfd文件描述符使用EPOLLET(邊緣觸發(fā))標記加入Epoll接口,第5步對epoll_wait的調用可能會掛住,盡管文件輸入緩沖區(qū)中仍然有可用數(shù)據(jù);與此同時,遠端實體由于已經(jīng)發(fā)送數(shù)據(jù),可能正在等待回應。其原因是邊緣觸發(fā)模式僅在所監(jiān)控的文件描述符狀態(tài)發(fā)生變化時才投遞事件。所以,第5步的調用方可能最終一直在等待數(shù)據(jù)到來,但數(shù)據(jù)其實已經(jīng)在輸入緩存區(qū)。經(jīng)過第2步的寫操作和第3步的事件處理,rfd上只會產(chǎn)生一次事件。由于第4步的讀操作沒有讀完全部的緩沖區(qū)數(shù)據(jù),第5步對epoll_wait的調用可能會永遠阻塞。
使用EPOLLET標記時,應該設置文件描述符為非阻塞,以避免阻塞讀寫,使處理多個文件描述符的任務餓死。因此,使用Epoll 邊緣觸發(fā)(EPOLLET)模式的接口,以下有兩點建議:
1 使用非阻塞的文件描述符
2 只有在read或write返回EAGAIN之后,才繼續(xù)等待事件(調用epoll_wait)
相比之下,當Epoll作為水平觸發(fā)接口(LT,默認模式)使用時,epoll相當于一個更快的poll,可以用于poll適用的任何場景,因為二者語義相同。
在邊緣觸發(fā)模式下,當收到多個數(shù)據(jù)塊時也可能會產(chǎn)生多個事件,調用方可以通過設置EPOLLONESHOT標記,告訴epoll當通過epoll_wait收到事件時,取消關聯(lián)的文件描述符。當給epoll設置EPOLLONESHOT標記時,調用方需要通過epoll_ctl對文件描述符設置EPOLL_CTL_MOD標記。
使用范例
當Epoll作為水平觸發(fā)接口使用時與poll語義相同,而作為邊緣觸發(fā)接口使用時需要注意應用層事件循環(huán)的細節(jié),以避免錯誤。以下舉例。設想一個非阻塞的socket為監(jiān)聽者,可以在該socket上調用listen。函數(shù)do_use_fd()處理新就緒的文件描述符,直到遇到讀(read)或寫(write)返回EAGAIN。事件驅動的狀態(tài)機應用,應該在收到EAGAIN之后記錄當前狀態(tài),以便在下次調用do_use_fd時,能夠繼續(xù)從之前停止讀寫數(shù)據(jù)的地方繼續(xù)讀寫(read / write)。
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
#define MAX_EVENTS 10
structepoll_event ev, events[MAX_EVENTS];
intlisten_sock, conn_sock, nfds, epollfd;
/*Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd= epoll_create1(0);
if(epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events= EPOLLIN;
ev.data.fd= listen_sock;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for(;;) {
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) {
conn_sock = accept(listen_sock,
(struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD,conn_sock,
&ev) == -1) {
perror("epoll_ctl:conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}
當作為邊緣觸發(fā)接口使用時,為性能考慮,可以通過設置EPOLLIN|EPOLLOUT,一次加入文件描述符到epoll接口(EPOLL_CTL_ADD)。這樣可以避免在EPOLLIN和EPOLLOUT之間通過調用epoll_ctl(EPOLL_CTL_MOD)切換。
自動休眠問題
如果系統(tǒng)設置了自動休眠模式(通過/sys/power/autosleep),當喚醒設備的事件發(fā)生時,設備驅動會保持喚醒狀態(tài),直到事件進入排隊狀態(tài)。為了保持設備喚醒直到事件處理完成,必須使用epoll EPOLLWAKEUP 標記。
一旦給structe poll_event中的events字段設置了EPOLLWAKEUP標記,系統(tǒng)會在事件排隊時就保持喚醒,從epoll_wait調用開始,持續(xù)要下一次epoll_wait調用。
監(jiān)測數(shù)量限制
以下文件可以用來限制epoll使用的內核態(tài)內存空間大小(Linux 2.6.28 開始):
/proc/sys/fs/epoll/max_user_watches
max_user_watches文件用來設置用戶在所有epoll實例中注冊的文件描述符數(shù)量上限,作用于每個用戶ID。單個注冊文件描述符在32位內核上消耗90字節(jié),在64位內核上消耗160字節(jié)。max_user_watches的默認值是可用內核內存空間的1/25(4%)除以單個注冊文件描述符消耗的字節(jié)數(shù)。
避免饑餓(邊緣觸發(fā))
如果I/O數(shù)據(jù)量很大,可能在讀取數(shù)據(jù)的過程中其他文件得不到處理,造成饑餓。解決方法是維護一個就緒列表,在關聯(lián)數(shù)據(jù)結構中標記文件描述符為就緒狀態(tài),由此可以記住哪些文件在等待,并對所有就緒文件作輪轉處理。
事件緩存陷阱
如果使用事件緩存,或者存儲epoll_wait返回的所有文件描述符,就需要提供方法動態(tài)標記關閉狀態(tài)(比如,由于其他事件處理造成文件描述符關閉),假設從epoll_wait收到100個事件,A事件造成B事件關閉,如果移除B事件結構并關閉文件描述符,事件緩存仍然認為有事件在等待文件描述符,從而造成混亂。
解決方法是,在A事件處理過程中,調用epoll_ctl(EPOLL_CTL_DEL)來移除B文件描述符并關閉,然后標記關聯(lián)的數(shù)據(jù)結構為已移除,并關聯(lián)到移除列表。在后續(xù)事件處理過程中,當發(fā)現(xiàn)B文件描述符的新事件時,可以通過檢查標記發(fā)現(xiàn)文件描述符已移除,避免產(chǎn)生混亂。
?
評論