一、select的實作原理
支援阻塞操作的裝置驅動通常會實作一組自身的等待隊列如讀/寫等待隊列用于支援上層(使用者層)所需的BLOCK或NONBLOCK操作。當應用程式通過裝置驅動通路該裝置時(預設為BLOCK操作),若該裝置目前沒有資料可讀或寫,則将該使用者程序插入到該裝置驅動對應的讀/寫等待隊列讓其睡眠一段時間,等到有資料可讀/寫時再将該程序喚醒。
select就是巧妙的利用等待隊列機制讓使用者程序适當在沒有資源可讀/寫時睡眠,有資源可讀/寫時喚醒。
二、poll的實作原理
poll()系統調用是System V的多元I/O解決方案。它有三個參數,第一個是pollfd結構的數組指針,也就是指向一組fd及其相關資訊的指針,因為這個結構包含的除了fd,還有期待的事件掩碼和傳回的事件掩碼,實質上就是将select的中的fd,傳入和傳出參數歸到一個結構之下,也不再把fd分為三組,也不再硬性規定fd感興趣的事件,這由調用者自己設定。這樣,不使用位圖來組織資料,也就不需要位圖的全部周遊了。按照一般隊列地周遊,每個fd做poll檔案操作,檢查傳回的掩碼是否有期待的事件,以及做是否有挂起和錯誤的必要性檢查,如果有事件觸發,就可以傳回調用了。
三、epoll的實作原理
回到poll和select的共同點,面對高并發多連接配接的應用情境,它們顯現出原來沒有考慮到的不足,雖然poll比起select又有所改進了。除了上述的關于每次調用都需要做一次從使用者空間到核心空間的拷貝,還有這樣的問題,就是當處于這樣的應用情境時,poll和select會不得不多次操作,并且每次操作都很有可能需要多次進入睡眠狀态,也就是多次全部輪詢fd,我們應該怎麼處理一些會出現重複而無意義的操作。
這些重複而無意義的操作有:
1、從使用者到核心空間拷貝,既然長期監視這幾個fd,甚至連期待的事件也不會改變,那拷貝無疑就是重複而無意義的,我們可以讓核心長期儲存所有需要監視的fd甚至期待事件,或者可以在需要時對部分期待事件進行修改(MOD,ADD,DEL);
2、将目前線程輪流加入到每個fd對應裝置的等待隊列,這樣做無非是哪一個裝置就緒時能夠通知程序退出調用,聰明的開發者想到,那就找個“代理”的回調函數,代替目前程序加入fd的等待隊列好了。這樣,像poll系統調用一樣,做poll檔案操作發現尚未就緒時,它就調用傳入的一個回調函數,這是epoll指定的回調函數,它不再像以前的poll系統調用指定的回調函數那樣,而是就将那個“代理”的回調函數加入裝置的等待隊列就好了,這個代理的回調函數就自己乖乖地等待裝置就緒時将它喚醒,然後它就把這個裝置fd放到一個指定的地方,同時喚醒可能在等待的程序,到這個指定的地方取fd就好了(ET與LT)。
我們把1和2結合起來就可以這樣做了,隻拷貝一次fd,一旦确定了fd就可以做poll檔案操作,如果有事件當然好啦,馬上就把fd放到指定的地方,而通常都是沒有的,那就給這個fd的等待隊列加一個回調函數,有事件就自動把fd放到指定的地方,目前程序不需要再一個個poll和睡眠等待了。
上面說的就是epoll了,epoll由三個系統調用組成,分别是epoll_create,epoll_ctl和epoll_wait。epoll_create用于建立和初始化一些内部使用的資料結構;epoll_ctl用于添加,删除或者修改指定的fd及其期待的事件,epoll_wait就是用于等待任何先前指定的fd事件。
下面在分析下select的缺點:
select在執行之前必須先循環添加要監聽的檔案描述符到fd集合中,是以
缺點一:每次調用select都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時開銷很大。
select調用的時候都需要在核心周遊傳遞進來的所有fd,判斷是不是我關心的事件。是以
缺點二:每次調用select都需要在核心周遊所有傳遞進來的fd,這個開銷在fd很多時,開銷也很大。
缺點三:這個由系統核心決定了,支援的檔案描述符的預設值隻有1024,想想應用到稍微大一點的伺服器就不夠用了。
下面在分析下poll的缺點:
poll對于select來說包含了一個pollfd結構,pollfd結構包含了要監視的event和發生的revent,而不像select那樣使用參數-值的傳遞方式。同時poll沒有最大數量的限制。但是
缺點一:數量過大以後其效率也會線性下降。
缺點二:poll和select一樣需要周遊檔案描述符來擷取已經就緒的socket。當數量很大時,開銷也就很大。
epoll的優點:
優點一:支援一個程序打開大數目的socket描述符
select 最不能忍受的是一個程序所打開的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對于那些需要支援的上萬連接配接數目的IM伺服器來說顯然太少了。這時候你一是可以選擇修改這個宏然後重新編譯核心,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多程序的解決方案(傳統的 Apache方案),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上線程間同步的高效,是以也不是一種完美的案。不過 epoll則沒有這個限制,它所支援的FD上限是最大可以打開檔案的數目,這個數字一般遠遠于2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。
優點二:IO效率不随FD數目增加而線性下降
傳統的select/poll另一個緻命弱點就是當你擁有一個很大的socket集合,不過由于網絡延時,任一時間隻有部分的socket是"活躍"的,但是select/poll每次調用都會線性掃描全部的集合,導緻效率呈現線性下降。但是epoll不存在這個問題,它隻會對"活躍"的socket進行操作---這是因為在核心實作中epoll是根據每個fd上面的callback函數實作的。那麼,隻有"活躍"的socket才會主動的去調用 callback函數,其他idle狀态socket則不會,在這點上,epoll實作了一個"僞"AIO,因為這時候推動是在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll并不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模拟WAN環境,epoll的效率就遠在select/poll之上了
優點三:使用mmap加速核心與使用者空間的消息傳遞
這點實際上涉及到epoll的具體實作了。無論是select,poll還是epoll都需要核心把FD消息通知給使用者空間,如何避免不必要的記憶體拷貝就很重要,在這點上,epoll是通過核心與使用者空間mmap同一塊記憶體實作的。而如果你想像我一樣從2.5核心就關注epoll的話,一定不會忘記手工mmap這一步的。(mmap底層是使用紅黑樹加隊列實作的,每次需要在操作的fd,先在紅黑樹中拿到,放到隊列中,那麼使用者收到epoll_wait消息以後隻需要看一下消息隊列中有沒有資料,有我就取走)
優點四:核心微調
這一點其實不算epoll的優點了,而是整個linux平台的優點。也許你可以懷疑linux平台,但是你無法回避linux平台賦予你微調核心的能力。比如,核心TCP/IP協定棧使用記憶體池管理sk_buff結構,那麼可以在運作時期動态調整這個記憶體pool(skb_head_pool)的大小-- 通過echoXXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數的第2個參數(TCP完成3次握手的資料包隊列長度),也可以根據你平台記憶體大小動态調整。更甚至在一個資料包裡面資料巨大但同時每個資料包本本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。
最後我們談下epoll ET模式為何fd必須要設定為非阻塞這個問題
ET邊緣觸發,資料就隻會通知一次,也就是說,如果要使用ET模式,當資料就緒時,需要一直read,知道完成或出錯為止。但倘若目前fd為阻塞的方式,那麼當讀完成緩沖區資料時,而對端并沒有關閉寫端,那麼該read就會阻塞,影響其他fd以及他以後的邏輯,是以需要設定為非阻塞,當沒有資料的時候,read雖然讀取不到資料,但是肯定不會阻塞,那麼說明此時資料已經讀取完畢,可以繼續處理後續邏輯了(讀取其他的fd或者進入wait)