一、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)