LT和ET概念簡述:
LT模式:
當epoll_wait檢測到監(jiān)聽文件描述符上有事件發(fā)生時通知應用程序,應用程序可以不理解處理該事件,下次調用epoll_wait時該事件還會被通告直到該事件被處理。
LT是系統默認, 工作在這種方式下, 程序員不易出問題, 在接收數據時,只要socket輸入緩存有數據,都能夠獲得EPOLLIN的持續(xù)通知, 同樣在發(fā)送數據時, 只要發(fā)送緩存夠用, 都會有持續(xù)不間斷的EPOLLOUT通知。
ET模式:
當epoll_wait檢測到事件發(fā)生告知應用程序后應用程序必須立即處理該事件,后續(xù)的epoll_wait將不會再向應用程序告知這一事件。
而對于ET是另外一種觸發(fā)方式, 比EPOLLLT要高效很多, 對程序員的要求也多些, 程序員必須小心使用,因為工作在此種方式下時, 在接收數據時, 如果有數據只會通知一次, 假如read時未讀完數據,那么不會再有EPOLLIN的通知了, 直到下次有新的數據到達時為止; 當發(fā)送數據時, 如果發(fā)送緩存未滿也只有一次EPOLLOUT的通知, 除非你把發(fā)送緩存塞滿了, 才會有第二次EPOLLOUT通知的機會, 所以在此方式下read和write時都要處理好。
LT和ET的相同點:
都是通過epoll_wait從EPOLL等待隊列讀取激活事件。
LT和ET的區(qū)別:
水平觸發(fā)LT和邊緣觸發(fā)ET的區(qū)別:只要句柄滿足某種狀態(tài),水平觸發(fā)就會發(fā)出通知;而只有當句柄狀態(tài)改變時,邊緣觸發(fā)才會發(fā)出通知。也就是說LT模式下只要某個socket處于readable/writable狀態(tài),無論什么時候進行epoll_wait都會返回該socket;而ET模式下只有某個socket從unreadable變?yōu)閞eadable或從unwritable變?yōu)閣ritable時,epoll_wait才會返回該socket。在epoll的ET模式下,正確的讀寫方式為:讀:只要可讀,就一直讀,直到返回0,或者 errno = EAGAIN;寫:只要可寫,就一直寫,直到數據發(fā)送完,或者 errno = EAGAIN。
LT模式:
- 水平觸發(fā),效率會低于ET觸發(fā),尤其在大并發(fā),大流量的情況下。但是LT對代碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有數據沒有被獲取,內核就不斷通知你,因此不用擔心事件丟失的情況。LT(level triggered)是缺省的工作方式,并且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續(xù)通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。LT模式的時候,epoll_wait 會把有事件的 file 再次加到 rdllist 列表中,以便下次epoll_wait可以再檢查一遍。
- LT 模式下,狀態(tài)不會丟失,程序完全可以于 epoll 來驅動。
- 對于長連接,大數據包應用,因為 LT模式只能設置當時感興趣的事件(如果不寫數據也設置POLLOUT的話,會導致cpu 100%) ,所以要頻繁調用epoll_ctl,內核也要多次操作鏈表,所以效率會比ET模式低。
- 相對于ET,LT可以減少系統調用次數。
ET模式: - 邊緣觸發(fā),效率非常高,在并發(fā),大流量的情況下,會比LT少很多epoll的系統調用,因此效率高。但是對編程要求高,需要細致的處理每個請求,否則容易發(fā)生丟失事件的情況。ET(edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變?yōu)榫途w時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,并且不會再為那個文件描述符發(fā)送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態(tài)了(比如,你在發(fā)送,接收或者接收請求,或者發(fā)送接收的數據少于一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發(fā)送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。
- ET模式下,程序首先要自己驅動邏輯,如果遇到 EAGAIN錯誤的時候,就要依賴epoll_wait來驅動,這時epoll幫助程序從阻礙中脫離。
- ET 邊沿觸發(fā)的 邊沿是 AGAIN錯誤,屬于下降沿觸發(fā)。
- ET 的驅動事件依靠 socket的 sk_sleep 等待隊列喚醒,這只有在有新包到來才能發(fā)生,數據包導致POLLIN, ACK確認導致 sk_buffer destroy從而導致POLLOUT, 但這不是一對一得關系,是多對一(多個網絡包產生一個POLLIN, POLLOUT事件)。
為什么epoll默認是LT?
LT(level triggered):LT是缺省的工作方式,并且同時支持block和no-block socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續(xù)通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。
ET常見錯誤:
recv到了合適的長度, 程序處理完畢后就epoll_wait
這時程序可能長期阻塞,因為這時socket的 rev_buffer里還有數據,或對端close了連接,但這些信息都在上次通知了你,你沒有處理完,就epoll_wait了
正確做法是:
recv到了合適的長度, 程序處理; 再次recv, 若果是EAGAIN則epoll_wait。
使用ET模式時, 程序自己驅動下,會發(fā)生socket被關閉的情況,這時要處理EPIPE信號和返回值。(如果不處理EPIPE那么會導致程序core掉)
總結:ET 使用準則,只有出現EAGAIN錯誤才調用epoll_wait。
通常的誤區(qū)
通常的誤區(qū)是,level-trigger 模式在 epoll 池中存在大量 fd 時效率要顯著低于 edge-trigger 模式。但從 kernel 代碼來看,edge-trigger/level-trigger 模式的處理邏輯幾乎完全相同,差別僅在于 level-trigger 模式在 event 發(fā)生時不會將其從 ready list 中移除,略為增大了 event 處理過程中 kernel space 中記錄數據的大小。
然而,edge-trigger 模式一定要配合 user app 中的 ready list 結構,以便收集已出現 event 的 fd,再通過 round-robin 方式挨個處理,以此避免通信數據量很大時出現忙于處理熱點 fd 而導致非熱點 fd 餓死的現象。統觀 kernel 和 user space,由于 user app 中 ready list 的實現千奇百怪,不一定都經過仔細的推敲優(yōu)化,因此 edge-trigger 的總內存開銷往往還大于 level-trigger 的開銷。
一般號稱 edge-trigger 模式的優(yōu)勢在于能夠減少 epoll 相關系統調用,這話不假,但 user app 里可不是只有 epoll 相關系統調用吧?為了繞過餓死問題,edge-trigger 模式的 user app 要自行進行 read/write 循環(huán)處理,這其中增加的系統調用和減少的 epoll 系統調用加起來,有誰能說一定就能明顯地快起來呢?
實際上,epoll_wait 的效率是 O(ready fd num) 級別的,因此 edge-trigger 模式的真正優(yōu)勢在于減少了每次epoll_wait 可能需要返回的 fd 數量,在并發(fā) event 數量極多的情況下能加快 epoll_wait 的處理速度,但別忘了這只是針對 epoll 體系自己而言的提升,與此同時 user app 需要增加復雜的邏輯、花費更多的 cpu/mem 與其配合工作,總體性能收益究竟如何?只有實際測量才知道,無法一概而論。不過,為了降低處理邏輯復雜度,常用的事件處理庫大部分都選擇了 level-trigger 模式。
epoll 的 edge-trigger 和 level-trigger 模式處理邏輯差異極小,性能測試結果表明常規(guī)應用場景 中二者性能差異可以忽略;使用 edge-trigger 的 user app 比使用 level-trigger 的邏輯復雜,出錯概率更高;edge-trigger 和 level-trigger 的性能差異主要在于 epoll_wait 系統調用的處理速度,是否是 user app 的性能瓶頸需要視應用場景而定,不可一概而論。
事件放入EPOLL等待隊列條件
對于讀事件:
- 從不可讀變?yōu)榭勺x;
- 有新數據到來;
- 有老數據,并且通過epoll_ctl設置EPOLL_CTL_MOD(ET模式);
- 數據未讀完(LT模式)。
對于寫事件: - 從不可寫變?yōu)榭蓪懀?/li>
- 有新的可寫數據出現;
- 數據可寫,并且通過epoll_ctl設置EPOLL_CTL_MOD(ET模式);
- 數據可寫(LT模式)。
LT模式ET模式存在的問題
LT模式問題:
如果可讀或可寫事件未處理,會頻繁激活未處理事件。
解決方法:
不想處理讀寫事件時, 從EPOLL中移除句柄。需要處理時再加入EPOLL。
ET模式問題:
如果可讀或可寫沒有全部處理,會有老數據殘留。要等待新數據到來。
解決方法:
- 循環(huán)讀取或寫入數據,一直到EAGAIN或EWOULDBLOCK;
- 讀取或者寫入數據后,通過epoll_ctl設置EPOLL_CTL_MOD,激活未處理事件。
accept 要考慮 的兩個問題
(1) 阻塞模式 accept 存在的問題
考慮這種情況:TCP連接被客戶端夭折,即在服務器調用accept之前,客戶端主動發(fā)送RST終止連接,導致剛剛建立的連接從就緒隊列中移出,如果套接口被設置成阻塞模式,服務器就會一直阻塞在accept調用上,直到其他某個客戶建立一個新的連接為止。但是在此期間,服務器單純地阻塞在accept調用上,就緒隊列中的其他描述符都得不到處理。 解決辦法是把監(jiān)聽套接口設置為非阻塞,當客戶在服務器調用accept之前中止某個連接時,accept調用可以立即返回-1,這時源自Berkeley的實現會在內核中處理該事件,并不會將該事件通知給epool,而其他實現把errno設置為ECONNABORTED或者EPROTO錯誤,我們應該忽略這兩個錯誤。
(2)ET模式下accept存在的問題
考慮這種情況:多個連接同時到達,服務器的TCP就緒隊列瞬間積累多個就緒連接,由于是邊緣觸發(fā)模式,epoll只會通知一次,accept只處理一個連接,導致TCP就緒隊列中剩下的連接都得不到處理。 解決辦法是用while循環(huán)抱住accept調用,處理完TCP就緒隊列中的所有連接后再退出循環(huán)。如何知道是否處理完就緒隊列中的所有連接呢?accept返回-1并且errno設置為EAGAIN就表示所有連接都處理完。
綜合以上兩種情況,服務器應該使用非阻塞地accept
參考鏈接:
epoll的EPOLLLT模式和EPOLLET模式比較
select、poll、epoll總結及ET、LT區(qū)別
epoll的LT和ET模式
epoll模型中LT、ET模式分析
ET/LT模式區(qū)別
epoll的兩種模式:LT和ET模式