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