epoll 2 使用及原始碼分析的引子

2021-09-29 01:16:59 字數 3602 閱讀 2743

本文**取自核心版本 4.17

epoll(2) - i/o 事件通知設施。

epoll 是核心在2.6版本後實現的,是對 select(2)/poll(2) 更高效的改進,同時它自身也是一種檔案,不恰當的比方可以看作 eventfd + poll。

多路復用也是一直在改進的,經歷的幾個階段

select(2) - 只能關注 1024 個檔案描述符,並且範圍固定在 0 - 1023,每次函式呼叫都需要把所有關注的資料複製進核心空間,再對所有的描述符集合進行遍歷判斷。

poll(2) - 改進 select(2) 前面兩個缺點,可以自定義關注的描述符,數量也不受限制(不超過系統的限制),每次呼叫同樣需要複製所有的事件進核心空間,全部遍歷。

epoll(2) - 不需要每次呼叫時所有關注的檔案描述符進行核心-使用者空間的複製,而是直接將所有的檔案描述符和事件常駐核心空間,同時也不需要每次遍歷所有檔案描述符。

#include typedef union epoll_data  epoll_data_t;

struct epoll_event ;

int epoll_create(int size);

int epoll_create1(int flags);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events,

int maxevents, int timeout);

epoll_create() - 用來建立乙個 epoll 例項,返回乙個新的檔案描述符。第乙個引數@size自 2.6.8 開始無意義,但必須大於 0。

epoll_create1() - 引數@flags為 0 則等效於 epoll_create(),flags 可以為 epoll_cloexec, 在exec新程式時關閉檔案描述符。

epoll_ctl() - epoll 的控制介面,使用者呼叫該系統呼叫來控制監聽的檔案描述符。引數@epfd為 epoll_create() 返回的新檔案描述符,引數@op為 epoll_ctl() 提供的控制操作:

epoll_wait() - 等待epoll中監聽檔案描述符就緒的 i/o 事件。引數@epfd為epoll例項對應的檔案描述符,由 epoll_create() 建立,、

引數@events為就緒的事件集合的位址,引數@maxevents為需要就緒事件集合的大小,必須大於 0,引數@timeout為超時時間,單位為 微秒。

epoll 預設使用水平觸發模式,邊緣觸發模式需要設定events |= epollet

邊緣觸發模式的特點是邊緣觸發模式只在關注的檔案描述符發生改變時才產生就緒的事件,考慮高低電平的,邊緣是有乙個瞬間的概念,而水平則有乙個持續的狀態。

這就導致了,邊緣觸發有可能會丟失需要通知的事件。分析如下

現有 5 個步驟:

管道讀端的檔案描述符 rfd 被註冊到 epoll 例項中。

管道寫端寫入了 2 kb 資料。

呼叫 epoll_wait(2) 返回了 rfd 作為就緒的檔案描述符。

管道讀端讀取了 1 kb 資料。

呼叫 epoll_wait(2)。

如果檔案描述符 rfd 使用epollet邊緣觸發模式註冊到 epoll 中,那麼在執行上面的5的時候,儘管管道的讀端緩衝區還有資料,epoll_wait(2) 還是可能會掛起,

同時寫端可能會基於其已傳送的資料期望響應。產生這個情況的原因是邊緣觸發模式只在關注的檔案描述符發生改變時才產生就緒的事件。在上面的步驟中,2寫入的資料,

因此在 rdf 上生成乙個事件,由於在4中的讀取操作不會消耗整個緩衝區資料,故在5對 epoll_wait(2) 呼叫可能發生阻塞。

使用邊緣觸發模式的程式應該使用非阻塞檔案描述符來避免阻塞讀寫造成處理多個檔案描述符時產生的飢餓問題。

所以建議使用的邊緣觸發模式時遵從一下兩點:

檔案描述符為非阻塞方式,並且

只在 read(2) / write(2) 返回eagain後進行等待。

在使用邊緣觸發模式時,在接收到多個資料塊的時候會產生多個事件,因此使用者可以選擇指定epolloneshot標誌,在 epoll_wait(2) 收到事件後禁用相關的檔案描述符。

而設定epolloneshot標誌後,需要使用者手動呼叫 epoll_ctl(2) 重新設定檔案描述符。

在示例**中可以看到邊緣觸發和水平觸發的區別

把 eventfd 註冊到 epoll 中,進行兩個執行緒間的通訊。使用 eventfd 的efd_semaphore的標誌來模擬 read(2) 讀取部分資料。

程式初始值設定 1000,在水平模式下,會先用 1000 次 read(2),把計數器的值消耗為 0,之後 write(2) 寫入 cnt,就呼叫 cnt 次 read(2),總之只要水位(count)不為0就可讀。

而 epoll 設定epollet後只有發生了 write(2) 操作 epoll_wait(2) 才能產生乙個可讀事件,而計數器則是逐漸增大。

#include #include #include #include #include #include int efd;

void *run_eventfd_write(void *arg)

}int main() else if (ret == 0)

}}單次命中,核心2.6.2引入,當事件就緒被 epoll_wait(2) 返回時,這個事件就不再被關注了。

核心3.5引入,如果epolloneshotepollet標誌被清除後,並且程序擁有cap_block_suspend(阻止系統掛起的特性)許可權,這個標誌能夠保證事件在掛起或者處理的時候,系統不會掛起或休眠。

本文不準備把原始碼分析放在這裡,由於是檔案的原因 epoll(2) 對比 poll(2) 和 select(2) 來說要複雜很多,這裡丟擲幾個點來引出原始碼分析的重點:

epoll(2) 得到就緒事件的複雜度為何是 \(o(1)\)

epoll(2) 和普通的檔案相比的區別在**,比如和 eventfd(2) 比較

epoll(2) 相對 poll(2)/select(2) 多提供了epollet的觸發模式,現象在上面可以看到區別,實現是如何做到的。

epoll(2) 相互關注時,有就緒事件到來會產生相互喚醒的問題,為何會出現這樣的問題

對於問題4,核心是如何解決這種相互喚醒的問題。

剛開始把原始碼分析放了出來,但是發現寫的過於混亂沒有重點,**貼的太多,所以想專門寫一文來著分析上面的問題。

Spring MVC 的原始碼分析 2

spring mvc 如何在servlet容器中處理業務的?我們依次檢視原始碼中三個方法的具體內容 1 requestmethod.patch都包含那些列舉型別呢?public enum requestmethod 2 processrequest方法是如何進行分流的呢?3 當所有的method方法...

iptables原始碼分析(2)

1.1 表的查詢 再回到iptc init 函式上來,它根據表名,從核心獲取對應的表的相關資訊,handle是乙個iptc handle t型別的指標,在libiptc.c中,有如下定義 transparent handle type.typedef struct iptc handle iptc ...

Leveldb原始碼分析 2

輕鬆一刻,前面約定中講過leveldb使用了很多varint型編碼,典型的如後面將涉及到的各種key。其中的編碼 解碼函式分為varint和fixedint兩種。int32和int64操作都是類似的。首先是fixedint編碼,直接上 很簡單明瞭。void encodefixed32 char bu...