- 論壇徽章:
- 0
|
什么是epoll
epoll是什么?按照man手冊(cè)的說法:是為處理大批量句柄而作了改進(jìn)的poll。當(dāng)然,這不是2.6內(nèi)核才有的,它是在2.5.44內(nèi)核中被引進(jìn)的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它幾乎具備了之前所說的一切優(yōu)點(diǎn),被公認(rèn)為Linux2.6下性能最好的多路I/O就緒通知方法。
?
epoll的相關(guān)系統(tǒng)調(diào)用
epoll只有epoll_create,epoll_ctl,epoll_wait 3個(gè)系統(tǒng)調(diào)用。
?
1. int epoll_create(int size);
創(chuàng)建一個(gè)epoll的句柄。自從linux2.6.8之后,size參數(shù)是被忽略的。需要注意的是,當(dāng)創(chuàng)建好epoll句柄后,它就是會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。
?
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊(cè)函數(shù),它不同于select()是在監(jiān)聽事件時(shí)告訴內(nèi)核要監(jiān)聽什么類型的事件,而是在這里先注冊(cè)要監(jiān)聽的事件類型。
第一個(gè)參數(shù)是epoll_create()的返回值。
第二個(gè)參數(shù)表示動(dòng)作,用三個(gè)宏來表示:
EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中;
EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd;
?
第三個(gè)參數(shù)是需要監(jiān)聽的fd。
第四個(gè)參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事,struct epoll_event結(jié)構(gòu)如下:
//保存觸發(fā)事件的某個(gè)文件描述符相關(guān)的數(shù)據(jù)(與具體使用方式有關(guān))
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
//感興趣的事件和被觸發(fā)的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下幾個(gè)宏的集合:
EPOLLIN?:表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉);
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET:?將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來說的。
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個(gè)socket的話,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll監(jiān)控的事件中已經(jīng)發(fā)送的事件。參數(shù)events是分配好的epoll_event結(jié)構(gòu)體數(shù)組,epoll將會(huì)把發(fā)生的事件賦值到events數(shù)組中(events不可以是空指針,內(nèi)核只負(fù)責(zé)把數(shù)據(jù)復(fù)制到這個(gè)events數(shù)組中,不會(huì)去幫助我們?cè)谟脩魬B(tài)中分配內(nèi)存)。maxevents告之內(nèi)核這個(gè)events有多大,這個(gè)?maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size,參數(shù)timeout是超時(shí)時(shí)間(毫秒,0會(huì)立即返回,-1將不確定,也有說法說是永久阻塞)。如果函數(shù)調(diào)用成功,返回對(duì)應(yīng)I/O上已準(zhǔn)備好的文件描述符數(shù)目,如返回0表示已超時(shí)。
?
epoll工作原理
epoll同樣只告知那些就緒的文件描述符,而且當(dāng)我們調(diào)用epoll_wait()獲得就緒文件描述符時(shí),返回的不是實(shí)際的描述符,而是一個(gè)代表就緒描述符數(shù)量的值,你只需要去epoll指定的一個(gè)數(shù)組中依次取得相應(yīng)數(shù)量的文件描述符即可,這里也使用了內(nèi)存映射(mmap)技術(shù),這樣便徹底省掉了這些文件描述符在系統(tǒng)調(diào)用時(shí)復(fù)制的開銷。
?
另一個(gè)本質(zhì)的改進(jìn)在于epoll采用基于事件的就緒通知方式。在select/poll中,進(jìn)程只有在調(diào)用一定的方法后,內(nèi)核才對(duì)所有監(jiān)視的文件描述符進(jìn)行掃描,而epoll事先通過epoll_ctl()來注冊(cè)一個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí),內(nèi)核會(huì)采用類似callback的回調(diào)機(jī)制,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用epoll_wait()時(shí)便得到通知。
?
Epoll的2種工作方式-水平觸發(fā)(LT)和邊緣觸發(fā)(ET)
假如有這樣一個(gè)例子:
1. 我們已經(jīng)把一個(gè)用來從管道中讀取數(shù)據(jù)的文件句柄(RFD)添加到epoll描述符
2. 這個(gè)時(shí)候從管道的另一端被寫入了2KB的數(shù)據(jù)
3. 調(diào)用epoll_wait(2),并且它會(huì)返回RFD,說明它已經(jīng)準(zhǔn)備好讀取操作
4. 然后我們讀取了1KB的數(shù)據(jù)
5. 調(diào)用epoll_wait(2)......
Edge Triggered 工作模式:
如果我們?cè)诘?步將RFD添加到epoll描述符的時(shí)候使用了EPOLLET標(biāo)志,那么在第5步調(diào)用epoll_wait(2)之后將有可能會(huì)掛起,因?yàn)槭S嗟臄?shù)據(jù)還存在于文件的輸入緩沖區(qū)內(nèi),而且數(shù)據(jù)發(fā)出端還在等待一個(gè)針對(duì)已經(jīng)發(fā)出數(shù)據(jù)的反饋信息。只有在監(jiān)視的文件句柄上發(fā)生了某個(gè)事件的時(shí)候 ET 工作模式才會(huì)匯報(bào)事件。因此在第5步的時(shí)候,調(diào)用者可能會(huì)放棄等待仍在存在于文件輸入緩沖區(qū)內(nèi)的剩余數(shù)據(jù)。在上面的例子中,會(huì)有一個(gè)事件產(chǎn)生在RFD句柄上,因?yàn)樵诘?步執(zhí)行了一個(gè)寫操作,然后,事件將會(huì)在第3步被銷毀。因?yàn)榈?步的讀取操作沒有讀空文件輸入緩沖區(qū)內(nèi)的數(shù)據(jù),因此我們?cè)诘?步調(diào)用 epoll_wait(2)完成后,是否掛起是不確定的。epoll工作在ET模式的時(shí)候,必須使用非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫操作把處理多個(gè)文件描述符的任務(wù)餓死。最好以下面的方式調(diào)用ET模式的epoll接口,在后面會(huì)介紹避免可能的缺陷。
? ?i ? ?基于非阻塞文件句柄
? ?ii ? 只有當(dāng)read(2)或者write(2)返回EAGAIN時(shí)才需要掛起,等待。但這并不是說每次read()時(shí)都需要循環(huán)讀,直到讀到產(chǎn)生一個(gè)EAGAIN才認(rèn)為此次事件處理完成,當(dāng)read()返回的讀到的數(shù)據(jù)長度小于請(qǐng)求的數(shù)據(jù)長度時(shí),就可以確定此時(shí)緩沖中已沒有數(shù)據(jù)了,也就可以認(rèn)為此事讀事件已處理完成。
Level Triggered 工作模式
相反的,以LT方式調(diào)用epoll接口的時(shí)候,它就相當(dāng)于一個(gè)速度比較快的poll(2),并且無論后面的數(shù)據(jù)是否被使用,因此他們具有同樣的職能。因?yàn)榧词故褂肊T模式的epoll,在收到多個(gè)chunk的數(shù)據(jù)的時(shí)候仍然會(huì)產(chǎn)生多個(gè)事件。調(diào)用者可以設(shè)定EPOLLONESHOT標(biāo)志,在 epoll_wait(2)收到事件后epoll會(huì)與事件關(guān)聯(lián)的文件句柄從epoll描述符中禁止掉。因此當(dāng)EPOLLONESHOT設(shè)定后,使用帶有 EPOLL_CTL_MOD標(biāo)志的epoll_ctl(2)處理文件句柄就成為調(diào)用者必須作的事情。
LT(level triggered)是epoll缺省的工作方式,并且同時(shí)支持block和no-block socket.在這種做法中,內(nèi)核告訴你一個(gè)文件描述符是否就緒了,然后你可以對(duì)這個(gè)就緒的fd進(jìn)行IO操作。如果你不作任何操作,內(nèi)核還是會(huì)繼續(xù)通知你?的,所以,這種模式編程出錯(cuò)誤可能性要小一點(diǎn)。傳統(tǒng)的select/poll都是這種模型的代表.
?
ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ET與LT的區(qū)別在于,當(dāng)一個(gè)新的事件到來時(shí),ET模式下當(dāng)然可以從epoll_wait調(diào)用中獲取到這個(gè)事件,可是如果這次沒有把這個(gè)事件對(duì)應(yīng)的套接字緩沖區(qū)處理完,在這個(gè)套接字中沒有新的事件再次到來時(shí),在ET模式下是無法再次從epoll_wait調(diào)用中獲取這個(gè)事件的。而LT模式正好相反,只要一個(gè)事件對(duì)應(yīng)的套接字緩沖區(qū)還有數(shù)據(jù),就總能從epoll_wait中獲取這個(gè)事件。
因此,LT模式下開發(fā)基于epoll的應(yīng)用要簡單些,不太容易出錯(cuò)。而在ET模式下事件發(fā)生時(shí),如果沒有徹底地將緩沖區(qū)數(shù)據(jù)處理完,則會(huì)導(dǎo)致緩沖區(qū)中的用戶請(qǐng)求得不到響應(yīng)。
圖示說明:
Nginx默認(rèn)采用ET模式來使用epoll。
?
epoll的優(yōu)點(diǎn):
1.支持一個(gè)進(jìn)程打開大數(shù)目的socket描述符(FD)
??? select?最不能忍受的是一個(gè)進(jìn)程所打開的FD是有一定限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是2048。對(duì)于那些需要支持的上萬連接數(shù)目的IM服務(wù)器來說顯然太少了。這時(shí)候你一是可以選擇修改這個(gè)宏然后重新編譯內(nèi)核,不過資料也同時(shí)指出這樣會(huì)帶來網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的?Apache方案),不過雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案。不過?epoll則沒有這個(gè)限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
?
2.IO效率不隨FD數(shù)目增加而線性下降
????傳統(tǒng)的select/poll另一個(gè)致命弱點(diǎn)就是當(dāng)你擁有一個(gè)很大的socket集合,不過由于網(wǎng)絡(luò)延時(shí),任一時(shí)間只有部分的socket是"活躍"的,但是select/poll每次調(diào)用都會(huì)線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個(gè)問題,它只會(huì)對(duì)"活躍"的socket進(jìn)行操作---這是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中epoll是根據(jù)每個(gè)fd上面的callback函數(shù)實(shí)現(xiàn)的。那么,只有"活躍"的socket才會(huì)主動(dòng)的去調(diào)用?callback函數(shù),其他idle狀態(tài)socket則不會(huì),在這點(diǎn)上,epoll實(shí)現(xiàn)了一個(gè)"偽"AIO,因?yàn)檫@時(shí)候推動(dòng)力在os內(nèi)核。在一些?benchmark中,如果所有的socket基本上都是活躍的---比如一個(gè)高速LAN環(huán)境,epoll并不比select/poll有什么效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環(huán)境,epoll的效率就遠(yuǎn)在select/poll之上了。
?
3.使用mmap加速內(nèi)核與用戶空間的消息傳遞
????這點(diǎn)實(shí)際上涉及到epoll的具體實(shí)現(xiàn)了。無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點(diǎn)上,epoll是通過內(nèi)核于用戶空間mmap同一塊內(nèi)存實(shí)現(xiàn)的。而如果你想我一樣從2.5內(nèi)核就關(guān)注epoll的話,一定不會(huì)忘記手工?mmap這一步的。
?
4.內(nèi)核微調(diào)
這一點(diǎn)其實(shí)不算epoll的優(yōu)點(diǎn)了,而是整個(gè)linux平臺(tái)的優(yōu)點(diǎn)。也許你可以懷疑linux平臺(tái),但是你無法回避linux平臺(tái)賦予你微調(diào)內(nèi)核的能力。比如,內(nèi)核TCP/IP協(xié)議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu),那么可以在運(yùn)行時(shí)期動(dòng)態(tài)調(diào)整這個(gè)內(nèi)存pool(skb_head_pool)的大小---?通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個(gè)參數(shù)(TCP完成3次握手的數(shù)據(jù)包隊(duì)列長度),也可以根據(jù)你平臺(tái)內(nèi)存大小動(dòng)態(tài)調(diào)整。更甚至在一個(gè)數(shù)據(jù)包面數(shù)目巨大但同時(shí)每個(gè)數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng)卡驅(qū)動(dòng)架構(gòu)。
?
linux下epoll如何實(shí)現(xiàn)高效處理百萬句柄的
開發(fā)高性能網(wǎng)絡(luò)程序時(shí),windows開發(fā)者們言必稱iocp,linux開發(fā)者們則言必稱epoll。大家都明白epoll是一種IO多路復(fù)用技術(shù),可以非常高效的處理數(shù)以百萬計(jì)的socket句柄,比起以前的select和poll效率高大發(fā)了。我們用起epoll來都感覺挺爽,確實(shí)快,那么,它到底為什么可以高速處理這么多并發(fā)連接呢?
?
使用起來很清晰,首先要調(diào)用epoll_create建立一個(gè)epoll對(duì)象。參數(shù)size是內(nèi)核保證能夠正確處理的最大句柄數(shù),多于這個(gè)最大數(shù)時(shí)內(nèi)核可不保證效果。
?
epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監(jiān)控,或者把?epoll正在監(jiān)控的某個(gè)socket句柄移出epoll,不再監(jiān)控它等等。
?
epoll_wait在調(diào)用時(shí),在給定的timeout時(shí)間內(nèi),當(dāng)在監(jiān)控的所有句柄中有事件發(fā)生時(shí),就返回用戶態(tài)的進(jìn)程。
?
從上面的調(diào)用方式就可以看到epoll比select/poll的優(yōu)越之處:因?yàn)楹笳呙看握{(diào)用時(shí)都要傳遞你所要監(jiān)控的所有socket給select/poll系統(tǒng)調(diào)用,這意味著需要將用戶態(tài)的socket列表copy到內(nèi)核態(tài),如果以萬計(jì)的句柄會(huì)導(dǎo)致每次都要copy幾十幾百KB的內(nèi)存到內(nèi)核態(tài),非常低效。而我們調(diào)用epoll_wait時(shí)就相當(dāng)于以往調(diào)用select/poll,但是這時(shí)卻不用傳遞socket句柄給內(nèi)核,因?yàn)閮?nèi)核已經(jīng)在epoll_ctl中拿到了要監(jiān)控的句柄列表。
?
所以,實(shí)際上在你調(diào)用epoll_create后,內(nèi)核就已經(jīng)在內(nèi)核態(tài)開始準(zhǔn)備幫你存儲(chǔ)要監(jiān)控的句柄了,每次調(diào)用epoll_ctl只是在往內(nèi)核的數(shù)據(jù)結(jié)構(gòu)里塞入新的socket句柄。
?當(dāng)一個(gè)進(jìn)程調(diào)用epoll_creaqte方法時(shí),Linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員與epoll的使用方式密切相關(guān):
/*
171 * This structure is stored inside the "private_data" member of the file
172 * structure and represents the main data structure for the eventpoll
173 * interface.
174 */
175struct eventpoll {
176 /* Protect the access to this structure */
177 spinlock_t lock;
178
179 /*
180 * This mutex is used to ensure that files are not removed
181 * while epoll is using them. This is held during the event
182 * collection loop, the file cleanup path, the epoll file exit
183 * code and the ctl operations.
184 */
185 struct mutex mtx;
186
187 /* Wait queue used by sys_epoll_wait() */
188 wait_queue_head_t wq;
189
190 /* Wait queue used by file->poll() */
191 wait_queue_head_t poll_wait;
192
193 /* List of ready file descriptors */
194 struct list_head rdllist;
195
196 /* RB tree root used to store monitored fd structs */
197 struct rb_root rbr;//紅黑樹根節(jié)點(diǎn),這棵樹存儲(chǔ)著所有添加到epoll中的事件,也就是這個(gè)epoll監(jiān)控的事件
198
199 /*
200 * This is a single linked list that chains all the "struct epitem" that
201 * happened while transferring ready events to userspace w/out
202 * holding ->lock.
203 */
204 struct epitem *ovflist;
205
206 /* wakeup_source used when ep_scan_ready_list is running */
207 struct wakeup_source *ws;
208
209 /* The user that created the eventpoll descriptor */
210 struct user_struct *user;
211
212 struct file *file;
213
214 /* used to optimize loop detection check */
215 int visited;
216 struct list_head visited_list_link;//雙向鏈表中保存著將要通過epoll_wait返回給用戶的、滿足條件的事件
217};
每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體會(huì)在內(nèi)核空間中創(chuàng)造獨(dú)立的內(nèi)存,用于存儲(chǔ)使用epoll_ctl方法向epoll對(duì)象中添加進(jìn)來的事件。這樣,重復(fù)的事件就可以通過紅黑樹而高效的識(shí)別出來。
在epoll中,對(duì)于每一個(gè)事件都會(huì)建立一個(gè)epitem結(jié)構(gòu)體:
/*
130 * Each file descriptor added to the eventpoll interface will
131 * have an entry of this type linked to the "rbr" RB tree.
132 * Avoid increasing the size of this struct, there can be many thousands
133 * of these on a server and we do not want this to take another cache line.
134 */
135struct epitem {
136 /* RB tree node used to link this structure to the eventpoll RB tree */
137 struct rb_node rbn;
138
139 /* List header used to link this structure to the eventpoll ready list */
140 struct list_head rdllink;
141
142 /*
143 * Works together "struct eventpoll"->ovflist in keeping the
144 * single linked chain of items.
145 */
146 struct epitem *next;
147
148 /* The file descriptor information this item refers to */
149 struct epoll_filefd ffd;
150
151 /* Number of active wait queue attached to poll operations */
152 int nwait;
153
154 /* List containing poll wait queues */
155 struct list_head pwqlist;
156
157 /* The "container" of this item */
158 struct eventpoll *ep;
159
160 /* List header used to link this item to the "struct file" items list */
161 struct list_head fllink;
162
163 /* wakeup_source used when EPOLLWAKEUP is set */
164 struct wakeup_source __rcu *ws;
165
166 /* The structure that describe the interested events and the source fd */
167 struct epoll_event event;
168};
此外,epoll還維護(hù)了一個(gè)雙鏈表,用戶存儲(chǔ)發(fā)生的事件。當(dāng)epoll_wait調(diào)用時(shí),僅僅觀察這個(gè)list鏈表里有沒有數(shù)據(jù)即eptime項(xiàng)即可。有數(shù)據(jù)就返回,沒有數(shù)據(jù)就sleep,等到timeout時(shí)間到后即使鏈表沒數(shù)據(jù)也返回。所以,epoll_wait非常高效。
?
而且,通常情況下即使我們要監(jiān)控百萬計(jì)的句柄,大多一次也只返回很少量的準(zhǔn)備就緒句柄而已,所以,epoll_wait僅需要從內(nèi)核態(tài)copy少量的句柄到用戶態(tài)而已,如何能不高效?!
?
那么,這個(gè)準(zhǔn)備就緒list鏈表是怎么維護(hù)的呢?當(dāng)我們執(zhí)行epoll_ctl時(shí),除了把socket放到epoll文件系統(tǒng)里file對(duì)象對(duì)應(yīng)的紅黑樹上之外,還會(huì)給內(nèi)核中斷處理程序注冊(cè)一個(gè)回調(diào)函數(shù),告訴內(nèi)核,如果這個(gè)句柄的中斷到了,就把它放到準(zhǔn)備就緒list鏈表里。所以,當(dāng)一個(gè)socket上有數(shù)據(jù)到了,內(nèi)核在把網(wǎng)卡上的數(shù)據(jù)copy到內(nèi)核中后就來把socket插入到準(zhǔn)備就緒鏈表里了。
?
如此,一顆紅黑樹,一張準(zhǔn)備就緒句柄鏈表,少量的內(nèi)核cache,就幫我們解決了大并發(fā)下的socket處理問題。執(zhí)行epoll_create時(shí),創(chuàng)建了紅黑樹和就緒鏈表,執(zhí)行epoll_ctl時(shí),如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹干上,然后向內(nèi)核注冊(cè)回調(diào)函數(shù),用于當(dāng)中斷事件來臨時(shí)向準(zhǔn)備就緒鏈表中插入數(shù)據(jù)。執(zhí)行epoll_wait時(shí)立刻返回準(zhǔn)備就緒鏈表里的數(shù)據(jù)即可。
?
epoll的使用方法
那么究竟如何來使用epoll呢?其實(shí)非常簡單。
?
通過在包含一個(gè)頭文件#include <sys/epoll.h>?以及幾個(gè)簡單的API將可以大大的提高你的網(wǎng)絡(luò)服務(wù)器的支持人數(shù)。
?
首先通過create_epoll(int maxfds)來創(chuàng)建一個(gè)epoll的句柄。這個(gè)函數(shù)會(huì)返回一個(gè)新的epoll句柄,之后的所有操作將通過這個(gè)句柄來進(jìn)行操作。在用完之后,記得用close()來關(guān)閉這個(gè)創(chuàng)建出來的epoll句柄。
?
之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網(wǎng)絡(luò)接口,看哪一個(gè)可以讀,哪一個(gè)可以寫了。基本的語法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
?
其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個(gè)epoll_event*的指針,當(dāng)epoll_wait這個(gè)函數(shù)操作成功之后,epoll_events里面將儲(chǔ)存所有的讀寫事件。max_events是當(dāng)前需要監(jiān)聽的所有socket句柄數(shù)。最后一個(gè)timeout是?epoll_wait的超時(shí),為0的時(shí)候表示馬上返回,為-1的時(shí)候表示一直等下去,直到有事件返回,為任意正整數(shù)的時(shí)候表示等這么長的時(shí)間,如果一直沒有事件,則返回。一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個(gè)線程的話,則可以用0來保證主循環(huán)的效率。
?
epoll_wait返回之后應(yīng)該是一個(gè)循環(huán),遍歷所有的事件。
?
?
幾乎所有的epoll程序都使用下面的框架:
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events.data.fd==listenfd) //有新的連接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個(gè)連接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監(jiān)聽隊(duì)列中
}
else if( events.events&EPOLLIN ) //接收到數(shù)據(jù),讀socket
{
n = read(sockfd, line, MAXLINE)) < 0 //讀
ev.data.ptr = md; //md為自定義類型,添加數(shù)據(jù)
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)發(fā)送數(shù)據(jù),異步處理的精髓
}
else if(events.events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送,寫socket
{
struct myepoll_data* md = (myepoll_data*)events.data.ptr; //取數(shù)據(jù)
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //發(fā)送數(shù)據(jù)
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識(shí)符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)
}
else
{
//其他的處理
}
}
}
epoll的程序?qū)嵗?br />
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <string.h>
#define MAXEVENTS 64
//函數(shù):
//功能:創(chuàng)建和綁定一個(gè)TCP socket
//參數(shù):端口
//返回值:創(chuàng)建的socket
static int
create_and_bind (char *port)
{
struct addrinfo hints;
struct addrinfo *result, *rp;
int s, sfd;
memset (&hints, 0, sizeof (struct addrinfo));
hints.ai_family = AF_UNSPEC; /* Return IPv4 and IPv6 choices */
hints.ai_socktype = SOCK_STREAM; /* We want a TCP socket */
hints.ai_flags = AI_PASSIVE; /* All interfaces */
s = getaddrinfo (NULL, port, &hints, &result);
if (s != 0)
{
fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));
return -1;
}
for (rp = result; rp != NULL; rp = rp->ai_next)
{
sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sfd == -1)
continue;
s = bind (sfd, rp->ai_addr, rp->ai_addrlen);
if (s == 0)
{
/* We managed to bind successfully! */
break;
}
close (sfd);
}
if (rp == NULL)
{
fprintf (stderr, "Could not bind\n");
return -1;
}
freeaddrinfo (result);
return sfd;
}
//函數(shù)
//功能:設(shè)置socket為非阻塞的
static int
make_socket_non_blocking (int sfd)
{
int flags, s;
//得到文件狀態(tài)標(biāo)志
flags = fcntl (sfd, F_GETFL, 0);
if (flags == -1)
{
perror ("fcntl");
return -1;
}
//設(shè)置文件狀態(tài)標(biāo)志
flags |= O_NONBLOCK;
s = fcntl (sfd, F_SETFL, flags);
if (s == -1)
{
perror ("fcntl");
return -1;
}
return 0;
}
//端口由參數(shù)argv[1]指定
int
main (int argc, char *argv[])
{
int sfd, s;
int efd;
struct epoll_event event;
struct epoll_event *events;
if (argc != 2)
{
fprintf (stderr, "Usage: %s [port]\n", argv[0]);
exit (EXIT_FAILURE);
}
sfd = create_and_bind (argv[1]);
if (sfd == -1)
abort ();
s = make_socket_non_blocking (sfd);
if (s == -1)
abort ();
s = listen (sfd, SOMAXCONN);
if (s == -1)
{
perror ("listen");
abort ();
}
//除了參數(shù)size被忽略外,此函數(shù)和epoll_create完全相同
efd = epoll_create1 (0);
if (efd == -1)
{
perror ("epoll_create");
abort ();
}
event.data.fd = sfd;
event.events = EPOLLIN | EPOLLET;//讀入,邊緣觸發(fā)方式
s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);
if (s == -1)
{
perror ("epoll_ctl");
abort ();
}
/* Buffer where events are returned */
events = calloc (MAXEVENTS, sizeof event);
/* The event loop */
while (1)
{
int n, i;
n = epoll_wait (efd, events, MAXEVENTS, -1);
for (i = 0; i < n; i++)
{
if ((events.events & EPOLLERR) ||
(events.events & EPOLLHUP) ||
(!(events.events & EPOLLIN)))
{
/* An error has occured on this fd, or the socket is not
ready for reading (why were we notified then?) */
fprintf (stderr, "epoll error\n");
close (events.data.fd);
continue;
}
else if (sfd == events.data.fd)
{
/* We have a notification on the listening socket, which
means one or more incoming connections. */
while (1)
{
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
in_len = sizeof in_addr;
infd = accept (sfd, &in_addr, &in_len);
if (infd == -1)
{
if ((errno == EAGAIN) ||
(errno == EWOULDBLOCK))
{
/* We have processed all incoming
connections. */
break;
}
else
{
perror ("accept");
break;
}
}
//將地址轉(zhuǎn)化為主機(jī)名或者服務(wù)名
s = getnameinfo (&in_addr, in_len,
hbuf, sizeof hbuf,
sbuf, sizeof sbuf,
NI_NUMERICHOST | NI_NUMERICSERV);//flag參數(shù):以數(shù)字名返回
//主機(jī)地址和服務(wù)地址
if (s == 0)
{
printf("Accepted connection on descriptor %d "
"(host=%s, port=%s)\n", infd, hbuf, sbuf);
}
/* Make the incoming socket non-blocking and add it to the
list of fds to monitor. */
s = make_socket_non_blocking (infd);
if (s == -1)
abort ();
event.data.fd = infd;
event.events = EPOLLIN | EPOLLET;
s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);
if (s == -1)
{
perror ("epoll_ctl");
abort ();
}
}
continue;
}
else
{
/* We have data on the fd waiting to be read. Read and
display it. We must read whatever data is available
completely, as we are running in edge-triggered mode
and won't get a notification again for the same
data. */
int done = 0;
while (1)
{
ssize_t count;
char buf[512];
count = read (events.data.fd, buf, sizeof(buf));
if (count == -1)
{
/* If errno == EAGAIN, that means we have read all
data. So go back to the main loop. */
if (errno != EAGAIN)
{
perror ("read");
done = 1;
}
break;
}
else if (count == 0)
{
/* End of file. The remote has closed the
connection. */
done = 1;
break;
}
/* Write the buffer to standard output */
s = write (1, buf, count);
if (s == -1)
{
perror ("write");
abort ();
}
}
if (done)
{
printf ("Closed connection on descriptor %d\n",
events.data.fd);
/* Closing the descriptor will make epoll remove it
from the set of descriptors which are monitored. */
close (events.data.fd);
}
}
}
}
free (events);
close (sfd);
return EXIT_SUCCESS;
}
運(yùn)行方式:
在一個(gè)終端運(yùn)行此程序:epoll.out PORT
另一個(gè)終端:telnet ?127.0.0.1 PORT
截圖:
參考學(xué)習(xí)資料:http://www.makeru.com.cn/course/details/2478/?s=60220Q群556042177
|
|