歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

Linux下套接字詳解(十)---epoll模式下的IO多路復用服務器

epoll模型簡介


epoll可是當前在Linux下開發大規模並發網絡程序的熱門人選,epoll 在Linux2.6內核中正式引入,和select相似,其實都I/O多路復用技術而已,並沒有什麼神秘的。

其實在Linux下設計並發網絡程序,向來不缺少方法,比如典型的Apache模型(Process Per Connection,簡稱PPC),TPC(Thread PerConnection)模型,以及select模型和poll模型,那為何還要再引入Epoll這個東東呢?那還是有得說說的…

常用模型的缺點


如果不擺出來其他模型的缺點,怎麼能對比出Epoll的優點呢。

多進程PPC/多線程TPC模型


這兩種模型思想類似,就是讓每一個到來的連接一邊自己做事去,別再來煩我。只是PPC是為它開了一個進程,而TPC開了一個線程。可是別煩我是有代價的,它要時間和空間啊,連接多了之後,那麼多的進程/線程切換,這開銷就上來了;

因此這類模型能接受的最大連接數都不會高,一般在幾百個左右。

select模型-O(n)


多進程多線程的模型龐大而且繁瑣,因此我們出現了select模型

  int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

select系統調用是用來讓我們的程序監視多個文件句柄(file descrīptor)的狀態變化的。通過select()系統調用來監視多個文件描述符的數組,當select()返回後,該數組中就緒的文件描述符便會被內核修改標志位,使得進程可以獲得這些文件描述符從而進行後續的讀寫操作。

select系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的。程序會停在select這裡等待,直到被監視的文件描述符有某一個或多個發生了狀態改變。

select()的機制中提供一fd_set的數據結構,實際上是一long類型的數組,每一個數組元素都能與一打開的文件句柄建立聯系,建立聯系的工作由程序員完成,當調用select()時,由內核根據IO狀態修改fd_set的內容,由此來通知執行了select()的進程哪些Socket或文件可讀可寫。

當某些描述符可以讀寫之後,select返回數據(沒有數據讀寫時,select也會返回,因為select是同步)時就掃描一遍描述符fd_set來查詢那些有數據請求的描述符,並進行處理。時間復雜度為O(n)

因此性能比那些阻塞的多進程或者多線程模型性能提高不少,但是仍然不夠。因為select有很多限制

最大並發數限制,因為一個進程所打開的FD(文件描述符)是有限制的,由FD_SETSIZE設置(可以查看深入解析為何select最多只能監聽1024個),默認值是1024/2048,因此Select模型的最大並發數就被相應限制了。用戶可以自己修改FD_SETSIZE,然後重新編譯,但是其實,並不推薦這麼做

linux 下 fd_set 是個 1024 位的位圖,每個位代表一個 fd 的值,返回後需要掃描位圖,這也是效率低的原因。性能問題且不提,正確性問題則更值得重視。

因為這是一個 1024 位的位圖,因此當進程內的 fd 值 >= 1024 時,就會越界,可能會造成崩潰。對於服務器程序,fd >= 1024 很容易達到,只要連接數 + 打開的文件數足夠大即可發生。

include/linux/posix_types.h:

#define __FD_SETSIZE         1024

效率問題,select每次調用都會線性掃描全部的FD集合,這樣效率就會呈現線性下降,把FD_SETSIZE改大的後果就是,大家都慢慢來,什麼?都超時了??!!

內核/用戶空間 內存拷貝問題,如何讓內核把FD消息通知給用戶空間呢?在這個問題上select采取了內存拷貝方法。

poll模型


poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多。

他通過注冊一堆事件組,當有事件請求時返回,然後仍然需要輪詢一遍pollfd才能知道查找到對應的文件描述符,數據也需要在內核空間和用戶空間來回拷貝。時間復雜度為O(n)

因此他只解決了select的問題1,但是問題2,3仍然得不帶解決。

epoll模型


這裡寫圖片描述<喎?http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMiBpZD0="epoll的性能提升">epoll的性能提升


把其他模型逐個批判了一下,再來看看Epoll的改進之處吧,其實把select的缺點反過來那就是Epoll的優點了。

epoll沒有最大並發連接的限制,上限是最大可以打開文件的數目,這個數字一般遠大於2048, 一般來說這個數目和系統內存關系很大,具體數目可以cat /proc/sys/fs/file-max察看。

效率提升,Epoll最大的優點就在於它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll。

內存拷貝,Epoll在這點上使用了“共享內存”,這個內存拷貝也省略了。

如何解決上述的3個缺點


epoll既然是對select和poll的改進,就避免上述的三個缺點。那epoll都是怎麼解決的呢?

在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都只提供了一個函數——select或者poll函數。

而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,

epoll_create是創建一個epoll句柄;

epoll_ctl是注冊要監聽的事件類型;

epoll_wait則是等待事件的產生。

支持一個進程打開大數 目的socket描述符(FD)


對於第一個缺點並發數目限制

epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。

這裡寫圖片描述

