I/O多路複用之epoll

一、select、poll的些許缺點

先回憶下select和poll的接口html

int select(int nfdsfd_set *readfdsfd_set *writefdsfd_set *exceptfds, struct timeval *timeout);linux

int poll(struct pollfd *fds, nfds_t nfds, int timeout);面試

這兩個多路複用實現的特色是:編程

  • 每次調用select和poll都要把用戶關心的事件集合(select爲readfds,writefds,exceptfds集合,poll爲fds結構體數組)從用戶空間到內核空間。
  • 若是某一時間段內,只有少部分事件是活躍的(用戶關心的事件集合只有少部分事件會發生),會浪費cpu在對無效事件輪詢上,使得效率較低,好比,用戶關心1024個tcp socket的讀事件,當是,每次調用select或poll時只有1個tcp連接是活躍的,那麼對其餘1023個事件的輪詢是沒有必要的。

select支持的文件描述符數量較小,通常只有1024,poll雖然沒有這個限制,但基於上面兩個緣由,poll和select存在一樣一個缺點,就是包含大量文件描述符的數組被總體複製於用戶態和內核的地址空間之間,並且不論這些文件描述符是否就緒,每次都會輪詢全部描述符的狀態,使得他們的開銷隨着文件描述符數量的增長而線性增大。epoll針對這幾個缺點進行了改進,再也不像select和poll那樣,每次調用select和poll都把描述符集合拷貝到內核空間,而是一次註冊永久使用;另外一方面,epoll也不會對每一個描述符都輪詢時間是否發生,而是隻針對事件已經發生的文件描述符進行資源搶佔(由於同一個描述符資源(如可讀或可寫)可能阻塞了多個進程,調用epoll的進程須要與這些進程搶佔該相應資源)。下面記錄一下本身對epoll的學習和理解。數組

二、epoll的幾個接口

上面說到每次調用select和poll都把描述符集合拷貝到內核空間,這是由於select和poll註冊事件和監聽事件是綁定在一塊兒的,爲甚這麼說呢,咱們看select和poll的編程模式就明白了:服務器

while(true){   select(maxfd+1,readfds,writefds,execpfds,timeout)/poll(pollfd,nfds,timeout); }

I/O多路複用之select中說到了select的實現,調用select時就會進行一次用戶空間到內核空間的拷貝。epoll的改進其實就是把註冊事件和監聽事件分開了,epoll使用了一個特殊的文件來管理用戶關心的事件集合,這個文件存在於內核之中,由特殊的數據結構和一組操做構成,這樣的話,用戶就能夠提早告知內核本身關心的事件,而後再進行監聽,所以,就只須要一次用戶空間到內核空間的拷貝了。其中管理事件集合的文件經過epoll_create建立,註冊用戶行爲經過epoll_ctl實現,監聽經過epoll_wait實現。那麼編程模型大概是這個樣子:數據結構

epoll_fd=epoll_create(size); epoll_ctl(epoll_fd,operation,fd,event); while(true){   epoll_wait(epoll_fd,events,max_events,timeout); }

2.一、epoll_create接口

#include <sys/epoll.h>app

int epoll_create(int size);socket

epoll_create建立epoll文件,其返回epoll的句柄,size用來告訴內核監聽文件描述符的最大數目,這個參數不一樣於select()中的第一個參數(給出最大監聽的fd+1的值)。須要注意的是,當建立好epoll句柄後,它會佔用一個fd值,在linux下若是查看/proc/進程id/fd/,可以看到這個fd,因此在使用完epoll後,必須調用close()關閉,不然可能致使fd被耗盡。(摘自epoll精髓tcp

epoll_create會在內核初始化完成epoll所需的數據結構,其中一個關鍵的結構就是rdlist,表示就緒的文件描述符鏈表,epoll_wait函數就是直接檢查該鏈表,從而搶佔準備好的事件;另外一個關鍵的結構是一顆紅黑樹,這棵樹專門用於管理用戶關心的文件描述符集合。

注:關於epoll文件的核心數據結構以及epoll_create的源碼請參考這兩份資料

linux 內核poll/select/epoll實現剖析

epoll源碼實現分析[整理]

2.二、epoll_ctl接口

#include <sys/epoll.h>

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

epoll_ctl用於用戶告知內核本身關心哪一個描述符(fd)的什麼事件(event),

  • epfd,使用epoll_create函數建立的epoll句柄,epfd文件描述符對應的結構中,有一顆紅黑樹,專門用於管理用戶關心的事件集合。
  • op,用於指定用戶行爲,op參數有三種取值:fd,用戶關心的文件描述符
    • EPOLL_CTL_ADD,註冊新的fd到epfd中;
    • EPOLL_CTL_MOD,修改已註冊fd的事件;
    • EPOLL_CTL_DEL,從epfd中刪除一個fd;
  • event,用戶關心的事件(讀,寫)

參數event的結構以下:

struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable,內核會修改該屬性 */ };

