epoll 原理
本文轉載自epoll 原理
導語
以前經常被人問道 select、poll、epoll 的區別,基本都是靠死記硬背的,最近正好復習 linux 相關的內容,就把這一塊做個筆記吧,以后也能方便查閱。
epoll 是 linux 2.6 之后新出的一種 I/O 多路復用方式,與傳統的 select、poll 相比,有著很大的優勢。一些開源的軟件如 nginx 也采用了 epoll 的設計思路。因此,學習 epoll 對于我們在 linux 環境下編程是很有幫助的。
本文是 epoll 的復習筆記,主要講一下 epoll 與傳統的 select、poll 區別,實現的原理,以及應用。
為什么要 I/O 多路復用
前文說過了,epoll 是一個優秀的 I/O 多路復用方式。所以,在講解 epoll 之前,我們先來看一下為什么需要 I/O 多路復用。
阻塞 OR 非阻塞
我們知道,對于 linux 來說,I/O 設備為特殊的文件,讀寫和文件是差不多的,但是 I/O 設備因為讀寫與內存讀寫相比,速度差距非常大。與 cpu 讀寫速度更是沒法比,所以相比于對內存的讀寫,I/O 操作總是拖后腿的那個。網絡 I/O 更是如此,我們很多時候不知道網絡 I/O 什么時候到來,就好比我們點了一份外賣,不知道外賣小哥們什么時候送過來,這個時候有兩個處理辦法:
第一個是我們可以先去睡覺,外賣小哥送到樓下了自然會給我們打電話,這個時候我們在醒來取外賣就可以了。
第二個是我們可以每隔一段時間就給外賣小哥打個電話,這樣就能實時掌握外賣的動態信息了。
第一種方式對應的就是阻塞的 I/O 處理方式,進程在進行 I/O 操作的時候,進入睡眠,如果有 I/O 時間到達,就喚醒這個進程。第二種方式對應的是非阻塞輪詢的方式,進程在進行 I/O 操作后,每隔一段時間向內核詢問是否有 I/O 事件到達,如果有就立刻處理。
線程池 OR 輪詢
在現實中,我們當然選擇第一種方式,但是在計算機中,情況就要復雜一些。我們知道,在 linux 中,不管是線程還是進程都會占用一定的資源,也就是說,系統總的線程和進程數是一定的。如果有許多的線程或者進程被掛起,無疑是白白消耗了系統的資源。而且,線程或者進程的切換也是需要一定的成本的,需要上下文切換,如果頻繁的進行上下文切換,系統會損失很大的性能。一個網絡服務器經常需要連接成千上萬個客戶端,而它能創建的線程可能之后幾百個,線程耗光就不能對外提供服務了。這些都是我們在選擇 I/O 機制的時候需要考慮的。這種阻塞的 I/O 模式下,一個線程只能處理一個流的 I/O 事件,這是問題的根源。
這個時候我們首先想到的是采用線程池的方式限制同時訪問的線程數,這樣就能夠解決線程不足的問題了。但是這又會有第二個問題了,多余的任務會通過隊列的方式存儲在內存只能夠,這樣很容易在客戶端過多的情況下出現內存不足的情況。
還有一種方式是采用輪詢的方式,我們只要不停的把所有流從頭到尾問一遍,又從頭開始。這樣就可以處理多個流了。
代理
采用輪詢的方式雖然能夠處理多個 I/O 事件,但是也有一個明顯的缺點,那就是會導致 CPU 空轉。試想一下,如果所有的流中都沒有數據,那么 CPU 時間就被白白的浪費了。
為了避免CPU空轉,可以引進了一個代理。這個代理比較厲害,可以同時觀察許多流的I/O事件,在空閑的時候,會把當前線程阻塞掉,當有一個或多個流有I/O事件時,就從阻塞態中醒來,于是我們的程序就會輪詢一遍所有的流。
這就是 select 與 poll 所做的事情,可見,采用 I/O 復用極大的提高了系統的效率。
為什么需要 epoll
select 與 poll 的缺陷
上文中我們發現,實現一個代理來幫助我們處理 I/O 時間能夠極大的提高工作效率,select 與 poll 就是這樣的代理。
但是它們也不是完美的,從上文中我們可以發現,我們能夠從 select 中知道是只是有 I/O 事件發生了。但是我們不知道那一個事件發生,每一個 I/O 事件發生的時候,都需要輪詢所有的流,這樣的時間復雜度 O(N)。但是很多情況下,發生 I/O 時間的只是少數的幾個。通過輪詢所有的找出少數的幾個發生 I/O 的流顯然效率非常低下,因此 select 和 epoll 通常只能處理幾千個并發連接。
epoll 的優勢
說了這么多,總算引出了我們的主人公 epoll 了。不同于忙輪詢和無差別輪詢,epoll 會把哪個流發生了怎樣的 I/O 事件通知我們。此時我們對這些流的操作都是有意義的。(復雜度降低到了O(k),k為產生 I/O 事件的流的個數。
epoll 原理
epoll 操作
epoll 在 linux 內核中申請了一個簡易的文件系統,把原先的一個 select 或者 poll 調用分為了三個部分:調用 epoll_create 建立一個 epoll 對象(在 epoll 文件系統中給這個句柄分配資源)、調用 epoll_ctl 向 epoll 對象中添加連接的套接字、調用 epoll_wait 收集發生事件的連接。這樣只需要在進程啟動的時候建立一個 epoll 對象,并在需要的時候向它添加或者刪除連接就可以了,因此,在實際收集的時候,epoll_wait 的效率會非常高,因為調用的時候只是傳遞了發生 IO 事件的連接。
epoll 實現
我們以 linux 內核 2.6 為例,說明一下 epoll 是如何高效的處理事件的。
當某一個進程調用 epoll_create 方法的時候,Linux 內核會創建一個 eventpoll 結構體,這個結構體中有兩個重要的成員。
第一個是 rb_root rbr,這是紅黑樹的根節點,存儲著所有添加到 epoll 中的事件,也就是這個 epoll 監控的事件。
第二個是 list_head rdllist 這是一個雙向鏈表,保存著將要通過 epoll_wait 返回給用戶的、滿足條件的事件。
每一個 epoll 對象都有一個獨立的 eventpoll 結構體,這個結構體會在內核空間中創造獨立的內存,用于存儲使用 epoll_ctl 方法向 epoll 對象中添加進來的事件。這些事件都會掛到 rbr 紅黑樹中,這樣就能夠高效的識別重復添加的節點。
所有添加到 epoll 中的事件都會與設備(如網卡等)驅動程序建立回調關系,也就是說,相應的事件發生時會調用這里的方法。這個回調方法在內核中叫做 ep_poll_callback,它把這樣的事件放到 rdllist 雙向鏈表中。在 epoll 中,對于每一個事件都會建立一個 epitem 結構體。
當調用 epoll_wait 檢查是否有發生事件的連接時,只需要檢查 eventpoll 對象中的 rdllist 雙向鏈表中是否有 epitem 元素,如果 rdllist 鏈表不為空,則把這里的事件復制到用戶態內存中的同時,將事件數量返回給用戶。通過這種方法,epoll_wait 的效率非常高。epoll-ctl 在向 epoll 對象中添加、修改、刪除事件時,從 rbr 紅黑樹中查找事件也非常快。這樣,epoll 就能夠輕易的處理百萬級的并發連接。
epoll 工作模式
epoll 有兩種工作模式,LT(水平觸發)模式與 ET(邊緣觸發)模式。默認情況下,epoll 采用 LT 模式工作。兩個的區別是:
Level_triggered(水平觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。如果這次沒有把數據一次性全部讀寫完(如讀寫緩沖區太小),那么下次調用 epoll_wait() 時,它還會通知你在上沒讀寫完的文件描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你。如果系統中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率。
Edge_triggered(邊緣觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait() 會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩沖區太小),那么下次調用 epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你。這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符。
當然,在 LT 模式下開發基于 epoll 的應用要簡單一些,不太容易出錯,而在 ET 模式下事件發生時,如果沒有徹底地將緩沖區的數據處理完,則會導致緩沖區的用戶請求得不到響應。注意,默認情況下 Nginx 采用 ET 模式使用 epoll 的。
參考資料
https://www.cnblogs.com/ajianbeyourself/p/5859989.html
https://www.zhihu.com/question/20122137
總結
- 上一篇: js常用字符串函数
- 下一篇: 存储过程-求当前日期是这个月的第几周