redis epoll 原理梗概

redis 是一個單線程卻性能很是好的內存數據庫, 主要用來做爲緩存系統。 redis 採用網絡IO多路複用技術來保證在多鏈接的時候, 系統的高吞吐量。
爲何 Redis 中要使用 I/O 多路複用這種技術呢?
首先,Redis 是跑在單線程中的,全部的操做都是按照順序線性執行的,可是因爲讀寫操做等待用戶輸入或輸出都是阻塞的,因此 I/O 操做在通常狀況下每每不能直接返回,這會致使某一文件的 I/O 阻塞致使整個進程沒法對其它客戶提供服務,而 I/O 多路複用就是爲了解決這個問題而出現的。
redis的io模型主要是基於epoll實現的,不過它也提供了 select和kqueue的實現,默認採用epoll。
那麼epoll究竟是個什麼東西呢? 其實只是衆多i/o多路複用技術當中的一種而已,可是相比其餘io多路複用技術(select, poll等等),epoll有諸多優勢:
  1. epoll 沒有最大併發鏈接的限制,上限是最大能夠打開文件的數目,這個數字通常遠大於 2048, 通常來講這個數目和系統內存關係很大  ,具體數目能夠 cat /proc/sys/fs/file-max 察看。
  2. 效率提高, Epoll 最大的優勢就在於它只管你「活躍」的鏈接 ,而跟鏈接總數無關,所以在實際的網絡環境中, Epoll 的效率就會遠遠高於 select 和 poll 。
  3. 內存拷貝, Epoll 在這點上使用了「共享內存 」,這個內存拷貝也省略了。redis

epoll與select/poll的區別
     select,poll,epoll都是IO多路複用的機制。I/O多路複用就經過一種機制,能夠監視多個描述符,一旦某個描述符就緒,可以通知程序進行相應的操做。
     select的本質是採用32個整數的32位,即32*32= 1024來標識,fd值爲1-1024。當fd的值超過1024限制時,就必須修改FD_SETSIZE的大小。這個時候就能夠標識32*max值範圍的fd。
     poll與select不一樣,經過一個pollfd數組向內核傳遞須要關注的事件,故沒有描述符個數的限制,pollfd中的events字段和revents分別用於標示關注的事件和發生的事件,故pollfd數組只須要被初始化一次。
     epoll仍是poll的一種優化,返回後不須要對全部的fd進行遍歷,在內核中維持了fd的列表。select和poll是將這個內核列表維持在用戶態,而後傳遞到內核中。與poll/select不一樣,epoll再也不是一個單獨的系統調用,而是由epoll_create/epoll_ctl/epoll_wait三個系統調用組成,後面將會看到這樣作的好處。epoll在2.6之後的內核才支持。
select/poll的幾大缺點:
一、每次調用select/poll,都須要把fd集合從用戶態拷貝到內核態,這個開銷在fd不少時會很大
二、同時每次調用select/poll都須要在內核遍歷傳遞進來的全部fd,這個開銷在fd不少時也很大
三、針對select支持的文件描述符數量過小了,默認是1024
4.select返回的是含有整個句柄的數組,應用程序須要遍歷整個數組才能發現哪些句柄發生了事件;
5.select的觸發方式是水平觸發,應用程序若是沒有完成對一個已經就緒的文件描述符進行IO操做,那麼以後每次select調用仍是會將這些文件描述符通知進程。
相比select模型,poll使用鏈表保存文件描述符,所以沒有了監視文件數量的限制,但其餘三個缺點依然存在。數據庫