events能夠是如下幾個宏的集合:

  • EPOLLIN ,表示對應的文件描述符能夠讀(包括對端SOCKET正常關閉);
  • EPOLLOUT,表示對應的文件描述符能夠寫;
  • EPOLLPRI,表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
  • EPOLLERR,表示對應的文件描述符發生錯誤;
  • EPOLLHUP,表示對應的文件描述符被掛起;
  • EPOLLET,將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來講的。
  • EPOLLONESHOT,只監聽一次事件,當監聽完此次事件以後,若是還須要繼續監聽這個socket的話,須要再次把這個socket加入到EPOLL隊列裏

 2.2.一、EPOLL_CTL_ADD

 重點說一下這個取值,當op=EPOLL_CTL_ADD時,epoll_ctl主要作了四件事:

  • 把當前文件描述符及其對應的事件(fd,epoll_event)加入紅黑樹,便於內核管理
  • 註冊設備驅動poll的回調函數ep_ptable_queue_proc,當調用f_op->poll()時,最終會調用該回調函數ep_ptable_queue_proc()
  • ep_ptable_queue_proc回調函數中,註冊回調函數ep_poll_callback,ep_poll_callback表示當描述符fd上相應的事件發生時該如何告知進程。
  • ep_ptable_queue_proc回調函數中,檢測是文件描述符fd對應的設備的epoll_event事件是否發生,若是發生則把fd及其epoll_event加入上面提到的就緒隊列rdlist中

注:關於epoll_ctl、ep_ptable_queue_proc、ep_poll_callback的原理及源碼請參考這兩份資料

linux 內核poll/select/epoll實現剖析

epoll源碼實現分析[整理]

2.三、epoll_wait接口

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

  • epfd,使用epoll_create函數建立的epoll句柄,epfd文件描述符對應的結構中,有一顆紅黑樹,專門用於管理用戶關心的事件集合。
  • events,傳出參數,表示發生的事件
  • maxevents,傳入參數,表示events數組的最大容量,其值不能超過epoll_create函數的參數size
  • timeout,0,不阻塞;整數,阻塞timeout時間;負數,無限阻塞

epoll_wait函數的原理就是去檢查上面提到的rdlist鏈表中每一個結點,rdlist的每個結點可以索引到監聽的文件描述符,就能夠調用該文件描述符對應設備的poll驅動函數f_op->poll,用以檢查該設備是否可用。這裏有個問題須要思考一下,既然rdlist就表示就緒的事件,也就是設備對應的資源可用了,爲何還要進行檢查?這是由於設備的某個資源可能被多個進程等待,當設備資源準備好後,設備會喚醒阻塞在這個資源上的全部進程,當前調用epoll_wait的進程未必能搶佔這個資源,因此須要再調用檢查一次資源是否可用,以防止被其餘進程搶佔而致使再次不可用,檢查的方法就是調用fd設備的驅動f_op->poll。

這也是爲何epoll效率可能比較高的緣由,epoll每次只檢查已經就緒的設備,不像select、poll,無論有沒有就緒,都去檢查。

注:關於epoll_wait的原理及源碼請參考這兩份資料

linux 內核poll/select/epoll實現剖析

epoll源碼實現分析[整理]

