epoll 邊沿觸發和水平觸發

1. epoll 邊沿觸發和水平觸發

對於epoll有兩種觸發模式:水平觸發LT和邊緣觸發ET,其中邊緣觸發必須需要設置所監聽的socket爲non_blocking。
邊緣觸發,顧名思義,不到邊緣情況,是死都不會觸發的。

EPOLLOUT事件:
EPOLLOUT事件在連接時建立時首先觸發觸發一次,表示可寫,其他時候的觸發條件爲:
1.某次write,寫滿了發送緩衝區,返回錯誤碼爲EAGAIN。
2.對端讀取了一些數據,又重新可寫了,此時會觸發EPOLLOUT。

簡單地說:EPOLLOUT事件只有socket從unwritable變爲writable時,纔會觸發一次。對於EPOLLOUT事件,必須要將該文件描述符緩衝區一直寫滿,讓 errno 返回 EAGAIN 爲止,或者發送完所有數據爲止。

EPOLLIN事件:
EPOLLIN事件則只有當對端有數據寫入時纔會觸發,所以觸發一次後需要不斷讀取所有數據直到讀完EAGAIN爲止,否則剩下的數據只有在下次對端有寫入時才能一起取出來了。

設想這樣一個場景:接收端接收完整的數據後會向對端發送應答報文,對端纔會繼續向接收端發送數據,從而觸發下一次的EPOLLIN,而這時沒有讀完socket緩衝區中的所有數據,導致接收端無法向對端發送應答報文,而對端沒有收到應答報文,也就不會再發送數據觸發下一次的EPOLLIN,而沒有下一次的EPOLLIN事件,接收端也就永遠不知道此socket緩衝區中還有未讀出的數據。(一個完美的死循環)

簡單的說:EPOLLIN事件只有對端新數據寫入時,纔會觸發一次。對於EPOLLIN事件,必須要將該文件描述符一直讀到空,讓 errno 返回 EAGAIN 爲止。

總結:現在明白爲什麼說epoll要求異步socket了吧?
如果你的文件描述符如果不是非阻塞的.

  1. 對於讀:由於需要一直讀直到把數據讀完,所以大家在編寫程序的時候一般會用一個循環一直讀取socket,那這個循環勢必會在最後一次阻塞,即沒有數據可讀的情況下,阻塞式socket會在數據讀完之後一直阻塞下去,而非阻塞式的socket則返回<0,並讓errno 返回 EAGAIN 。
  2. 對於寫,當使用阻塞式socket時,socket的unwritable/writable狀態變化沒有任何意義!!因爲此時無論發送多大的數據write總是會阻塞直到所有數據都發送出去。(也就是說,邊緣觸發的epoll如果不和非阻塞的socket搭配,使用起來會產生問題)

2. 使用epoll是否需要將socket設置爲nonblocking?

取決於你使用的觸發方式, 如果你使用水平觸發(Level-triggered) 那麼此時的epoll相當於高級的select, 你的論述是對的, 是不需要一定將socket設置爲非阻塞的; 然而, 當你使用邊緣觸發(Edge-triggered) 那麼此時從業務的完整性考慮, 是建議將socket設置爲nonbocking模式, 並且在讀寫觸發EAGAIN之後再進行epoll_wait.

水平觸發和邊緣觸發, 類似於數字電路當中的電位水平, 從低電平到高電平的瞬間觸發動作叫邊緣觸發, 而處於高電平觸發動作叫做水平觸發。想象這樣一個場景:有一個pipe描述符 fd 按順序發生瞭如下的動作:

  1. 讀端的 fd 被註冊到一個epoll的描述符當中, 監聽讀信號, 此時pipe中沒有消息, 無論是邊緣觸發還是水平觸發此刻都不會被觸發
  2. fd 的寫端被寫入2kb數據
  3. 讀端調用epoll_wait, 返回 fd, 此刻pipe中有2kb數據, 並且從不可讀變爲可讀, 所以邊緣觸發和水平觸發都會返回
  4. 讀端讀取1kb的數據
  5. 讀端繼續調用epoll_wait在第五步的時候, 邊緣觸發和水平觸發的差異就顯現出來了, 此時pipe中仍然有數據,所以水平觸發的epoll會立刻返回, 但是邊緣觸發的epoll_wait 並不會返回, 因爲此時pipe一直可讀, 並沒有從不可讀變爲可讀狀態所以這裏就會出現一個問題, 如果寫端在等讀端處理完數據返回, 而讀端卻在等寫端的2kb數據中的另外1kb, 雙方就會產生死鎖。 因此, 在使用邊緣觸發的時候, 建議將描述符設置爲nonblocking, 並且在read/write產生EAGAIN的錯誤之後再使用epoll_wait
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章