epoll 或者 kqueue 的原理是什麼? 【轉自知乎】服務器
首先咱們來定義流的概念,一個流能夠是文件,socket,pipe等等能夠進行I/O操做的內核對象。數據結構
很明顯通常人不會用第二種作法,不只顯很無腦,浪費話費不說,還佔用了快遞員大量的時間。
大部分程序也不會用第二種作法,由於第一種方法經濟而簡單,經濟是指消耗不多的CPU時間,若是線程睡眠了,就掉出了系統的調度隊列,暫時不會去瓜分CPU寶貴的時間片了。多線程
這四個情形涵蓋了四個I/O事件,緩衝區滿,緩衝區空,緩衝區非空,緩衝區非滿(注都是說的內核緩衝區,且這四個術語都是我生造的,僅爲解釋其原理而造)。這四個I/O事件是進行阻塞同步的根本。(若是不能理解「同步」是什麼概念,請學習操做系統的鎖,信號量,條件變量等任務同步方面的相關知識)。併發
而後咱們來講說阻塞I/O的缺點。可是阻塞I/O模式下,一個線程只能處理一個流的I/O事件。若是想要同時處理多個流,要麼多進程(fork),要麼多線程(pthread_create),很不幸這兩種方法效率都不高。
因而再來考慮非阻塞忙輪詢的I/O方式,咱們發現咱們能夠同時處理多個流了(把一個流從阻塞模式切換到非阻塞模式再此不予討論):socket
while true { for i in stream[]; { if i has data read until unavailable } }
咱們只要不停的把全部流從頭至尾問一遍,又從頭開始。這樣就能夠處理多個流了,但這樣的作法顯然很差,由於若是全部的流都沒有數據,那麼只會白白浪費CPU。這裏要補充一點,阻塞模式下,內核對於I/O事件的處理是阻塞或者喚醒,而非阻塞模式下則把I/O事件交給其餘對象(後文介紹的select以及epoll)處理甚至直接忽略。函數
爲了不CPU空轉,能夠引進了一個代理(一開始有一位叫作select的代理,後來又有一位叫作poll的代理,不過二者的本質是同樣的)。這個代理比較厲害,能夠同時觀察許多流的I/O事件,在空閒的時候, 會把當前線程阻塞掉,當有一個或多個流有I/O事件時,就從阻塞態中醒來,因而咱們的程序就會輪詢一遍全部的流(因而咱們能夠把「忙」字去掉了)。代碼長這樣:while true { select(streams[]) for i in streams[] { if i has data read until unavailable } }
因而,若是沒有I/O事件產生,咱們的程序就會阻塞在select處。可是依然有個問題,咱們從select那裏僅僅知道了,有I/O事件發生了,但卻並不知道是那幾個流(可能有一個,多個,甚至所有),咱們只能無差異輪詢全部流,找出能讀出數據,或者寫入數據的流,對他們進行操做。學習
可是使用select,咱們有O(n)的無差異輪詢複雜度,同時處理的流越多,每一次無差異輪詢時間就越長。再次while true { active_stream[] = epoll_wait(epollfd) for i in active_stream[] { read or write till unavailable } }
限於篇幅,我只說這麼多,以揭示原理性的東西,至於epoll的使用細節,請參考man和googlegoogle
關鍵詞:應用程序 文件句柄 用戶態 內核態 監控者spa
要比較epoll相比較select高效在什麼地方,就須要比較兩者作相同事情的方法。操作系統
要完成對I/O流的複用須要完成以下幾個事情:
1.用戶態怎麼將文件句柄傳遞到內核態?
2.內核態怎麼判斷I/O流可讀可寫?
3.內核怎麼通知監控者有I/O流可讀可寫?
4.監控者如何找到可讀可寫的I/O流並傳遞給用戶態應用程序?
5.繼續循環時監控者怎樣重複上述步驟?
搞清楚上述的步驟也就能解開epoll高效的緣由了。
select的作法:
步驟1的解法:select建立3個文件描述符集,並將這些文件描述符拷貝到內核中,這裏限制了文件句柄的最大的數量爲1024(注意是所有傳入---第一次拷貝);
步驟2的解法:內核針對讀緩衝區和寫緩衝區來判斷是否可讀可寫,這個動做和select無關;
步驟3的解法:內核在檢測到文件句柄可讀/可寫時就產生中斷通知監控者select,select被內核觸發以後,就返回可讀可寫的文件句柄的總數;
步驟4的解法:select會將以前傳遞給內核的文件句柄再次從內核傳到用戶態(第2次拷貝),select返回給用戶態的只是可讀可寫的文件句柄總數,再使用FD_ISSET宏函數來檢測哪些文件I/O可讀可寫(遍歷);
步驟5的解法:select對於事件的監控是創建在內核的修改之上的,也就是說通過一次監控以後,內核會修改位,所以再次監控時須要再次從用戶態向內核態進行拷貝(第N次拷貝)
epoll的作法:
步驟1的解法:首先執行epoll_create在內核專屬於epoll的高速cache區,並在該緩衝區創建紅黑樹和就緒鏈表,用戶態傳入的文件句柄將被放到紅黑樹中(第一次拷貝)。
步驟2的解法:內核針對讀緩衝區和寫緩衝區來判斷是否可讀可寫,這個動做與epoll無關;
步驟3的解法:epoll_ctl執行add動做時除了將文件句柄放到紅黑樹上以外,還向內核註冊了該文件句柄的回調函數,內核在檢測到某句柄可讀可寫時則調用該回調函數,回調函數將文件句柄放到就緒鏈表。
步驟4的解法:epoll_wait只監控就緒鏈表就能夠,若是就緒鏈表有文件句柄,則表示該文件句柄可讀可寫,並返回到用戶態(少許的拷貝);
步驟5的解法:因爲內核不修改文件句柄的位,所以只須要在第一次傳入就能夠重複監控,直到使用epoll_ctl刪除,不然不須要從新傳入,所以無屢次拷貝。
簡單說:epoll是繼承了select/poll的I/O複用的思想,並在兩者的基礎上從監控IO流、查找I/O事件等角度來提升效率,具體地說就是內核句柄列表、紅黑樹、就緒list鏈表來實現的。
第二部分:epoll詳解
先簡單回顧下如何使用C庫封裝的3個epoll系統調用吧。
使用起來很清晰:
A.epoll_create創建一個epoll對象。參數size是內核保證可以正確處理的最大句柄數,多於這個最大數時內核可不保證效果。
B.epoll_ctl能夠操做上面創建的epoll,例如,將剛創建的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,再也不監控它等等(也就是將I/O流放到內核)。
C.epoll_wait在調用時,在給定的timeout時間內,當在監控的全部句柄中有事件發生時,就返回用戶態的進程(也就是在內核層面捕獲可讀寫的I/O事件)。
從上面的調用方式就能夠看到epoll比select/poll的優越之處:
由於後者每次調用時都要傳遞你所要監控的全部socket給select/poll系統調用,這意味着須要將用戶態的socket列表copy到內核態,若是以萬計的句柄會致使每次都要copy幾十幾百KB的內存到內核態,很是低效。而咱們調用epoll_wait時就至關於以往調用select/poll,可是這時卻不用傳遞socket句柄給內核,由於內核已經在epoll_ctl中拿到了要監控的句柄列表。
====>select監控的句柄列表在用戶態,每次調用都須要從用戶態將句柄列表拷貝到內核態,可是epoll中句柄就是創建在內核中的,這樣就減小了內核和用戶態的拷貝,高效的緣由之一。
因此,實際上在你調用epoll_create後,內核就已經在內核態開始準備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構裏塞入新的socket句柄。
在內核裏,一切皆文件。因此,epoll向內核註冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統裏建立一個file結點。固然這個file不是普通文件,它只服務於epoll。
epoll在被內核初始化時(操做系統啓動),同時會開闢出epoll本身的內核高速cache區,用於安置每個咱們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache裏,以支持快速的查找、插入、刪除。這個內核高速cache區,就是創建連續的物理內存頁,而後在之上創建slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閒的已分配好的對象。
epoll高效的緣由:
這是因爲咱們在調用epoll_create時,內核除了幫咱們在epoll文件系統裏建了個file結點,在內核cache裏建了個紅黑樹用於存儲之後epoll_ctl傳來的socket外,還會再創建一個list鏈表,用於存儲準備就緒的事件.
當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據便可。有數據就返回,沒有數據就sleep,等到timeout時間到後即便鏈表沒數據也返回。因此,epoll_wait很是高效。並且,一般狀況下即便咱們要監控百萬計的句柄,大多一次也只返回不多量的準備就緒句柄而已,因此,epoll_wait僅須要從內核態copy少許的句柄到用戶態而已.
那麼,這個準備就緒list鏈表是怎麼維護的呢?
當咱們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上以外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,若是這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。因此,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。
epoll綜合的執行過程:
如此,一棵紅黑樹,一張準備就緒句柄鏈表,少許的內核cache,就幫咱們解決了大併發下的socket處理問題。執行epoll_create時,建立了紅黑樹和就緒鏈表,執行epoll_ctl時,若是增長socket句柄,則檢查在紅黑樹中是否存在,存在當即返回,不存在則添加到樹幹上,而後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時馬上返回準備就緒鏈表裏的數據便可。
epoll水平觸發和邊緣觸發的實現:
當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時咱們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,而後清空準備就緒list鏈表, 最後,epoll_wait幹了件事,就是檢查這些socket,若是不是ET模式(就是LT模式的句柄了),而且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了,因此,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即便socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。
====>區別就在於epoll_wait將socket返回到用戶態時是否狀況就緒鏈表。
第三部分:epoll高效的本質
1.減小用戶態和內核態之間的文件句柄拷貝;
2.減小對可讀可寫文件句柄的遍歷;