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