三、epoll的兩種觸發模式ET&LT

兩者的差別在於level-trigger模式下只要某個socket處於readable/writable狀態,不管何時進行epoll_wait都會返回該socket;而edge-trigger模式下只有某個socket從unreadable變爲readable或從unwritable變爲writable時,epoll_wait纔會返回該socket,et模式注重的是狀態發生改變的時候才觸發。下面兩幅圖清晰反映了兩者區別,這兩幅圖摘自Epoll在LT和ET模式下的讀寫方式 

       

在ET模式下,在使用epoll_ctl註冊文件描述符的事件時,應該把描述符設置爲非阻塞,爲何呢?以上面左邊這幅圖爲例,當數據到來以後,該socket實例從不可讀狀態邊爲可讀狀態,從該socket讀取一部分數據後,再次調用epoll_wait,因爲socket的狀態沒有發生改變(buffer上一次空到有數據可讀觸發了et,而這一次buffer還有數據可讀,狀態沒改變),因此該次調用epoll_wait並不會返回這個socket的可讀事件,並且以後也不會再發生改變,這個socket實例將永遠也得不處處理。這就是爲何將監聽的描述符設置爲非阻塞的緣由。

使用ET模式時,正確的讀寫方式應該是這樣的:

設置監聽的文件描述符爲非阻塞
while(true){
  epoll_wait(epoll_fd,events,max_evens);
  讀,只要可讀,就一直讀,直到返回0,或者-1,errno=EAGAIN/EWOULDBLOCK
}

正確的寫方式應該是這樣的:

設置監聽的文件描述符爲非阻塞
while(true){
  epoll_wait(epoll_fd,events,max_evens);
  寫,只要可寫,就一直寫,直到返回0,或者-1,errno=EAGAIN/EWOULDBLOCK
}

四、兩個問題

使用單進程單線程IO多路複用,服務器端該如何正確使用accept函數?

應該將監聽的socket實例設置爲非阻塞。

使用io多路複用時,通常會把監聽鏈接的socket實例listen_fd交給select、poll或epoll管理,若是使用阻塞模式,假設,select、poll或epoll調用返回時,有大量描述符的讀或寫事件準備好了,並且listen_fd也可讀,

咱們知道,從select、poll或epoll返回到調用accept接收新鏈接是有一個時間差的,若是這個時間內,發起請求的一端主動發送RST復位請求,服務器會把該鏈接從ACCEPT隊列(socket原理詳解,3.6節)中取出,並把該鏈接復位,這個時候再調用accept接收鏈接時,服務器將被阻塞,那其餘的可讀可寫的描述符將得不處處理,直到有新鏈接時,accept才得以返回,才能去處理其餘早已準備好的描述符。因此應該將listen_fd設置爲非阻塞。

 

騰訊後臺開發麪試題。使用Linux epoll模型,LT觸發模式,當socket可寫時,會不停的觸發socket可寫的事件,但並不老是須要寫,該如何處理?

第一種最廣泛的方式,步驟以下:

  1. 須要向socket寫數據的時候才把socket加入epoll,等待可寫事件。
  2. 接受到可寫事件後,調用write或者send發送數據,直到數據寫完。
  3. 把socket移出epoll。

這種方式的缺點是,即便發送不多的數據,也要把socket加入epoll,寫完後在移出epoll,有必定操做代價。

一種改進的方式,步驟以下:

  1. 設置socket爲非阻塞模式
  2. 調用write或者send發送數據,直到數據寫完
  3. 若是返回EAGAIN,把socket加入epoll,在epoll的驅動下寫數據,所有數據發送完畢後,再移出epoll。

這種方式的優勢是:數據很少的時候能夠避免epoll的事件處理,提升效率。

 

參考資料:

linux 內核poll/select/epoll實現剖析

epoll源碼實現分析[整理]

Epoll在LT和ET模式下的讀寫方式 

Epoll在LT和ET模式下的讀寫方式(搞不懂這兩個誰是原創,不少一樣的博文,都標誌着原創的字樣)

相關文章
相關標籤/搜索