select 最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置,默認值是2048。對於那些需要支持的上萬連接數目的IM服務器來說顯 然太少了。這時候你一是可以選擇修改這個宏然後重新編譯內核,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多進程的解決方案(傳統的 Apache方案),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完 美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左 右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。

IO 效率不隨FD數目增加而線性下降


對於第二個缺點輪詢描述符的線性復雜度

epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)並為每個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的f

傳統的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的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進內核,而不是在epoll_wait的時候重復拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。

這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的內存拷貝就 很重要,在這點上,epoll是通過內核於用戶空間mmap同一塊內存實現的。而如果你想我一樣從2.5內核就關注epoll的話,一定不會忘記手工 mmap這一步的。

總結


(1)select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。

(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,並且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裡的等待隊列並不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。

Epoll的使用


epoll的工作模式


令人高興的是,2.6內核的epoll比其2.5開發版本的/dev/epoll簡潔了許多,所以,大部分情況下,強大的東西往往是簡單的。唯一有點麻煩 是epoll有2種工作方式:LT和ET。

LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你 的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表.

ET (edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然後它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述 符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致 了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。

epoll只有epoll_create,epoll_ctl,epoll_wait 3個系統調用,具體用法請參考http://www.xmailserver.org/linux-patches/nio-improve.html ,在http://www.kegel.com/rn/也有一個完整的例子,大家一看就知道如何使用了
Leader/follower模式線程 pool實現,以及和epoll的配合。

Epoll的高效和其數據結構的設計是密不可分的,這個下面就會提到。

epoll關鍵數據結構


前面提到Epoll速度快和其數據結構密不可分,其關鍵數據結構就是:

structepoll_event {

    __uint32_t events;      // Epoll events

    epoll_data_t data;      // User datavariable

};

typedef union epoll_data {

    void *ptr;

   int fd;

    __uint32_t u32;

    __uint64_t u64;

} epoll_data_t;

可見epoll_data是一個union結構體,借助於它應用程序可以保存很多類型的信息:fd、指針等等。有了它,應用程序就可以直接定位目標了。

使用Epoll


首先回憶一下select模型,當有I/O事件到來時,select通知應用程序有事件到了快去處理,而應用程序必須輪詢所有的FD集合,測試每個FD是否有事件發生,並處理事件;代碼像下面這樣:
Epoll的高效和其數據結構的設計是密不可分的,這個下面就會提到。

首先回憶一下select模型,當有I/O事件到來時,select通知應用程序有事件到了快去處理,而應用程序必須輪詢所有的FD集合,測試每個FD是否有事件發生,並處理事件;

代碼像下面這樣:


int res = select(maxfd+1, &readfds, NULL, NULL, 120);
if(res > 0)
{

    for(int i = 0; i < MAX_CONNECTION; i++)
    {
        if(FD_ISSET(allConnection[i],&readfds))
        {
            handleEvent(allConnection[i]);
        }
    }
}
// if(res == 0) handle timeout, res < 0 handle error

epoll不僅會告訴應用程序有I/0事件到來,還會告訴應用程序相關的信息,這些信息是應用程序填充的,因此根據這些信息應用程序就能直接定位到事件,而不必遍歷整個FD集合。

intres = epoll_wait(epfd, events, 20, 120);

for(int i = 0; i < res;i++)
{
    handleEvent(events[n]);
}

首先通過create_epoll(int maxfds)來創建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,之後的所有操作 將通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個創建出來的epoll句柄。之後在你的網絡主循環裡面,每一幀的調用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的語法為:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中kdpfd為用epoll_create創建之後的句柄,events是一個 epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裡面將儲存所有的讀寫事件。 max_events是當前需要監聽的所有socket句柄數。最後一個timeout是 epoll_wait的超時,為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件范圍,為任意正整數的時候表示等這麼長的時間,如果一直沒 有事件,則范圍。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環 的效率。
既然epoll相比select這麼好,那麼用起來如何呢?會不會很繁瑣啊…先看看下面的三個函數吧,就知道epoll的易用了。

intepoll_create(int size);

生成一個Epoll專用的文件描述符,其實是申請一個內核空間,用來存放你想關注的socket fd上是否發生以及發生了什麼事件。size就是你在這個Epoll fd上能關注的最大socket fd數,大小自定,只要內存足夠。

int epoll_ctl(int epfd, intop, int fd, structepoll_event *event);

控制某個Epoll文件描述符上的事件:注冊、修改、刪除。其中參數epfd是epoll_create()創建Epoll專用的文件描述符。相對於select模型中的FD_SET和FD_CLR宏。

int epoll_wait(int epfd,structepoll_event * events,int maxevents,int timeout);

等待I/O事件的發生,返回發生事件數;

功能類似與select函數

參數說明:

參數 描述 epfd 由epoll_create() 生成的Epoll專用的文件描述符 epoll_event 用於回傳代處理事件的數組 maxevents 每次能處理的事件數 timeout 等待I/O事件發生的超時值

參考

我讀過最好的Epoll模型講解

Epoll模型詳解

通過完整示例來理解如何使用 epol

epoll 使用詳解

Copyright © Linux教程網 All Rights Reserved