epoll是Linux內(nèi)核為處理大批量文件描述符而作了改進(jìn)的poll,,是Linux下多路復(fù)用IO接口select/poll的增強(qiáng)版本,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率,。另一點(diǎn)原因就是獲取事件的時(shí)候,,它無須遍歷整個(gè)被偵聽的描述符集,只要遍歷那些被內(nèi)核IO事件異步喚醒而加入Ready隊(duì)列的描述符集合就行了,。
epoll除了提供select/poll那種IO事件的水平觸發(fā)(Level Triggered)外,,還提供了邊緣觸發(fā)(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態(tài),,減少epoll_wait/epoll_pwait的調(diào)用,,提高應(yīng)用程序效率。
一、初識(shí)epoll 1.1工作方式 LT(level triggered)
水平觸發(fā),,缺省方式,,同時(shí)支持block和no-block socket,在這種做法中,,內(nèi)核告訴我們一個(gè)文件描述符是否被就緒了,,如果就緒了,你就可以對(duì)這個(gè)就緒的fd進(jìn)行IO操作,。如果你不作任何操作,,內(nèi)核還是會(huì)繼續(xù)通知你的,所以,,這種模式編程出錯(cuò)的可能性較小,。傳統(tǒng)的select\poll都是這種模型的代表。
ET(edge-triggered)
邊沿觸發(fā),,高速工作方式,,只支持no-block socket。在這種模式下,,當(dāng)描述符從未就緒變?yōu)榫途w狀態(tài)時(shí),,內(nèi)核通過epoll告訴你。然后它會(huì)假設(shè)你知道文件描述符已經(jīng)就緒,,并且不會(huì)再為那個(gè)描述符發(fā)送更多的就緒通知,,直到你做了某些操作導(dǎo)致那個(gè)文件描述符不再為就緒狀態(tài)了(比如:你在發(fā)送、接受或者接受請(qǐng)求,,或者發(fā)送接受的數(shù)據(jù)少于一定量時(shí)導(dǎo)致了一個(gè)EWOULDBLOCK錯(cuò)誤),。但是請(qǐng)注意,如果一直不對(duì)這個(gè)fs做IO操作(從而導(dǎo)致它再次變成未就緒狀態(tài)),,內(nèi)核不會(huì)發(fā)送更多的通知,。
區(qū)別:LT事件不會(huì)丟棄,而是只要讀buffer里面有數(shù)據(jù)可以讓用戶讀取,,則不斷的通知你,。而ET則只在事件發(fā)生之時(shí)通知。
1.2epoll的接口 int epoll_create(int size)
創(chuàng)建一個(gè)epoll句柄,,size用來告訴內(nèi)核,,這個(gè)句柄監(jiān)聽的數(shù)目一共有多大,當(dāng)創(chuàng)建好句柄以后,,他就會(huì)占用一個(gè)fd值,,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的,,所以在使用完epoll后,,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。
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_crete()的返回值,,第二個(gè)參數(shù)表示動(dòng)作,用三個(gè)宏來表示:LL_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)如下:
typedef union epoll_data{ void *ptr; int fd; __uint32_t u32; __uint64_t u64; }epoll_data_t; 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ì)列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產(chǎn)生,類似于select()調(diào)用,。參數(shù)events用來從內(nèi)核得到事件的集合,,maxevents告之內(nèi)核這個(gè)events有多大,這個(gè) maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size,,參數(shù)timeout是超時(shí)時(shí)間(毫秒,,0會(huì)立即返回,-1將不確定,,也有說法說是永久阻塞),。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時(shí),。
關(guān)于ET,、LT兩種工作模式:可以得出這樣的結(jié)論
ET 模式僅當(dāng)狀態(tài)發(fā)生變化的時(shí)候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說,如果要采用ET模式,需要一直 read/write直到出錯(cuò)為止,很多人反映為什么采用ET模式只接收了一部分?jǐn)?shù)據(jù)就再也得不到通知了,大多因?yàn)檫@樣;而LT模式是只要有數(shù)據(jù)沒有處理 就會(huì)一直通知下去的。
二,、epoll原理 為什么需要epoll,?
epoll是Linux操作系統(tǒng)提供的一種事件驅(qū)動(dòng)的I/O模型,用于高效地處理大量并發(fā)連接的網(wǎng)絡(luò)編程,。它相比于傳統(tǒng)的select和poll方法,,具有更高的性能和擴(kuò)展性。使用epoll可以實(shí)現(xiàn)以下幾個(gè)優(yōu)勢(shì):
高效處理大量并發(fā)連接:epoll采用了事件驅(qū)動(dòng)的方式,只有當(dāng)有可讀或可寫事件發(fā)生時(shí)才會(huì)通知應(yīng)用程序,,避免了遍歷所有文件描述符的開銷,。
內(nèi)核與用戶空間數(shù)據(jù)拷貝少:使用epoll時(shí),內(nèi)核將就緒的文件描述符直接填充到用戶空間的事件數(shù)組中,,減少了內(nèi)核與用戶空間之間數(shù)據(jù)拷貝次數(shù),。
支持邊緣觸發(fā)(Edge Triggered)模式:邊緣觸發(fā)模式下,僅在狀態(tài)變化時(shí)才通知應(yīng)用程序,。這意味著每次通知只包含最新狀態(tài)的文件描述符信息,,可以有效避免低效循環(huán)檢查。
支持水平觸發(fā)(Level Triggered)模式:水平觸發(fā)模式下,,在就緒期間不斷地進(jìn)行通知,,直到應(yīng)用程序處理完該文件描述符。
select與poll的缺陷,?
select
和 poll
都是Unix系統(tǒng)中用來監(jiān)視一組文件描述符的變化的系統(tǒng)調(diào)用,。它們可以監(jiān)視文件描述符的三種變化:可讀性、可寫性和異常條件,。select
和 poll
的主要缺陷如下:
文件描述符數(shù)量限制: select 和 poll 都有一個(gè)限制,,就是它們只能監(jiān)視少于1024個(gè)文件描述符的變化。這對(duì)于現(xiàn)代的網(wǎng)絡(luò)編程來說是不夠的,,因?yàn)橐粋€(gè)進(jìn)程往往需要監(jiān)視成千上萬的連接,。
效率問題: 雖然 select 和 poll 可以監(jiān)視多個(gè)文件描述符,但是它們?cè)诿看握{(diào)用的時(shí)候都需要傳遞所有要監(jiān)視的文件描述符集合,,這會(huì)導(dǎo)致效率的降低,。
信息不足: select 和 poll 返回的只是哪些文件描述符已經(jīng)準(zhǔn)備好了,但是它們并不告訴你具體是哪一個(gè),。這就需要對(duì)所有要監(jiān)視的文件描述符進(jìn)行遍歷,,直到找到準(zhǔn)備好的文件描述符為止。
信號(hào)中斷: select 和 poll 調(diào)用可以被信號(hào)中斷,,這可能會(huì)導(dǎo)致調(diào)用失敗,。
為了解決這些問題,現(xiàn)代操作系統(tǒng)中引入了新的系統(tǒng)調(diào)用 epoll 來替代 select 和 poll,。epoll 沒有文件描述符的限制,,它可以監(jiān)視大量的文件描述符,并且可以實(shí)現(xiàn)即開即用,,無需傳遞所有文件描述符集合,。此外,epoll 可以直接告訴你哪些文件描述符已經(jīng)準(zhǔn)備好,,這大大提高了處理效率,。
2.1epoll操作 epoll 在 linux 內(nèi)核中申請(qǐng)了一個(gè)簡(jiǎn)易的文件系統(tǒng),,把原先的一個(gè) select 或者 poll 調(diào)用分為了三個(gè)部分:調(diào)用 epoll_create 建立一個(gè) epoll 對(duì)象(在 epoll 文件系統(tǒng)中給這個(gè)句柄分配資源)、調(diào)用 epoll_ctl 向 epoll 對(duì)象中添加連接的套接字,、調(diào)用 epoll_wait 收集發(fā)生事件的連接,。這樣只需要在進(jìn)程啟動(dòng)的時(shí)候建立一個(gè) epoll 對(duì)象,并在需要的時(shí)候向它添加或者刪除連接就可以了,,因此,,在實(shí)際收集的時(shí)候,epoll_wait 的效率會(huì)非常高,,因?yàn)檎{(diào)用的時(shí)候只是傳遞了發(fā)生 IO 事件的連接,。
epoll 實(shí)現(xiàn)
我們以 linux 內(nèi)核 2.6 為例,,說明一下 epoll 是如何高效的處理事件的,,當(dāng)某一個(gè)進(jìn)程調(diào)用 epoll_create 方法的時(shí)候,Linux 內(nèi)核會(huì)創(chuàng)建一個(gè) eventpoll 結(jié)構(gòu)體,,這個(gè)結(jié)構(gòu)體中有兩個(gè)重要的成員,。
第一個(gè)是 rb_root rbr,這是紅黑樹的根節(jié)點(diǎn),,存儲(chǔ)著所有添加到 epoll 中的事件,,也就是這個(gè) epoll 監(jiān)控的事件。 第二個(gè)是 list_head rdllist 這是一個(gè)雙向鏈表,,保存著將要通過 epoll_wait 返回給用戶的,、滿足條件的事件。
每一個(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)來的事件。這些事件都會(huì)掛到 rbr 紅黑樹中,,這樣就能夠高效的識(shí)別重復(fù)添加的節(jié)點(diǎn),。
所有添加到 epoll 中的事件都會(huì)與設(shè)備(如網(wǎng)卡等)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說,,相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這里的方法,。這個(gè)回調(diào)方法在內(nèi)核中叫做 ep_poll_callback,它把這樣的事件放到 rdllist 雙向鏈表中,。在 epoll 中,,對(duì)于每一個(gè)事件都會(huì)建立一個(gè) epitem 結(jié)構(gòu)體。
當(dāng)調(diào)用 epoll_wait 檢查是否有發(fā)生事件的連接時(shí),,只需要檢查 eventpoll 對(duì)象中的 rdllist 雙向鏈表中是否有 epitem 元素,,如果 rdllist 鏈表不為空,則把這里的事件復(fù)制到用戶態(tài)內(nèi)存中的同時(shí),,將事件數(shù)量返回給用戶,。通過這種方法,,epoll_wait 的效率非常高。epoll-ctl 在向 epoll 對(duì)象中添加,、修改,、刪除事件時(shí),從 rbr 紅黑樹中查找事件也非???。這樣,epoll 就能夠輕易的處理百萬級(jí)的并發(fā)連接,。
epoll工作模式
epoll 有兩種工作模式,,LT(水平觸發(fā))模式與 ET(邊緣觸發(fā))模式。默認(rèn)情況下,,epoll 采用 LT 模式工作,。兩個(gè)的區(qū)別是:
Level_triggered(水平觸發(fā)):當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時(shí),epoll_wait()會(huì)通知處理程序去讀寫,。如果這次沒有把數(shù)據(jù)一次性全部讀寫完(如讀寫緩沖區(qū)太小),,那么下次調(diào)用 epoll_wait() 時(shí),它還會(huì)通知你在上沒讀寫完的文件描述符上繼續(xù)讀寫 ,,當(dāng)然如果你一直不去讀寫,,它會(huì)一直通知你。如果系統(tǒng)中有大量你不需要讀寫的就緒文件描述符,,而它們每次都會(huì)返回,,這樣會(huì)大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率。
Edge_triggered(邊緣觸發(fā)):當(dāng)被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時(shí),,epoll_wait() 會(huì)通知處理程序去讀寫,。如果這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),那么下次調(diào)用 epoll_wait()時(shí),,它不會(huì)通知你,,也就是它只會(huì)通知你一次 ,直到該文件描述符上出現(xiàn)第二次可讀寫事件才會(huì)通知你,。這種模式比水平觸發(fā)效率高,,系統(tǒng)不會(huì)充斥大量你不關(guān)心的就緒文件描述符,。
當(dāng)然,在 LT 模式下開發(fā)基于 epoll 的應(yīng)用要簡(jiǎn)單一些,不太容易出錯(cuò),,而在 ET 模式下事件發(fā)生時(shí),,如果沒有徹底地將緩沖區(qū)的數(shù)據(jù)處理完,,則會(huì)導(dǎo)致緩沖區(qū)的用戶請(qǐng)求得不到響應(yīng),。注意,默認(rèn)情況下 Nginx 采用 ET 模式使用 epoll 的,。
2.2 I/O 多路復(fù)用 (1)阻塞OR非阻塞
我們知道,,對(duì)于 linux 來說,,I/O 設(shè)備為特殊的文件,讀寫和文件是差不多的,,但是 I/O 設(shè)備因?yàn)樽x寫與內(nèi)存讀寫相比,,速度差距非常大。與 cpu 讀寫速度更是沒法比,,所以相比于對(duì)內(nèi)存的讀寫,,I/O 操作總是拖后腿的那個(gè)。網(wǎng)絡(luò) I/O 更是如此,,我們很多時(shí)候不知道網(wǎng)絡(luò) I/O 什么時(shí)候到來,,就好比我們點(diǎn)了一份外賣,不知道外賣小哥們什么時(shí)候送過來,,這個(gè)時(shí)候有兩個(gè)處理辦法:
第一個(gè)是我們可以先去睡覺,,外賣小哥送到樓下了自然會(huì)給我們打電話,這個(gè)時(shí)候我們?cè)谛褋砣⊥赓u就可以了,。 第二個(gè)是我們可以每隔一段時(shí)間就給外賣小哥打個(gè)電話,,這樣就能實(shí)時(shí)掌握外賣的動(dòng)態(tài)信息了,。
第一種方式對(duì)應(yīng)的就是阻塞的 I/O 處理方式,,進(jìn)程在進(jìn)行 I/O 操作的時(shí)候,進(jìn)入睡眠,,如果有 I/O 時(shí)間到達(dá),,就喚醒這個(gè)進(jìn)程。第二種方式對(duì)應(yīng)的是非阻塞輪詢的方式,,進(jìn)程在進(jìn)行 I/O 操作后,,每隔一段時(shí)間向內(nèi)核詢問是否有 I/O 事件到達(dá),如果有就立刻處理,。
阻塞的原理
工作隊(duì)列
阻塞是進(jìn)程調(diào)度的關(guān)鍵一環(huán),,指的是進(jìn)程在等待某事件(如接收到網(wǎng)絡(luò)數(shù)據(jù))發(fā)生之前的等待狀態(tài),recv,、select和epoll都是阻塞方法,,以簡(jiǎn)單網(wǎng)絡(luò)編程為例
下圖中的計(jì)算機(jī)中運(yùn)行著A、B,、C三個(gè)進(jìn)程,,其中進(jìn)程A執(zhí)行著上述基礎(chǔ)網(wǎng)絡(luò)程序,一開始,,這3個(gè)進(jìn)程都被操作系統(tǒng)的工作隊(duì)列所引用,,處于運(yùn)行狀態(tài),會(huì)分時(shí)執(zhí)行
等待隊(duì)列
當(dāng)進(jìn)程A執(zhí)行到創(chuàng)建socket的語句時(shí),,操作系統(tǒng)會(huì)創(chuàng)建一個(gè)由文件系統(tǒng)管理的socket對(duì)象(如下圖),。這個(gè)socket對(duì)象包含了發(fā)送緩沖區(qū),、接收緩沖區(qū)、等待隊(duì)列等成員,。等待隊(duì)列是個(gè)非常重要的結(jié)構(gòu),,它指向所有需要等待該socket事件的進(jìn)程。
當(dāng)程序執(zhí)行到recv時(shí),,操作系統(tǒng)會(huì)將進(jìn)程A從工作隊(duì)列移動(dòng)到該socket的等待隊(duì)列中(如下圖),。由于工作隊(duì)列只剩下了進(jìn)程B和C,依據(jù)進(jìn)程調(diào)度,,cpu會(huì)輪流執(zhí)行這兩個(gè)進(jìn)程的程序,,不會(huì)執(zhí)行進(jìn)程A的程序。所以進(jìn)程A被阻塞,,不會(huì)往下執(zhí)行代碼,,也不會(huì)占用cpu資源 。
ps:操作系統(tǒng)添加等待隊(duì)列只是添加了對(duì)這個(gè)“等待中”進(jìn)程的引用,,以便在接收到數(shù)據(jù)時(shí)獲取進(jìn)程對(duì)象,、將其喚醒,而非直接將進(jìn)程管理納入自己之下,。上圖為了方便說明,,直接將進(jìn)程掛到等待隊(duì)列之下。
喚醒進(jìn)程
當(dāng)socket接收到數(shù)據(jù)后,,操作系統(tǒng)將該socket等待隊(duì)列上的進(jìn)程重新放回到工作隊(duì)列,,該進(jìn)程變成運(yùn)行狀態(tài),繼續(xù)執(zhí)行代碼,。也由于socket的接收緩沖區(qū)已經(jīng)有了數(shù)據(jù),,recv可以返回接收到的數(shù)據(jù)。
(2)線程池OR輪詢
在現(xiàn)實(shí)中,,我們當(dāng)然選擇第一種方式,,但是在計(jì)算機(jī)中,情況就要復(fù)雜一些,。我們知道,,在 linux 中,不管是線程還是進(jìn)程都會(huì)占用一定的資源,,也就是說,,系統(tǒng)總的線程和進(jìn)程數(shù)是一定的。如果有許多的線程或者進(jìn)程被掛起,,無疑是白白消耗了系統(tǒng)的資源,。而且,線程或者進(jìn)程的切換也是需要一定的成本的,,需要上下文切換,,如果頻繁的進(jìn)行上下文切換,,系統(tǒng)會(huì)損失很大的性能。一個(gè)網(wǎng)絡(luò)服務(wù)器經(jīng)常需要連接成千上萬個(gè)客戶端,,而它能創(chuàng)建的線程可能之后幾百個(gè),,線程耗光就不能對(duì)外提供服務(wù)了。這些都是我們?cè)谶x擇 I/O 機(jī)制的時(shí)候需要考慮的,。這種阻塞的 I/O 模式下,,一個(gè)線程只能處理一個(gè)流的 I/O 事件,這是問題的根源,。
這個(gè)時(shí)候我們首先想到的是采用線程池的方式限制同時(shí)訪問的線程數(shù),,這樣就能夠解決線程不足的問題了。但是這又會(huì)有第二個(gè)問題了,,多余的任務(wù)會(huì)通過隊(duì)列的方式存儲(chǔ)在內(nèi)存只能夠,,這樣很容易在客戶端過多的情況下出現(xiàn)內(nèi)存不足的情況。
還有一種方式是采用輪詢的方式,,我們只要不停的把所有流從頭到尾問一遍,,又從頭開始。這樣就可以處理多個(gè)流了,。
(3)代理
采用輪詢的方式雖然能夠處理多個(gè) I/O 事件,,但是也有一個(gè)明顯的缺點(diǎn),那就是會(huì)導(dǎo)致 CPU 空轉(zhuǎn),。試想一下,,如果所有的流中都沒有數(shù)據(jù),那么 CPU 時(shí)間就被白白的浪費(fèi)了,。
為了避免CPU空轉(zhuǎn),可以引進(jìn)了一個(gè)代理,。這個(gè)代理比較厲害,,可以同時(shí)觀察許多流的I/O事件,在空閑的時(shí)候,,會(huì)把當(dāng)前線程阻塞掉,,當(dāng)有一個(gè)或多個(gè)流有I/O事件時(shí),就從阻塞態(tài)中醒來,,于是我們的程序就會(huì)輪詢一遍所有的流,,這就是 select 與 poll 所做的事情,可見,,采用 I/O 復(fù)用極大的提高了系統(tǒng)的效率,。
2.3內(nèi)核接收網(wǎng)絡(luò)數(shù)據(jù)全過程 如下圖所示,進(jìn)程在recv阻塞期間,,計(jì)算機(jī)收到了對(duì)端傳送的數(shù)據(jù)(步驟①),。數(shù)據(jù)經(jīng)由網(wǎng)卡傳送到內(nèi)存(步驟②),,然后網(wǎng)卡通過中斷信號(hào)通知CPU有數(shù)據(jù)到達(dá),CPU執(zhí)行中斷程序(步驟③),。此處的中斷程序主要有兩項(xiàng)功能,,先將網(wǎng)絡(luò)數(shù)據(jù)寫入到對(duì)應(yīng)socket的接收緩沖區(qū)里面(步驟④),再喚醒進(jìn)程A(步驟⑤),,重新將進(jìn)程A放入工作隊(duì)列中,。
喚醒線程的過程如下圖所示:
2.4epoll原理(源碼) (1)創(chuàng)建epoll對(duì)象
如下圖所示,當(dāng)某個(gè)進(jìn)程調(diào)用epoll_create方法時(shí),,內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll對(duì)象(也就是程序中epfd所代表的對(duì)象),。eventpoll對(duì)象也是文件系統(tǒng)中的一員,和socket一樣,,它也會(huì)有等待隊(duì)列,。
最后 epoll_create 生成的文件描述符如下圖所示:
/* * 此結(jié)構(gòu)體存儲(chǔ)在file->private_data中 */ struct eventpoll { // 自旋鎖,在kernel內(nèi)部用自旋鎖加鎖,,就可以同時(shí)多線(進(jìn))程對(duì)此結(jié)構(gòu)體進(jìn)行操作 // 主要是保護(hù)ready_list spinlock_t lock; // 這個(gè)互斥鎖是為了保證在eventloop使用對(duì)應(yīng)的文件描述符的時(shí)候,,文件描述符不會(huì)被移除掉 struct mutex mtx; // epoll_wait使用的等待隊(duì)列,和進(jìn)程喚醒有關(guān) wait_queue_head_t wq; // file->poll使用的等待隊(duì)列,,和進(jìn)程喚醒有關(guān) wait_queue_head_t poll_wait; // 就緒的描述符隊(duì)列 struct list_head rdllist; // 通過紅黑樹來組織當(dāng)前epoll關(guān)注的文件描述符 struct rb_root rbr; // 在向用戶空間傳輸就緒事件的時(shí)候,,將同時(shí)發(fā)生事件的文件描述符鏈入到這個(gè)鏈表里面 struct epitem *ovflist; // 對(duì)應(yīng)的user struct user_struct *user; // 對(duì)應(yīng)的文件描述符 struct file *file; // 下面兩個(gè)是用于環(huán)路檢測(cè)的優(yōu)化 int visited; struct list_head visited_list_link; }; 本文講述的是 kernel 是如何將就緒事件傳遞給 epoll 并喚醒對(duì)應(yīng)進(jìn)程上,因此在這里主要聚焦于 (wait_queue_head_t wq) 等成員,。
就緒列表的數(shù)據(jù)結(jié)構(gòu)
就緒列表引用著就緒的socket,,所以它應(yīng)能夠快速的插入數(shù)據(jù)。
程序可能隨時(shí)調(diào)用epoll_ctl添加監(jiān)視socket,,也可能隨時(shí)刪除,。當(dāng)刪除時(shí),若該socket已經(jīng)存放在就緒列表中,,它也應(yīng)該被移除,。
所以就緒列表應(yīng)是一種能夠快速插入和刪除的數(shù)據(jù)結(jié)構(gòu)。雙向鏈表就是這樣一種數(shù)據(jù)結(jié)構(gòu),,epoll使用雙向鏈表來實(shí)現(xiàn)就緒隊(duì)列(對(duì)應(yīng)上圖的rdllist),。
索引結(jié)構(gòu)
既然epoll將“維護(hù)監(jiān)視隊(duì)列”和“進(jìn)程阻塞”分離,也意味著需要有個(gè)數(shù)據(jù)結(jié)構(gòu)來保存監(jiān)視的socket,。至少要方便的添加和移除,,還要便于搜索,以避免重復(fù)添加,。紅黑樹是一種自平衡二叉查找樹,,搜索、插入和刪除時(shí)間復(fù)雜度都是O(log(N)),效率較好,。epoll使用了紅黑樹作為索引結(jié)構(gòu)(對(duì)應(yīng)上圖的rbr)
ps:因?yàn)椴僮飨到y(tǒng)要兼顧多種功能,,以及由更多需要保存的數(shù)據(jù),rdlist并非直接引用socket,,而是通過epitem間接引用,,紅黑樹的節(jié)點(diǎn)也是epitem對(duì)象。同樣,,文件系統(tǒng)也并非直接引用著socket,。為方便理解,本文中省略了某些間接結(jié)構(gòu),。
維護(hù)監(jiān)視列表
創(chuàng)建epoll對(duì)象后,,可以用epoll_ctl添加或刪除所要監(jiān)聽的socket。以添加socket為例,,如下圖,,如果通過epoll_ctl添加sock1、sock2和sock3的監(jiān)視,,內(nèi)核會(huì)將eventpoll添加到這三個(gè)socket的等待隊(duì)列中,。
添加所要監(jiān)聽的socket 當(dāng)socket收到數(shù)據(jù)后,中斷程序會(huì)操作eventpoll對(duì)象,,而不是直接操作進(jìn)程,。
接收數(shù)據(jù)
當(dāng)socket收到數(shù)據(jù)后,中斷程序會(huì)給eventpoll的“就緒列表”添加socket引用,。如下圖展示的是sock2和sock3收到數(shù)據(jù)后,,中斷程序讓rdlist引用這兩個(gè)socket。
給就緒列表添加引用 eventpoll對(duì)象相當(dāng)于是socket和進(jìn)程之間的中介,,socket的數(shù)據(jù)接收并不直接影響進(jìn)程,,而是通過改變eventpoll的就緒列表來改變進(jìn)程狀態(tài)。
當(dāng)程序執(zhí)行到epoll_wait時(shí),,如果rdlist已經(jīng)引用了socket,,那么epoll_wait直接返回,如果rdlist為空,,阻塞進(jìn)程。
阻塞和喚醒進(jìn)程
假設(shè)計(jì)算機(jī)中正在運(yùn)行進(jìn)程A和進(jìn)程B,,在某時(shí)刻進(jìn)程A運(yùn)行到了epoll_wait語句,。如下圖所示,內(nèi)核會(huì)將進(jìn)程A放入eventpoll的等待隊(duì)列中,,阻塞進(jìn)程,。
epoll_wait阻塞進(jìn)程 當(dāng)socket接收到數(shù)據(jù),中斷程序一方面修改rdlist,另一方面喚醒eventpoll等待隊(duì)列中的進(jìn)程,,進(jìn)程A再次進(jìn)入運(yùn)行狀態(tài)(如下圖),。也因?yàn)閞dlist的存在,進(jìn)程A可以知道哪些socket發(fā)生了變化,。
epoll喚醒進(jìn)程 值得注意的是,,我們?cè)?close 對(duì)應(yīng)的文件描述符的時(shí)候,會(huì)自動(dòng)調(diào)用 eventpoll_release 將對(duì)應(yīng)的 file 從其關(guān)聯(lián)的 epoll_fd 中刪除,。 close fd |->filp_close |->fput |->__fput |->eventpoll_release |->ep_remove 所以我們?cè)陉P(guān)閉對(duì)應(yīng)的文件描述符后,,并不需要通過 epoll_ctl_del 來刪掉對(duì)應(yīng) epoll 中相應(yīng)的描述符。
三,、如何使用epoll 通過在包含一個(gè)頭文件#include
應(yīng)用舉例
下面,,我引用google code中別人寫的一個(gè)簡(jiǎn)單程序來進(jìn)行說明。svn路徑:http://sechat./svn/trunk/
該程序一個(gè)簡(jiǎn)單的聊天室程序,,用Linux C++寫的,,服務(wù)器主要是用epoll模型實(shí)現(xiàn),支持高并發(fā),,我測(cè)試在有10000個(gè)客戶端連接服務(wù)器的時(shí)候,,server處理時(shí)間不到1秒,當(dāng)然客戶端只是與服務(wù)器連接之后,,接受服務(wù)器的歡迎消息而已,,并沒有做其他的通信。雖然程序比較簡(jiǎn)單,,但是在我們考慮服務(wù)器高并發(fā)時(shí)也提供了一個(gè)思路,。在這個(gè)程序中,我已經(jīng)把所有的調(diào)試信息和一些與epoll無關(guān)的信息干掉,,并添加必要的注釋,,應(yīng)該很容易理解。
程序共包含2個(gè)頭文件和3個(gè)cpp文件,。其中3個(gè)cpp文件中,,每一個(gè)cpp文件都是一個(gè)應(yīng)用程序,server.cpp:服務(wù)器程序,,client.cpp:?jiǎn)蝹€(gè)客戶端程序,,tester.cpp:模擬高并發(fā),開啟10000個(gè)客戶端去連服務(wù)器,。
utils.h頭文件,,就包含一個(gè)設(shè)置socket為不阻塞函數(shù),如下:
int setnonblocking(int sockfd) { CHK(fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK)); return 0; }
local.h頭文件,,一些常量的定義和函數(shù)的聲明,,如下:
#define BUF_SIZE 1024 //默認(rèn)緩沖區(qū) #define SERVER_PORT 44444 //監(jiān)聽端口 #define SERVER_HOST "192.168.34.15" //服務(wù)器IP地址 #define EPOLL_RUN_TIMEOUT -1 //epoll的超時(shí)時(shí)間 #define EPOLL_SIZE 10000 //epoll監(jiān)聽的客戶端的最大數(shù)目 #define STR_WELCOME "Welcome to seChat! You ID is: Client #%d" #define STR_MESSAGE "Client #%d>> %s" #define STR_NOONE_CONNECTED "Noone connected to server except you!" #define CMD_EXIT "EXIT" //兩個(gè)有用的宏定義:檢查和賦值并且檢測(cè) #define CHK(eval) if(eval < 0){perror("eval"); exit(-1);} #define CHK2(res, eval) if((res = eval) < 0){perror("eval"); exit(-1);} //================================================================================================ //函數(shù)名:setnonblocking //函數(shù)描述:設(shè)置socket為不阻塞 //輸入:[in] sockfd socket標(biāo)示符 //輸出:無 //返回:0 //================================================================================================ int setnonblocking(int sockfd); //================================================================================================ //函數(shù)名:handle_message //函數(shù)描述:處理每個(gè)客戶端socket //輸入:[in] new_fd socket標(biāo)示符 //輸出:無 //返回:返回從客戶端接受的數(shù)據(jù)的長(zhǎng)度 //================================================================================================ int handle_message(int new_fd);
server.cpp文件,epoll模型就在這里實(shí)現(xiàn),如下:
#include "local.h" #include "utils.h" using namespace std; // 存放客戶端socket描述符的list list<int> clients_list; int main(int argc, char *argv[]) { int listener; //監(jiān)聽socket struct sockaddr_in addr, their_addr; addr.sin_family = PF_INET; addr.sin_port = htons(SERVER_PORT); addr.sin_addr.s_addr = inet_addr(SERVER_HOST); socklen_t socklen; socklen = sizeof(struct sockaddr_in); static struct epoll_event ev, events[EPOLL_SIZE]; ev.events = EPOLLIN | EPOLLET; //對(duì)讀感興趣,,邊沿觸發(fā) char message[BUF_SIZE]; int epfd; //epoll描述符 clock_t tStart; //計(jì)算程序運(yùn)行時(shí)間 int client, res, epoll_events_count; CHK2(listener, socket(PF_INET, SOCK_STREAM, 0)); //初始化監(jiān)聽socket setnonblocking(listener); //設(shè)置監(jiān)聽socket為不阻塞 CHK(bind(listener, (struct sockaddr *)&addr, sizeof(addr))); //綁定監(jiān)聽socket CHK(listen(listener, 1)); //設(shè)置監(jiān)聽 CHK2(epfd,epoll_create(EPOLL_SIZE)); //創(chuàng)建一個(gè)epoll描述符,,并將監(jiān)聽socket加入epoll ev.data.fd = listener; CHK(epoll_ctl(epfd, EPOLL_CTL_ADD, listener, &ev)); while(1) { CHK2(epoll_events_count,epoll_wait(epfd, events, EPOLL_SIZE, EPOLL_RUN_TIMEOUT)); tStart = clock(); for(int i = 0; i < epoll_events_count ; i++) { if(events[i].data.fd == listener) //新的連接到來,將連接添加到epoll中,,并發(fā)送歡迎消息 { CHK2(client,accept(listener, (struct sockaddr *) &their_addr, &socklen)); setnonblocking(client); ev.data.fd = client; CHK(epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev)); clients_list.push_back(client); // 添加新的客戶端到list bzero(message, BUF_SIZE); res = sprintf(message, STR_WELCOME, client); CHK2(res, send(client, message, BUF_SIZE, 0)); }else { CHK2(res,handle_message(events[i].data.fd)); //注意:這里并沒有調(diào)用epoll_ctl重新設(shè)置socket的事件類型,,但還是可以繼續(xù)收到客戶端發(fā)送過來的信息 } } printf("Statistics: %d events handled at: %.2f second(s)\n", epoll_events_count, (double)(clock() - tStart)/CLOCKS_PER_SEC); } close(listener); close(epfd); return 0; } int handle_message(int client) { char buf[BUF_SIZE], message[BUF_SIZE]; bzero(buf, BUF_SIZE); bzero(message, BUF_SIZE); int len; CHK2(len,recv(client, buf, BUF_SIZE, 0)); //接受客戶端信息 if(len == 0) //客戶端關(guān)閉或出錯(cuò),關(guān)閉socket,,并從list移除socket { CHK(close(client)); clients_list.remove(client); } else //向客戶端發(fā)送信息 { if(clients_list.size() == 1) { CHK(send(client, STR_NOONE_CONNECTED, strlen(STR_NOONE_CONNECTED), 0)); return len; } sprintf(message, STR_MESSAGE, client, buf); list<int>::iterator it; for(it = clients_list.begin(); it != clients_list.end(); it++) { if(*it != client) { CHK(send(*it, message, BUF_SIZE, 0)); } } } return len; }
tester.cpp文件,,模擬服務(wù)器的高并發(fā),開啟10000個(gè)客戶端去連接服務(wù)器,,如下:
#include "local.h" #include "utils.h" using namespace std; char message[BUF_SIZE]; //接受服務(wù)器信息 list<int> list_of_clients; //存放所有客戶端 int res; clock_t tStart; int main(int argc, char *argv[]) { int sock; struct sockaddr_in addr; addr.sin_family = PF_INET; addr.sin_port = htons(SERVER_PORT); addr.sin_addr.s_addr = inet_addr(SERVER_HOST); tStart = clock(); for(int i=0 ; i<EPOLL_SIZE; i++) //生成EPOLL_SIZE個(gè)客戶端,,這里是10000個(gè),模擬高并發(fā) { CHK2(sock,socket(PF_INET, SOCK_STREAM, 0)); CHK(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0); list_of_clients.push_back(sock); bzero(&message, BUF_SIZE); CHK2(res,recv(sock, message, BUF_SIZE, 0)); printf("%s\n", message); } list<int>::iterator it; //移除所有客戶端 for(it = list_of_clients.begin(); it != list_of_clients.end() ; it++) close(*it); printf("Test passed at: %.2f second(s)\n", (double)(clock() - tStart)/CLOCKS_PER_SEC); printf("Total server connections was: %d\n", EPOLL_SIZE); return 0; }
我就不給出程序的執(zhí)行結(jié)果的截圖了,,不過下面這張截圖是代碼作者自己測(cè)試的,,可以看出,并發(fā)10000無壓力呀
單個(gè)客戶端去連接服務(wù)器,,client.cpp文件,,如下:
#include "local.h" #include "utils.h" using namespace std; char message[BUF_SIZE]; /* 流程: 調(diào)用fork產(chǎn)生兩個(gè)進(jìn)程,兩個(gè)進(jìn)程通過管道進(jìn)行通信 子進(jìn)程:等待客戶輸入,,并將客戶輸入的信息通過管道寫給父進(jìn)程 父進(jìn)程:接受服務(wù)器的信息并顯示,,將從子進(jìn)程接受到的信息發(fā)送給服務(wù)器 */ int main(int argc, char *argv[]) { int sock, pid, pipe_fd[2], epfd; struct sockaddr_in addr; addr.sin_family = PF_INET; addr.sin_port = htons(SERVER_PORT); addr.sin_addr.s_addr = inet_addr(SERVER_HOST); static struct epoll_event ev, events[2]; ev.events = EPOLLIN | EPOLLET; //退出標(biāo)志 int continue_to_work = 1; CHK2(sock,socket(PF_INET, SOCK_STREAM, 0)); CHK(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0); CHK(pipe(pipe_fd)); CHK2(epfd,epoll_create(EPOLL_SIZE)); ev.data.fd = sock; CHK(epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev)); ev.data.fd = pipe_fd[0]; CHK(epoll_ctl(epfd, EPOLL_CTL_ADD, pipe_fd[0], &ev)); // 調(diào)用fork產(chǎn)生兩個(gè)進(jìn)程 CHK2(pid,fork()); switch(pid) { case 0: // 子進(jìn)程 close(pipe_fd[0]); // 關(guān)閉讀端 printf("Enter 'exit' to exit\n"); while(continue_to_work) { bzero(&message, BUF_SIZE); fgets(message, BUF_SIZE, stdin); // 當(dāng)收到exit命令時(shí),退出 if(strncasecmp(message, CMD_EXIT, strlen(CMD_EXIT)) == 0) { continue_to_work = 0; } else { CHK(write(pipe_fd[1], message, strlen(message) - 1)); } } break; default: // 父進(jìn)程 close(pipe_fd[1]); // 關(guān)閉寫端 int epoll_events_count, res; while(continue_to_work) { CHK2(epoll_events_count,epoll_wait(epfd, events, 2, EPOLL_RUN_TIMEOUT)); for(int i = 0; i < epoll_events_count ; i++) { bzero(&message, BUF_SIZE); if(events[i].data.fd == sock) //從服務(wù)器接受信息 { CHK2(res,recv(sock, message, BUF_SIZE, 0)); if(res == 0) //服務(wù)器已關(guān)閉 { CHK(close(sock)); continue_to_work = 0; } else { printf("%s\n", message); } } else //從子進(jìn)程接受信息 { CHK2(res, read(events[i].data.fd, message, BUF_SIZE)); if(res == 0) { continue_to_work = 0; } else { CHK(send(sock, message, BUF_SIZE, 0)); } } } } } if(pid) { close(pipe_fd[0]); close(sock); }else { close(pipe_fd[1]); } return 0; }