【IO】IO多路複用及select,poll,epoll運行機制

上篇文章梳理了四種不同的IO模式,這篇博客繼續梳理IO多路複用和Reactor模式。

IO多路複用

概念

io多路複用就是利用 select、poll、epoll 可以同時監察多個流的 I/O 事件的能力,在空閒的時候,
會把當前線程阻塞掉。當有一個或多個流有 I/O事件時,就從阻塞態中喚醒,
於是程序就會輪詢一遍所有的流(epoll 是隻輪詢那些真正發出了事件的流),並且只依次順序的處理就緒的流,
這種做法就避免了大量的無用操作。

故事
知乎上一個很有意思的小故事來解釋:

模擬一個tcp服務器處理30個客戶socket。假設你是一個老師,讓30個學生解答一道題目,然後檢查學生做的是否正確,你有下面幾個選擇:

1. 第一種選擇:按順序逐個檢查,先檢查A,然後是B,之後是C、D。。。這中間如果有一個學生卡主,
全班都會被耽誤。這種模式就好比,你用循環挨個處理socket,根本不具有併發能力。
2. 第二種選擇:你創建30個分身,每個分身檢查一個學生的答案是否正確。 這種類似於爲每一個用戶
創建一個進程或者線程處理連接。
3. 第三種選擇,你站在講臺上等,誰解答完誰舉手。這時C、D舉手,表示他們解答問題完畢,你下去依次
檢查C、D的答案,然後繼續回到講臺上等。此時E、A又舉手,然後去處理E和A。。。 
這種就是IO複用模型,Linux下的select、poll和epoll就是幹這個的。
將用戶socket對應的fd註冊進epoll,然後epoll幫你監聽哪些socket上有消息到達,
這樣就避免了大量的無用操作。此時的socket應該採用非阻塞模式。這樣,
整個過程只在調用select、poll、epoll這些調用的時候纔會阻塞,收發客戶消息是不會阻塞的,
整個進程或者線程就被充分利用起來,這就是事件驅動,所謂的reactor模式。	 

這個故事的三種選擇分別解釋了網絡IO的進化歷程,從最開始的由一個線程的完全阻塞模式到第二種完全支持併發但消耗資源巨大,最後到第三種的多路複用。多路複用在select,poll和epoll執行時仍然是阻塞的。這裏的select,poll和epoll就等同於故事裏面的老師的角色。

一張圖
一直想用一張圖來表示IO多路複用,從網絡上找了一張比較形象的如下圖:
在這裏插入圖片描述
這裏的Selector就可以理解爲一個多路複用器,每個客戶端連接就是一個SocketChannel,這些SocketChannel會在Selector上註冊,並且設置對各個Channel感興趣的事件。當Selector監聽到對應的事件之後,其就會將事件交由下層的線程處理。

接下來關於select,poll和epoll三個方法有啥區別呢。

select,poll,epoll

select

int select(int maxfdp1,fd_set *readset,fd_set *writeset,
fd_set *exceptset,const struct timeval *timeout);

【參數說明】
int maxfdp1 指定待測試的文件描述字個數,它的值是待測試的最大描述字加1。
fd_set *readset , fd_set *writeset , fd_set *exceptset
fd_set可以理解爲一個集合,這個集合中存放的是文件描述符(file descriptor),即文件句柄。中間的三個參數指定我們要讓內核測試讀、寫和異常條件的文件描述符集合。如果對某一個的條件不感興趣,就可以把它設爲空指針。
const struct timeval *timeout timeout告知內核等待所指定文件描述符集合中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。

select運行機制
select()的機制中提供一種fd_set的數據結構,實際上是一個long類型的數組,每一個數組元素都能與一打開的文件句柄(不管是Socket句柄,還是其他文件或命名管道或設備句柄)建立聯繫,建立聯繫的工作由程序員完成,當調用select()時,由內核根據IO狀態修改fd_set的內容,由此來通知執行了select()的進程哪一Socket或文件可讀。

從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以後最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以註冊多個socket,然後不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。

select機制的問題
每次調用select,都需要把fd_set集合從用戶態拷貝到內核態,如果fd_set集合很大時,那這個開銷也很大
同時每次調用select都需要在內核遍歷傳遞進來的所有fd_set,如果fd_set集合很大時,那這個開銷也很大
爲了減少數據拷貝帶來的性能損壞,內核對被監控的fd_set集合大小做了限制,並且這個是通過宏控制的,大小不可改變(限制爲1024)

POLL
poll的機制與select類似,與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大文件描述符數量的限制。也就是說,poll只解決了上面的問題3,並沒有解決問題1,2的性能開銷問題。

poll函數原型:

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

typedef struct pollfd {
        int fd;                         // 需要被檢測或選擇的文件描述符
        short events;                   // 對文件描述符fd上感興趣的事件
        short revents;                  // 文件描述符fd上當前實際發生的事件
} pollfd_t;

poll改變了文件描述符集合的描述方式,使用了pollfd結構而不是select的fd_set結構,使得poll支持的文件描述符集合限制遠大於select的1024

【參數說明】

struct pollfd *fds fds是一個struct pollfd類型的數組,用於存放需要檢測其狀態的socket描述符,並且調用poll函數之後fds數組不會被清空;一個pollfd結構體表示一個被監視的文件描述符,通過傳遞fds指示 poll() 監視多個文件描述符。其中,結構體的events域是監視該文件描述符的事件掩碼,由用戶來設置這個域,結構體的revents域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域

nfds_t nfds 記錄數組fds中描述符的總數量

EPOLL
epoll在Linux2.6內核正式提出,是基於事件驅動的I/O方式,相對於select來說,epoll沒有描述符個數限制,使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次

epoll中的函數:

int epoll_create(int size);
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);

(1) int epoll_create(int size);
  創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。

(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  epoll的事件註冊函數,它不同與select()是在監聽事件時告訴內核要監聽什麼類型的事件epoll的事件註冊函數,它不同與select()是在監聽事件時告訴內核要監聽什麼類型的事件,而是在這裏先註冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;

epoll_event 結構體定義如下:

struct epoll_event {
    __uint32_t events;  /* Epoll events */
    epoll_data_t data;  /* User data variable */
};

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;
  1. epoll_wait 函數等待事件的就緒,成功時返回就緒的事件數目,調用失敗時返回 -1,等待超時返回 0。

epfd 是epoll句柄
events 表示從內核得到的就緒事件集合
maxevents 告訴內核events的大小
timeout 表示等待的超時事件

epoll是Linux內核爲處理大批量文件描述符而作了改進的poll,是Linux下多路複用IO接口select/poll的增強版本,它能顯著提高程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率。原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入Ready隊列的描述符集合就行了。

epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait/epoll_pwait的調用,提高應用程序效率。

水平觸發(LT):默認工作模式,即當epoll_wait檢測到某描述符事件就緒並通知應用程序時,應用程序可以不立即處理該事件;下次調用epoll_wait時,會再次通知此事件
邊緣觸發(ET): 當epoll_wait檢測到某描述符事件就緒並通知應用程序時,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次通知此事件。(直到你做了某些操作導致該描述符變成未就緒狀態了,也就是說邊緣觸發只在狀態由未就緒變爲就緒時只通知一次)。

一張表總結:
在這裏插入圖片描述

裏面很多概念自己也不是非常清楚,還是要不斷學習探索繼續加深理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章