以一個生活中的例子來解釋.
假設你在大學中讀書,要等待一個朋友來訪,而這個朋友只知道你在A號樓,但是不知道你具體住在哪里,于是你們約好了在A號樓門口見面.
如果你使用的阻塞IO模型來處理這個問題,那么你就只能一直守候在A號樓門口等待朋友的到來,在這段時間里你不能做別的事情,不難知道,這種方式的效率是低下的.
進一步解釋select和epoll模型的差異.
select版大媽做的是如下的事情:比如同學甲的朋友來了,select版大媽比較笨,她帶著朋友挨個房間進行查詢誰是同學甲,你等的朋友來了,于是在實際的代碼中,select版大媽做的是以下的事情:
int n = select(&readset,NULL,NULL,100);
for (int i = 0; n > 0; ++i) {
if (FD_ISSET(fdarray[i], &readset)) {
do_something(fdarray[i]); --n;
}
}
epoll版大媽就比較先進了,她記下了同學甲的資訊,比如說他的房間號,那么等同學甲的朋友到來時,只需要告訴該朋友同學甲在哪個房間即可,不用自己親自帶著人滿大樓的找人了.于是epoll版大媽做的事情可以用如下的代碼表示:
n = epoll_wait(epfd,events,20,500);
for(i=0;i<n;++i) { do_something(events[n]);
}
在epoll中,關鍵的資料結構epoll_event定義如下:
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 */
};
可以看到,epoll_data是一個union結構體,它就是epoll版大媽用于保存同學資訊的結構體,它可以保存很多型別的資訊:
fd,指標,等等.有了這個結構體,epoll大媽可以不用吹灰之力就可以定位到同學甲.
別小看了這些效率的提高,在一個大規模并發的服務器中,輪詢IO是最耗時間的操作之一.再回到那個例子中,如果每到來一個朋友樓管大媽都要全樓的查詢同學,那么處理的效率必然就低下了,過不久樓底就有不少的人了.
對比最早給出的阻塞IO的處理模型, 可以看到采用了多路復用IO之后, 程式可以自由的進行自己除了IO操作之外的作業, 只有到IO狀態發生變化的時候由多路復用IO進行通知, 然后再采取相應的操作, 而不用一直阻塞等待IO狀態發生變化了.
從上面的分析也可以看出,epoll比select的提高實際上是一個用空間換時間思想的具體應用.
【文章福利】小編推薦自己的linuxC/C++語言交流群:832218493,整理了一些個人覺得比較好的學習書籍、視頻資料共享在里面,有需要的可以自行添加哦!~

更多優秀文章在公眾號

