epoll介紹【轉】

epoll是多路複用IO(I/O Multiplexing)中的一種方式,但是僅用於linux2.6以上內核,在開始討論這個問題之前,先來解釋一下爲什麼需要多路複用IO.

以一個生活中的例子來解釋.
假設你在大學中讀書,要等待一個朋友來訪,而這個朋友只知道你在A號樓,但是不知道你具體住在哪裏,於是你們約好了在A號樓門口見面.
如果你使用的阻塞IO模型來處理這個問題,那麼你就只能一直守候在A號樓門口等待朋友的到來,在這段時間裏你不能做別的事情,不難知道,這種方式的效率是低下的.
現在時代變化了,開始使用多路複用IO模型來處理這個問題.你告訴你的朋友來了A號樓找樓管大媽,讓她告訴你該怎麼走.這裏的樓管大媽扮演的就是多路複用IO的角色.
進一步解釋select和epoll模型的差異.
select版大媽做的是如下的事情:比如同學甲的朋友來了,select版大媽比較笨,她帶着朋友挨個房間進行查詢誰是同學甲,你等的朋友來了,於是在實際的代碼中,select版大媽做的是以下的事情:

int n = select(&readset,NULL,NULL,100);
for (int i = 0; n > 0; ++i)
{
   if (FD_ISSET(fdarray[i], &readset))
   {
      do_something(fdarray[i]);
      --n;
   }
}
 
epoll版大媽就比較先進了,她記下了同學甲的信息,比如說他的房間號,那麼等同學甲的朋友到來時,只需要告訴該朋友同學甲在哪個房間即可,不用自己親自帶着人滿大樓的找人了.於是epoll版大媽做的事情可以用如下的代碼表示:

在epoll中,關鍵的數據結構epoll_event定義如下:
typedef union epoll_data {
                void *ptr;
                int fd;
                __uint32_t u32;
                __uint64_t u64;
        } epoll_data_t;
        struct epoll_event {
                __uint32_t events;      /* Epoll events */
                epoll_data_t data;      /* User data variable */
        }; 
 
可以看到,epoll_data是一個union結構體,它就是epoll版大媽用於保存同學信息的結構體,它可以保存很多類型的信息:fd,指針,等等.有了這個結構體,epoll大媽可以不用吹灰之力就可以定位到同學甲.
別小看了這些效率的提高,在一個大規模併發的服務器中,輪詢IO是最耗時間的操作之一.再回到那個例子中,如果每到來一個朋友樓管大媽都要全樓的查詢同學,那麼處理的效率必然就低下了,過不久樓底就有不少的人了.
對比最早給出的阻塞IO的處理模型, 可以看到採用了多路複用IO之後, 程序可以自由的進行自己除了IO操作之外的工作, 只有到IO狀態發生變化的時候由多路複用IO進行通知, 然後再採取相應的操作, 而不用一直阻塞等待IO狀態發生變化了.
從上面的分析也可以看出,epoll比select的提高實際上是一個用空間換時間思想的具體應用.


我們目前的網絡模型大都是epoll的,因爲epoll模型會比select模型性能高很多, 尤其在大連接數的情況下,作爲後臺開發人員需要理解其中的原因。
select/epoll的特點
select的特點:select 選擇句柄的時候,是遍歷所有句柄,也就是說句柄有事件響應時,select需要遍歷所有句柄才能獲取到哪些句柄有事件通知,因此效率是非常低。但是如果連接很少的情況下, select和epoll的LT觸發模式相比, 性能上差別不大。
這裏要多說一句,select支持的句柄數是有限制的, 同時只支持1024個,這個是句柄集合限制的,如果超過這個限制,很可能導致溢出,而且非常不容易發現問題, TAF就出現過這個問題, 調試了n天,才發現:)當然可以通過修改linux的socket內核調整這個參數。
epoll的特點:epoll對於句柄事件的選擇不是遍歷的,是事件響應的,就是句柄上事件來就馬上選擇出來,不需要遍歷整個句柄鏈表,因此效率非常高,內核將句柄用紅黑樹保存的。
對於epoll而言還有ET和LT的區別,LT表示水平觸發,ET表示邊緣觸發,兩者在性能以及代碼實現上差別也是非常大的。
epoll的LT和ET的區別
LT:水平觸發,效率會低於ET觸發,尤其在大併發,大流量的情況下。但是LT對代碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有數據沒有被獲取,內核就不斷通知你,因此不用擔心事件丟失的情況。
ET:邊緣觸發,效率非常高,在併發,大流量的情況下,會比LT少很多epoll的系統調用,因此效率高。但是對編程要求高,需要細緻的處理每個請求,否則容易發生丟失事件的情況。
下面舉一個列子來說明LT和ET的區別(都是非阻塞模式,阻塞就不說了,效率太低):
採用LT模式下, 如果accept調用有返回就可以馬上建立當前這個連接了,再epoll_wait等待下次通知,和select一樣。
但是對於ET而言,如果accpet調用有返回,除了建立當前這個連接外,不能馬上就epoll_wait還需要繼續循環accpet,直到返回-1,且errno==EAGAIN,TAF裏面的示例代碼:
if(ev.events & EPOLLIN)
{
    do
    {
        struct sockaddr_in stSockAddr;
        socklen_t iSockAddrSize = sizeof(sockaddr_in);
        TC_Socket cs;
        cs.setOwner(false);
        //接收連接
        TC_Socket s;
        s.init(fd, false, AF_INET);
        int iRetCode = s.accept(cs, (struct sockaddr *) &stSockAddr, iSockAddrSize);
        if (iRetCode > 0)
        {
            ...建立連接
        }
        else
        {
            //直到發生EAGAIN纔不繼續accept
            if(errno == EAGAIN)
            {
                break;
            }
        }
    }while(true);
}
 
