本篇是多路復用的第五篇,主要來講解epoll的水平觸發和邊緣觸發是怎么回事。
一、概念介紹
EPOLL事件有兩種模型,水平出發和邊緣觸發,如下所示:
1. Level Triggered (LT) 水平觸發
1. socket接收緩沖區不為空 有數據可讀 讀事件一直觸發2. socket發送緩沖區不滿 可以繼續寫入數據 寫事件一直觸發備注:符合思維習慣,epoll_wait返回的事件就是socket的狀態
例子介紹:
1.?accept一個連接,添加到epoll中監聽EPOLLIN事件2.?當EPOLLIN事件到達時,read?fd中的數據并處理3.?當需要寫出數據時,把數據write到fd中;如果數據較大,無法一次性寫出,那么在epoll中監聽EPOLLOUT事件4.?當EPOLLOUT事件到達時,繼續把數據write到fd中;如果數據寫出完畢,那么在epoll中關閉EPOLLOUT事件
2. Edge Triggered (ET) 邊沿觸發
1. socket的接收緩沖區狀態變化時觸發讀事件,即空的接收緩沖區剛接收到數據時觸發讀事件2. socket的發送緩沖區狀態變化時觸發寫事件,即滿的緩沖區剛空出空間時觸發讀事件備注:僅在狀態變化時觸發事件
例子介紹:
1.?accept一個一個連接,添加到epoll中監聽EPOLLIN|EPOLLOUT事件2.?當EPOLLIN事件到達時,read?fd中的數據并處理,read需要一直讀,直到返回EAGAIN為止3.?當需要寫出數據時,把數據write到fd中,直到數據全部寫完,或者write返回EAGAIN4.?當EPOLLOUT事件到達時,繼續把數據write到fd中,直到數據全部寫完,或者write返回EAGAIN
3.LT和ET兩者比較:
1. 從ET的處理過程中可以看到,ET的要求是需要一直讀寫,直到返回EAGAIN,否則就會遺漏事件。ET的編程可以做到更加簡潔,某些場景下更加高效,但另一方面容易遺漏事件,容易產生bug。2. LT的處理過程中,直到返回EAGAIN不是硬性要求,但通常的處理過程都會讀寫直到返回EAGAIN,但LT比ET多了一個開關EPOLLOUT事件的步驟。LT的編程與poll/select接近,符合一直以來的習慣,不易出錯。
二?、內核調度實現方式
在epoll_wait的時候,阻塞等待事件發生, 事件發生時通過回調掛到ready list鏈表中
epoll_wait返回, 處理ready list, 返回事件給調用者
此時ET模式已經將事件從ready list中刪除,LT模式中還存在
此時假設應用程序處理完了事件, 再次epoll_wait. ?ET模式繼續阻塞
LT模式由于ready list中依然存在事件則不會阻塞, 對這些socket調用poll方法獲取最新的事件信息,如果確認沒事件了才會刪除。
三、?水平觸發和邊緣觸發的常見問題
1. 水平觸發的問題:不必要的喚醒
內核:收到一個新建連接的請求
內核:由于 “驚群效應” ,喚醒兩個正在 epoll_wait() 的線程 A 和線程 B
線程A:epoll_wait() 返回
線程B:epoll_wait() 返回
線程A:執行 accept() 并且成功
線程B:執行 accept() 失敗,accept() 返回 EAGAIN
2. 邊緣觸發的問題:不必要的喚醒以及饑餓
1)不必要的喚醒:
1.內核:收到第一個連接請求。線程 A 和 線程 B 兩個線程都在 epoll_wait() 上等待。由于采用邊緣觸發模式,所以只有一個線程會收到通知。這里假定線程 A 收到通知2.線程A:epoll_wait() 返回3.線程A:調用 accpet() 并且成功4.內核:此時 accept queue 為空,所以將邊緣觸發的 socket 的狀態從可讀置成不可讀5.內核:收到第二個建連請求6.內核:此時,由于線程 A 還在執行 accept() 處理,只剩下線程 B 在等待 epoll_wait(),于是喚醒線程 B。7.線程A:繼續執行 accept() 直到返回 EAGAIN8.線程B:執行 accept(),并返回 EAGAIN,此時線程 B 可能有點困惑(“明明通知我有事件,結果卻返回 EAGAIN”)9.線程A:再次執行 accept(),這次終于返回 EAGAIN
2)饑餓:
1.內核:接收到兩個建連請求。線程 A 和 線程 B 兩個線程都在等在 epoll_wait()。由于采用邊緣觸發模式,只有一個線程會被喚醒,我們這里假定線程 A 先被喚醒2.線程A:epoll_wait() 返回3.線程A:調用 accpet() 并且成功4.內核:收到第三個建連請求。由于線程?A?還沒有處理完(沒有返回?EAGAIN),當前?socket?還處于可讀的狀態,由于是邊緣觸發模式,所有不會產生新的事件5.線程A:繼續執行 accept() 希望返回 EAGAIN 再進入 epoll_wait() 等待,然而它又 accept() 成功并處理了一個新連接6.內核:又收到了第四個建連請求7.線程A:又繼續執行 accept(),結果又返回成功
參考文檔:
https://blog.csdn.net/dongfuye/article/details/50880251
https://www.zhihu.com/question/20502870
https://blog.lucode.net/linux/epoll-tutorial.html
https://plantegg.github.io/2019/12/09/epoll%E7%9A%84LT%E5%92%8CET/