二、深入理解epoll的實作原理:開發高性能網路程式時,windows開發者們言必稱iocp,linux開發者們則言必稱epoll,
大家都明白epoll是一種IO多路復用技術,可以非常高效的處理數以百萬計的socket句柄,比起以前的select和poll效率高大發了,
我們用起epoll來都感覺挺爽,確實快,那么,它到底為什么可以高速處理這么多并發連接呢?
先簡單回顧下如何使用C庫封裝的3個epoll系統呼叫吧,
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
使用起來很清晰,首先要呼叫epoll_create建立一個epoll物件,引數size是內核保證能夠正確處理的最大句柄數,多于這個最大數時內核可不保證效果,
epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等,
epoll_wait在呼叫時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就回傳用戶態的行程,
從上面的呼叫方式就可以看到epoll比select/poll的優越之處:
因為后者每次呼叫時都要傳遞你所要監控的所有socket給select/poll系統呼叫,這意味著需要將用戶態的socket串列copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的記憶體到內核態,非常低效,
而我們呼叫epoll_wait時就相當于以往呼叫select/poll,但是這時卻不用傳遞socket句柄給內核,因為內核已經在epoll_ctl中拿到了要監控的句柄串列,
所以,實際上在你呼叫epoll_create后,內核就已經在內核態開始準備幫你存盤要監控的句柄了,每次呼叫epoll_ctl只是在往內核的資料結構里塞入新的socket句柄,
在內核里,一切皆檔案,所以,epoll向內核注冊了一個檔案系統,用于存盤上述的被監控socket,
當你呼叫epoll_create時,就會在這個虛擬的epoll檔案系統里創建一個file結點,當然這個file不是普通檔案,它只服務于epoll,epoll在被內核初始化時(作業系統啟動),同時會開辟出epoll自己的內核高速cache區,用于安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache里,以支持快速的查找、插入、洗掉,
這個內核高速cache區,就是建立連續的物理記憶體頁,然后在之上建立slab層,簡單的說,就是物理上分配好你想要的size的記憶體物件,每次使用時都是使用空閑的已分配好的物件,
static int __init eventpoll_init(void) {
… …
/* Allocates slab cache used to allocate “struct epitem” items /
epi_cache = kmem_cache_create(“eventpoll_epi”, sizeof(struct epitem),0,SLAB_HWCACHE_ALIGN| EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);
/ Allocates slab cache used to allocate “struct eppoll_entry” */
pwq_cache = kmem_cache_create(“eventpoll_pwq”, sizeof(struct eppoll_entry), 0, EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);
… …
epoll的高效就在于,當我們呼叫epoll_ctl往里塞入百萬個句柄時,epoll_wait仍然可以飛快的回傳,并有效的將發生事件的句柄給我們用戶,
這是由于我們在呼叫epoll_create時,內核除了幫我們在epoll檔案系統里建了個file結點,在內核cache里建了個紅黑樹用于存盤以后epoll_ctl傳來的socket外,還會再建立一個list鏈表,用于存盤準備就緒的事件,當epoll_wait呼叫時,僅僅觀察這個list鏈表里有沒有資料即可,有資料就回傳,沒有資料就sleep,等到timeout時間到后即使鏈表沒資料也回傳,所以,epoll_wait非常高效,
那么,這個準備就緒list鏈表是怎么維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll檔案系統里file物件對應的紅黑樹上之外,還會給內核中斷處理程式注冊一個回呼函式,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表里,
所以,當一個socket上有資料到了,內核在把網卡上的資料copy到內核中后就來把socket插入到準備就緒鏈表里了,
如此,一顆紅黑樹,一張準備就緒句柄鏈表,少量的內核cache,就幫我們解決了大并發下的socket處理問題,
執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即回傳,不存在則添加到樹干上,然后向內核注冊回呼函式,用于當中斷事件來臨時向準備就緒鏈表中插入資料,執行epoll_wait時立刻回傳準備就緒鏈表里的資料即可,
最后看看epoll獨有的兩種模式LT和ET,無論是LT和ET模式,都適用于以上所說的流程,
區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以后呼叫epoll_wait時次次回傳這個句柄,而ET模式僅在第一次回傳,
這件事怎么做到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時我們呼叫epoll_wait,會把準備就緒的socket拷貝到用戶態記憶體,然后清空準備就緒list鏈表,最后,epoll_wait干了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),并且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了,
所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會回傳,而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait回傳的,
三、擴展閱讀(epoll與之前其他相關技術的比較):
Linux提供了select、poll、epoll介面來實作IO復用,三者的原型如下所示,本文從引數、實作、性能等方面對三者進行對比,
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
select、poll、epoll_wait引數及實作對比
1、select的第一個引數nfds為fdset集合中最大描述符值加1,fdset是一個位陣列,其大小限制為__FD_SETSIZE(1024),位陣列的每一位代表其對應的描述符是否需要被檢查,
select的第二三四個引數表示需要關注讀、寫、錯誤事件的檔案描述符位陣列,這些引數既是輸入引數也是輸出引數,可能會被內核修改用于標示哪些描述符上發生了關注的事件,
所以每次呼叫select前都需要重新初始化fdset,
timeout引數為超時時間,該結構會被內核修改,其值為超時剩余的時間,
select對應于內核中的sys_select呼叫,sys_select首先將第二三四個引數指向的fd_set拷貝到內核,然后對每個被SET的描述符呼叫進行poll,并記錄在臨時結果中(fdset),如果有事件發生,select會將臨時結果寫到用戶空間并回傳;當輪詢一遍后沒有任何事件發生時,如果指定了超時時間,則select會睡眠到超時,睡眠結束后再進行一次輪詢,并將臨時結果寫到用戶空間,然后回傳, select回傳后,需要逐一檢查關注的描述符是否被SET(事件是否發生),
2、poll與select不同,通過一個pollfd陣列向內核傳遞需要關注的事件,故沒有描述符個數的限制,pollfd中的events欄位和revents分別用于標示關注的事件和發生的事件,故pollfd陣列只需要被初始化一次,
poll的實作機制與select類似,其對應內核中的sys_poll,只不過poll向內核傳遞pollfd陣列,然后對pollfd中的每個描述符進行poll,相比處理fdset來說,poll效率更高, poll回傳后,需要對pollfd中的每個元素檢查其revents值,來得指事件是否發生,
3、epoll通過epoll_create創建一個用于epoll輪詢的描述符,通過epoll_ctl添加/修改/洗掉事件,通過epoll_wait檢查事件,epoll_wait的第二個引數用于存放結果, epoll與select、poll不同,首先,其不用每次呼叫都向內核拷貝事件描述資訊,在第一次呼叫后,事件資訊就會與對應的epoll描述符關聯起來,另外epoll不是通過輪詢,而是通過在等待的描述符上注冊回呼函式,當事件發生時,回呼函式負責把發生的事件存盤在就緒事件鏈表中,最后寫到用戶空間,
作者:金發萌音
鏈接:https://www.jianshu.com/p/b5bc204da984
來源:簡書
著作權歸作者所有,商業轉載請聯系作者獲得授權,非商業轉載請注明出處,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/230766.html
標籤:其他
