1. 程式人生 > 其它 >移除類名沒有觸發transition_epoll邊緣觸發模式

移除類名沒有觸發transition_epoll邊緣觸發模式

技術標籤:移除類名沒有觸發transition

epoll(kqueue),支援兩種事件觸發模式。水平觸發以及邊緣觸發。

epoll實際可以監聽多種描述符,下文主要以套接字介紹,並且假設同時註冊了讀/寫。

  • 水平觸發:只要套接字可讀/可寫epollwait都會將描述符返回。即只要套接字的接收緩衝中尚有資料或傳送緩衝有空間容納要傳送的資料。這個套接字都會被epoll_wait返回。
  • 邊緣觸發:當套接字的緩衝狀態發生變化時返回。對於讀緩衝,有新到達的資料被新增到讀緩衝時觸發。對於寫緩衝,當緩衝發生容量變更的時候觸發(對端確認分組,核心刪除已經確認的分組,空出空間,寫緩衝容量發生變更)。

對於邊緣觸發,很多人,包括我自己,一個開始對邊緣這個定義並不是太清楚。例如對於讀緩衝,一直以為邊緣的意思是,緩衝從空到有資料。也就是以為只有當讀緩衝從空到第一次接到資料的時候才會觸發一次,如果不把資料讀空,那麼後續資料到達時不會再次觸發。對於寫緩衝,我之前的理解是當緩衝從滿到有空閒空間時觸發。

下面是linux核心中對epoll readylist的處理介紹。

fs/eventpoll.c

判斷一個tcp套接字上是否有啟用事件:net/ipv4/tcp.c:tcp_poll函式

epoll_wait返回readylist中的fd。eventpoll.rdllist

epitem是與epollfd關聯的.如果一個fd被加入到多個epollfd中每個epollfd都會為它建立一個epitem。

下面看下readylist是如何返回到使用者空間的。

epoll_wait通過ep_send_events把fd返回到使用者空間。ep_send_events呼叫ep_scan_ready_list,ep_scan_ready_list呼叫ep_send_events_proc。在ep_send_events_proc裡,將readylist中的epitem一一出列,如果其上確實有使用者關注的事件啟用,將其新增到使用者空間傳入的陣列中。新增完之後,如果epitem非EPOLLONESHOT,非EPOLLET,會重新將epitem添加回readylist中。供下次epoll_wait時處理。對於EPOLLONESHOT,關注事件將被全部清空,需要使用者重新註冊事件。

可見,對於水平觸發且沒有設定的EPOLLONESHOT fd,epoll_wait返回之前會將fd重新新增到readylist中。如果一個fd只註冊了in事件,並在epoll_wait返回之後將這個fd的讀緩衝讀空(假設讀空之後再沒有後續資料到來)。這個fd也會一直儲存在readylist中,直到下一次呼叫epoll_wait發現它沒有啟用的關注事件從readylist中移除。

下面看下epitem在哪幾種情況下會被主動加入readylist。

  1. EPOLL_ADD:在ep_insert中處理,如果fd上有關注的事件啟用,將epitem加入readylist
  2. EPOLL_MOD:在ep_modify中處理,如果fd上有關注的事件啟用,將epitem加入readylist
  3. ep_poll_callback:fd有事件到達,如果到達事件是被關注的事件,將epitem加入readylist

實際上,在linux下,如果用邊緣觸發同時註冊了讀和寫,當讀觸發的時候,核心向用戶返回fd的時候同時會檢查fd是否符合可寫的條件(有空間容納待寫入的資料),如果滿足可寫的條件,同時會加上EPOLLOUT標記。在這一點上,mac 下 kqueue的實現更符合邊緣觸發的描述。

邊緣觸發可能造成飢餓