同樣,recv/send等函數, 都需要到errno==EAGAIN
從本質上講:與LT相比,ET模型是通過減少系統調用來達到提高並行效率的。
epoll ET詳解
ET模型的邏輯:內核的讀buffer有內核態主動變化時,內核會通知你, 無需再去mod。寫事件是給用戶使用的,最開始add之後,內核都不會通知你了,你可以強制寫數據(直到EAGAIN或者實際字節數小於 需要寫的字節數),當然你可以主動mod OUT,此時如果句柄可以寫了(send buffer有空間),內核就通知你。
這裏內核態主動的意思是:內核從網絡接收了數據放入了讀buffer(會通知用戶IN事件,即用戶可以recv數據)
並且這種通知只會通知一次,如果這次處理(recv)沒有到剛纔說的兩種情況(EAGIN或者實際字節數小於 需要讀寫的字節數),則該事件會被丟棄,直到下次buffer發生變化。
與LT的差別就在這裏體現,LT在這種情況下,事件不會丟棄,而是隻要讀buffer裏面有數據可以讓用戶讀,則不斷的通知你。
另外對於ET而言,當然也不一定非send/recv到前面所述的結束條件才結束,用戶可以自己隨時控制,即用戶可以在自己認爲合適的時候去設置IN和OUT事件:
1 如果用戶主動epoll_mod OUT事件,此時只要該句柄可以發送數據(發送buffer不滿),則epoll
_wait就會響應(有時候採用該機制通知epoll_wai醒過來)。
2 如果用戶主動epoll_mod IN事件,只要該句柄還有數據可以讀,則epoll_wait會響應。
這種邏輯在普通的服務裏面都不需要,可能在某些特殊的情況需要。 但是請注意,如果每次調用的時候都去epoll mod將顯著降低效率,已經喫過幾次虧了!
因此採用et寫服務框架的時候,最簡單的處理就是:
建立連接的時候epoll_add IN和OUT事件, 後面就不需要管了
每次read/write的時候,到兩種情況下結束:
1 發生EAGAIN
2 read/write的實際字節數小於 需要讀寫的字節數
對於第二點需要注意兩點:
A:如果是UDP服務,處理就不完全是這樣,必須要recv到發生EAGAIN爲止,否則就丟失事件了
因爲UDP和TCP不同,是有邊界的,每次接收一定是一個完整的UDP包,當然recv的buffer需要至少大於一個UDP包的大小
隨便再說一下,一個UDP包到底應該多大?
對於internet,由於MTU的限制,UDP包的大小不要超過576個字節,否則容易被分包,對於公司的IDC環境,建議不要超過1472,否則也比較容易分包。
B 如果發送方發送完數據以後,就close連接,這個時候如果recv到數據是實際字節數小於讀寫字節數,根據開始所述就認爲到EAGIN了從而直接返回,等待下一次事件,這樣是有問題的,close事件丟失了!
因此如果依賴這種關閉邏輯的服務,必須接收數據到EAGIN爲止,例如lb。

本文收集自:
Vimer的程序世界
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章