epoll IO多路複用模型實現機制
因爲epoll的實現機制與select/poll機制徹底不一樣,上面所說的 select的缺點在epoll上不復存在。
epoll沒有這個限制,它所支持的FD上限是最大能夠打開文件的數目,這個數字通常遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右
設想一下以下場景:有100萬個客戶端同時與一個服務器進程保持着TCP鏈接。而每一時刻,一般只有幾百上千個TCP鏈接是活躍的(事實上大部分場景都是這種狀況)。如何實現這樣的高併發?
在select/poll時代,服務器進程每次都把這100萬個鏈接告訴操做系統(從用戶態複製句柄數據結構到內核態),讓操做系統內核去查詢這些套接字上是否有事件發生,輪詢完後,再將句柄數據複製到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,所以,select/poll通常只能處理幾千的併發鏈接。
若是沒有I/O事件產生,咱們的程序就會阻塞在select處。可是依然有個問題,咱們從select那裏僅僅知道了,有I/O事件發生了,但卻並不知道是那幾個流(可能有一個,多個,甚至所有),咱們只能無差異輪詢全部流,找出能讀出數據,或者寫入數據的流,對他們進行操做。
可是使用select,咱們有O(n)的無差異輪詢複雜度,同時處理的流越多,每一次無差異輪詢時間就越長
epoll的設計和實現與select徹底不一樣。epoll經過在Linux內核中申請一個簡易的文件系統(文件系統通常用什麼數據結構實現?B+樹)。把原先的select/poll調用分紅了3個部分:
1)調用epoll_create()創建一個epoll對象(在epoll文件系統中爲這個句柄對象分配資源)
2)調用epoll_ctl向epoll對象中添加這100萬個鏈接的套接字
3)調用epoll_wait收集發生的事件的鏈接
如此一來,要實現上面說是的場景,只須要在進程啓動時創建一個epoll對象,而後在須要的時候向這個epoll對象中添加或者刪除鏈接。同時,epoll_wait的效率也很是高,由於調用epoll_wait時,並無一股腦的向操做系統複製這100萬個鏈接的句柄數據,內核也不須要去遍歷所有的鏈接。數組

底層實現:
當某一進程調用epoll_create方法時,Linux內核會建立一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式密切相關。eventpoll結構體以下所示:緩存


每個epoll對象都有一個獨立的eventpoll結構體,用於存放經過epoll_ctl方法向epoll對象中添加進來的事件。這些事件都會掛載在紅黑樹中,如此,重複添加的事件就能夠經過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n爲樹的高度)。
而全部添加到epoll中的事件都會與設備(網卡)驅動程序創建回調關係,也就是說,當相應的事件發生時會調用這個回調方法。這個回調方法在內核中叫ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中。
在epoll中,對於每個事件,都會創建一個epitem結構體,以下所示:服務器


當調用epoll_wait檢查是否有事件發生時,只須要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素便可。若是rdlist不爲空,則把發生的事件複製到用戶態,同時將事件數量返回給用戶。
優點:
1. 不用重複傳遞。咱們調用epoll_wait時就至關於以往調用select/poll,可是這時卻不用傳遞socket句柄給內核,由於內核已經在epoll_ctl中拿到了要監控的句柄列表。
 2. 在內核裏,一切皆文件。因此,epoll向內核註冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統裏建立一個file結點。固然這個file不是普通文件,它只服務於epoll。
epoll在被內核初始化時(操做系統啓動),同時會開闢出epoll本身的內核高速cache區,用於安置每個咱們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache裏,以支持快速的查找、插入、刪除。這個內核高速cache區,就是創建連續的物理內存頁,而後在之上創建slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閒的已分配好的對象。
 3. 極其高效的緣由:
這是因爲咱們在調用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插入到準備就緒鏈表裏了。(注:好好理解這句話!)
從上面這句能夠看出,epoll的基礎就是回調呀! 
 
    如此,一顆紅黑樹,一張準備就緒句柄鏈表,少許的內核cache,就幫咱們解決了大併發下的socket處理問題。執行epoll_create時,建立了紅黑樹和就緒鏈表,執行epoll_ctl時,若是增長socket句柄,則檢查在紅黑樹中是否存在,存在當即返回,不存在則添加到樹幹上,而後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時馬上返回準備就緒鏈表裏的數據便可。網絡

    最後看看epoll獨有的兩種模式LT和ET。不管是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在之後調用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。
 
    關於LT,ET,有一端描述,LT和ET都是電子裏面的術語,ET是邊緣觸發,LT是水平觸發,一個表示只有在變化的邊際觸發,一個表示在某個階段都會觸發。
LT, ET這件事怎麼作到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時咱們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,而後清空準備就緒list鏈表,最後,epoll_wait幹了件事,就是檢查這些socket,若是不是ET模式(就是LT模式的句柄了),而且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。因此,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回這個句柄。(從上面這段,能夠看出,LT還有個回放的過程,低效了)數據結構

---------------------
做者:wxy941011
來源:CSDN
原文:https://blog.csdn.net/wxy941011/article/details/80274233
版權聲明:本文爲博主原創文章,轉載請附上博文連接!併發

相關文章
相關標籤/搜索