邊緣模式在什麼情況下可能會造成飢餓。我們考慮一個應用,從外來連線接收資料,然後對資料進行處理。如果用邊緣觸發處理,對一個套接字就需要迴圈讀取,直到沒有資料可讀為止(通過返回EAGIN,實際上如果讀取的資料小於提供的緩衝大小也可以做這樣的斷定)。如果其中一個連線源源不斷的傳送資料,這個套介面的讀迴圈就無法退出,導致其它連線沒有機會被處理。

為了避免這種情況的出現,epoll_wait返回時,並不直接處理套接字,而是將套接字新增到active list中。等到把epoll_wait返回的套接字都新增完後,再對active list中的套接字執行io處理。

對於active list中的每個元素,如果判定套接字不可讀,則從active list中移除。否則最多執行有限次操作,超過次數之後將套接字保留在active list中,處理後續套接字。

虛擬碼如下:

func 

邊緣觸發模式是否比水平觸發模式更高效

對於這個問題,我實際測試過兩種模式,並沒有發現肉眼可見的效率差異。

兩種模式執行epoll_wait的次數是大致相當的,區別是readylist大小會有所不同,

對於只從套接字接收資料而不傳送資料的應用來說。考慮以下處理模式:

每次對一個fd只執行一read,然後處理接收到的資料。

如果每次呼叫epoll_wait前,所有fd都會接收到資料,那麼無論哪種觸發模式,readylist都是所有被監聽的fd。開銷完全一樣。

邊緣觸發只有在下述情形下才能獲得優勢:

假如有X個連線,套接字接收緩衝為N,對端每次傳送都能將N填滿,read時提供的使用者緩衝為N/M。且傳送端的傳送頻率是接收端處理頻率的1/N/M,即讀端正好消費完N位元組後寫端菜繼續傳送。 在這種模式下水平模式每一輪epoll_wait的readylist都是X。而邊緣模式則以N/M為週期,除了週期第一次為X,後續都是N/M-1次0(因為無外來資料到達,不會被新增到readylist中)。

下面再看下發送的情況。對於out事件,水平觸發返回的條件是傳送緩衝有空間,邊緣觸發的條件是傳送緩衝容量變更(對端確認資料包,傳送端釋放傳送緩衝中的空間)。

對於水平觸發模式out事件必須按需註冊。主要的註冊方式有以下兩種:

  • 上層呼叫send,將資料新增到應用層的傳送緩衝,如果當前沒有註冊out則註冊out,當epoll通知out啟用時,傳送應用緩衝中的資料,如果資料傳送完畢登出out。
  • 上層呼叫send,直接傳送,如果資料未傳送完或返回EAGAIN,則註冊out,當epoll通知out啟用時,繼續傳送未傳送完成的資料,如果資料傳送完畢登出out。

上述的新增和登出out都是通過epoll_ctl完成。邊緣觸發模式則無此需要。

我這裡主要分析第二種模式。

假如傳送每次均能將資料全部發完。那麼out的註冊和登出都不會發生。

如果接收方慢導致每次均無法將資料全部發送完,那麼out將只會註冊一次,登出不會發生。因為上層的send會導致待發送佇列無法排空。

導致out的註冊和登出頻繁發生的情況只有一種,傳送端每次send請求傳送M位元組,但實際只能傳送M/N位元組,因此註冊out,每次out觸發後都能傳送M/N位元組,直到M位元組全部發送完畢登出out。在這種情況下,在所有有M位元組傳送完畢前上層不會觸發新的send請求。對於這種情況無論哪種模式每次呼叫epoll_wait,readylist大小都是一樣的。開銷的差距只會體現在用epoll_ctl註冊和登出out上。

因此,邊緣觸發在機制上應該是更高效的,但是構造出一個能明顯看出效率優勢的場景並不容易。

下面貼一下我的一個多執行緒邊緣觸發模式的實驗網路庫,支援linux epoll和mac kqueue。區別於muduo的loop per thread模式。這個實驗庫只啟動一個執行緒監聽epoll/kqueue。當套接字被啟用後將其交給io執行緒執行實際的io。

sniperHW/hwnet​github.com dfb56e2d293216cb182469cc69aa3134.png