完全弄懂IO複用:IO處理殺手鐗,帶您深刻了解select,poll,epoll

前言

本節,咱們介紹IO複用,經過簡單的例子演示IO複用的使用,以及實現原理,這個技術是目前構建目前的高性能服務器必備技術,在後面咱們介紹到各類網絡編程模型的時候,會用到IO複用。nginx

看完本文,您將瞭解到:程序員

  • IO複用的執行流程;redis

  • select函數的使用和優缺點,以及實現原理;編程

  • poll函數的使用和優缺點,以及實現原理;數組

  • epoll函數的使用和優缺點,以及實現原理;服務器

  • epoll的條件觸發和邊緣觸發,以及實現原理。網絡

一、I/O複用模型介紹

I/O複用(I/O multiplexing),指的是經過一個支持同時感知多個描述符的函數系統調用,阻塞在這個系統調用上,等待某一個或者幾個描述符準備就緒,就返回可讀條件。常見的如select,poll,epoll系統調用能夠實現此類功能功能。這種模型不用阻塞在真正的I/O系統調用上。數據結構

工做原理以下圖所示:架構

如上圖,這種模型與非阻塞式I/O相比,把輪訓判斷數據是否準備好的處理方式替換爲了經過select()系統調用的方式來實現。併發

經常使用的實現IO複用的相關函數有select,poll和epoll,接下倆咱們介紹下這三個函數。

二、select函數

**select是實現I/O多路複用的經典系統調用函數。**select()能夠同時等待多個套接字的變爲可讀,只要有任意一個套接字可讀,那麼就會馬上返回,處理已經準備好的套接字了。

2.一、select函數定義

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

int pselect(int nfds, fd_set *readfds, fd_set *writefds,
            fd_set *exceptfds, const struct timespec *timeout,
            const sigset_t *sigmask);複製代碼

select函數參數:

  • int nfds:指定待測試的描述符的個數,它的值是待測試的最大描述符加1;

  • fd_set readfds:指定要讓內核測試讀的描述符;

  • fd_set writefds:指定要讓內核測試寫的描述符;

  • fd_set exceptfds:指定要讓內核測試異常的描述符;

  • timeval timeout:告知內核等待所制定描述符中的任何一個準備就緒的超時時間。

其中有一個重要的結構體:fd_set,用於存儲描述符集,底層使用bitmap記錄描述符的。

與之相關的4個宏:

  • FD_CLR:清除fdset中的全部bit位;

  • FD_SET:開啓fdset中fd描述符對應的bit位;

  • FD_ZERO:關閉fdset中fd描述符對應的bit位;

  • FD_ISSET:判斷fd描述符對應的bit位是否開啓;

2.二、select函數例子

下面經過一個例子演示select是如何使用的,而且分析其執行原理。

這個例子開啓了一個監聽套接字,而後獲取5個客戶端鏈接,經過select函數判斷是否有數據到達服務器端,若是有則讀取,我把詳細的註釋都加上了,下面重點介紹標註了序號的代碼:

一、SOCKET

調用socket建立一個監聽套接字,並拿到監聽套接字描述符;

二、BIND

調用bind把本地協議地址賦予套接字;

三、LISTEN

調用listen轉換爲被動套接字,開始接受指向該套接字的鏈接請求;

四、獲得MAXFD

在獲取5個已鏈接套接字的過程當中,判斷獲取到最大的套接字文件描述符;

五、初始化FD_SET

在循環裏面,每次從新調用select以前,都須要從新設置rset,在第7步咱們解釋爲何要這樣作;

fd_set是一個bitmap,由內核固定設置的大小,最大長度爲1024,這也限制了咱們最多隻能同時監聽1024個描述符。假如咱們這裏獲得的五個描述符是:1 2 5 6 8,那麼這個位圖會是這樣的:

六、SELECT函數傳入的待測試描述符+1

這裏爲何要加1呢?

根據第五步,能夠知道,fd_set中的bitmap是從0開始的,因此rset實際有效的bitmap長度是待測試描述符+1。

七、往SELECT中傳入要讓內核測試讀的描述符,而後阻塞等待內核返回

這一步的流程是這樣的:

  • 應用進程調用了select以後,會把fd_set從用戶空間拷貝到內核空間,隨後應用進程進入阻塞;

  • 內核根據fd_set獲得須要處理的描述符,根據描述符是否準備好數據,給fd_set進行置位;

  • 進程被喚醒,拿到內核處理事後的fd_set,就能夠經過FD_ISSET判斷到套接字數據是否已經準備好了。

八、判斷描述符是否可讀

這裏會把已準備好的數據的套接字描述符對應的fd_set中的標識進行標記,經過FD_ISSET便可判斷到標記結果。

2.三、select函數優缺點

2.3.一、優勢

非阻塞IO直接輪訓查詢數據是否準備好,每次查詢都要切換內核態,輪訓消耗CPU。而select函數則直接把查詢多個描述符的動做交給了內核,這樣避免了CPU消耗和減小了內核態的切換。

2.3.二、缺點

根據上面的過程描述,咱們能夠知道select有以下缺點:

  • fd_set中的bitmap是固定1024位的,也就是說最多隻能監聽1024個套接字。固然也能夠改內核源碼,不過代價比較大;

  • fd_set每次傳入內核以後,都會被改寫,致使不可重用,每次調用select都須要從新初始化fd_set;

  • 每次調用select都須要拷貝新的fd_set到內核空間,這裏會作一個用戶態到內核態的切換;

  • 拿到fd_set的結果後,應用進程須要遍歷整個fd_set,才知道哪些文件描述符有數據能夠處理。

三、poll

基於epoll的缺點,因而出現了第二個系統調用,poll,poll與內核交互的數據有所不一樣,而且突破了文件描述符數量的限制。

3.一、poll函數定義

下面是poll函數的定義:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
              // 返回:如有就緒描述符則爲其數目,若超時則爲0,若出錯則爲1複製代碼

poll函數參數:

  • pollfd * fds:指向一個結構數組第一個元素的指針,每一個元素,都是一個pollfd結構,用於指定測試某個給定描述符fd的條件,結構體格式以下:
struct pollfd {
  int   fd;         /* 待檢測的文件描述符 */
  short events;     /* 描述符上待檢測的事件類型 */
  short revents;    /* 返回描述符對應事件的狀態 */
};複製代碼
  • int fd:爲待檢測的文件描述符;

  • short events:爲描述符上待檢測的事件類型,這裏用了short類型,具體的實現用二進制掩碼位操做來完成,經常使用的事件類型以下:

  • short revents:返回描述符對應事件的狀態,在pollfd由系統調用返回以後,會響應具體的事件狀態;

  • nfds_t nfds:nfds指定fds數組的大小;

  • int timeout:指定poll函數返回前須要等待多長時間。

接下來咱們仍是看具體的例子。

3.二、poll函數例子

下面經過一個例子演示poll是如何使用的,而且分析其執行原理。

與select的例子很相似,開啓了一個監聽套接字,而後獲取5個客戶端鏈接,經過poll函數判斷是否有數據到達服務器端,若是有則讀取,我把詳細的註釋都加上了,下面重點介紹標註了序號的代碼:

一、設置POLLFD描述符

這裏經過accept阻塞獲取已鏈接描述符,賦值給pollfd結構的fd中。

二、設置POLLFD事件

而後給pollfd的events設置POLLIN,指定須要檢測POLLIN,即數據讀入。

三、POLL

調用poll函數,傳入剛剛初始化好的pollfds,數量爲5,超時時間爲10秒。這裏進入阻塞等待,直到從內核返回。

與select相似,這一步的執行流程是這樣的:

  • 應用進程調用了poll以後,會把poll_fd從用戶空間拷貝到內核空間,隨後應用進程進入阻塞;

  • 內核根據poll_fd的fd獲得須要處理的描述符,根據描述符是否準備好數據,給poll_fd的revents進行置位;

  • 進程被喚醒,拿到內核處理事後的poll_fd,就能夠經過與操做判斷到對應的事件是否被置位,從而知道套接字數據是否已經準備好了。

四、判斷事件是否準備好

從內核返回以後,咱們循環判斷pollfds中每一個元素的revents,經過與操做,看看POLLIN是否被置位了,若是置位了就說明數據已經準備好了。

五、重置事件

這裏對revents進行了重置,下次就能夠複用這個pollfds,繼續執行poll函數了。

3.三、poll函數優缺點

3.3.一、優勢

  • 與select相似,非阻塞IO直接輪訓查詢數據是否準備好,每次查詢都要切換內核態,輪訓消耗CPU,而poll則是把查詢多個描述符的動做交給了內核,避免了CPU消耗和減小了內核態的切換。

  • 與select相比,這裏不是用的bitmap,而是直接用poll_fd數組,沒有1024個描述符的限制;

  • 這裏引入了poll_fd結構體,內核只是修改poll_fd結構體中的revents,這樣每次讀取數據的時候,重置revents,就能夠複用poll_fd了,不用像select那樣反覆初始化一個新的rset。

3.3.二、缺點

  • 每次調用poll都須要拷貝新的poll_fd到內核空間,這裏會作一個用戶態到內核態的切換;

  • 拿到poll_fd的結果後,應用進程須要遍歷整個poll_fd,才知道哪些文件描述符有數據能夠處理。

四、epoll

與poll不一樣,epoll自己不是系統調用,而是一種內核數據結構,它容許進程在多個文件描述符上多路複用I / O。

能夠經過三個系統調用來建立,修改和刪除此數據結構。

4.一、epoll的相關函數

4.1.一、EPOLL_CREATE

epoll實例是經過epoll_create系統調用建立的,該系統調用將文件描述符返回到epoll實例,函數定義以下:

#include <sys/epoll.h>

int epoll_create(int size);複製代碼

size參數向內核指示進程要監視的文件描述符的數量,這有助於內核肯定epoll實例的大小。從Linux 2.6.8開始,此參數將被忽略,由於epoll數據結構會隨着文件描述符的添加或刪除而動態調整大小。

epoll_create系統調用將返回新建立的epoll內核數據結構的文件描述符。而後,調用過程當中可使用此文件描述符來添加,刪除或修改其要監視的epoll實例的I/O的其餘文件描述符。

以下圖,用戶進程最終拿到了epoll實例的描述符 EPFD,以支持對epoll實例的訪問:

還有另外一個系統調用epoll_create1,其定義以下:

int epoll_create1(int flags);複製代碼

flags參數能夠爲0或EPOLL_CLOEXEC。

  • 設置爲0時,epoll_create1的行爲與epoll_create相同;

  • 設置EPOLL_CLOEXEC標誌後,當前進程派生的任何子進程將在執行前關閉epoll描述符,所以該子進程將沒法再訪問epoll實例;

4.1.二、EPOLL_CTL

進程能夠經過調用epoll_ctl將想要監視的文件描述符添加到epoll實例。

向epoll實例註冊的全部文件描述符統稱爲epoll的興趣列表[1],會包裝成epitem結構體,放到一顆紅黑樹rbr中:

在上圖中,用戶進程向epoll實例註冊了文件描述符fd1,fd2,fd3,fd4,這是該epoll實例的興趣列表集。

當任何已註冊的文件描述符準備好進行I/O時,它們就被放入事件就緒隊列。事件就緒隊列是興趣列表的一個子集。內核在接收到I/O準備好的事件回調的時候,把rbr中的epitem移到事件就緒隊列。

epoll_ctl系統調用定義以下:

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);複製代碼
  • int epfd:epoll_create返回的文件描述符,用於標識內核中的epoll實例;

  • int op:指要在文件描述符fd上執行的操做。一般,支持三種操做:

  • EPOLL_CTL_ADD:向epoll實例註冊文件描述符對應的事件;

  • EPOLL_CTL_DEL:從epoll實例註銷fd。這意味着該進程將再也不獲取有關該文件描述符上事件的任何通知。若是已將文件描述符添加到多個epoll實例,則關閉該文件描述符會將其從添加了該文件的全部epoll興趣列表中刪除

  • EPOLL_CTL_MOD:修改文件描述符對應的事件。

  • int fd:咱們要添加到epoll興趣列表的文件描述符;

  • struct epoll_event *event:指向名爲epoll_event的結構的指針,該結構存儲咱們實際上要監視fd的事件。

typedef union epoll_data { 
  void *ptr; 
  int fd;           /* 須要監視的文件描述符 */
  uint32_t u32; 
  uint64_t u64; 
} epoll_data_t; 

struct epoll_event { 
  uint32_t events;   /* 須要監視的Epoll事件,與poll同樣,基於mask的事件類型 */ 
  epoll_data_t data; /* User data variable */ 
};複製代碼

4.1.三、EPOLL_WAIT

能夠經過調用epoll_wait系統調用來等到內核通知進程epoll實例的興趣列表上發生的事件,該事件將阻塞直到被監視的任何描述符準備好進行I/O操做爲止。

函數定義以下:

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);複製代碼
  • int epfd:epoll實例描述符;

  • struct epoll_event *events:返回給用戶空間須要處理的IO事件數組;

  • int maxevents:指定epoll_wait能夠返回的最大事件值;

  • int timeout:指定阻塞調用超時時間。-1表示不超時,0表示當即返回。

4.二、epoll例子

注意:epoll機制是在Linux 2.6以後引入的,因此Mac OS不支持。Mac OS下使用kqueue機制代替epoll實現IO複用。

一、定義EPOLL_EVENT

定義一個epoll_event,存儲實際要監視的fd相關信息,以及待接收epll_wait返回的就緒事件列表。

二、調用EPOLL_CREATE

在內核中建立epoll實例,併發揮epfd文件描述符。

三、設置EVENT.DATA.FD

event結構設置實際要監視的描述符。

四、設置EVENT.EVENTS

event和值實際要監視的事件。

五、調用EPOLL_CTL

將要監視的event添加到epoll實例。

六、調用EPOLL_WAIT

獲取內核epoll實例中興趣列表上發生的事件,即事件就緒隊列的內容,epoll_wait的返回值爲就緒隊列的大小。

4.三、epoll原理解析[2]

仍是看看剛纔那個圖:

這裏咱們重點來看看epoll實例的如下相關結構體:

eventpoll epoll實例
    rdllist:事件就緒隊列
    rbr:用於快速查找fd的紅黑樹
        epitem:一個fd會對應建立一個epitem複製代碼
  • eventpoll實例是存在內核空間的,每次用戶進程要請求epoll_wait調用的時候,都須要經過傳遞epfd描述符讓內核找到用戶要訪問的eventpoll實例;

  • 每次調用epoll_ctl爲描述符訂閱事件的時候,實際上是把描述符和事件相關內容包裝成epitem結構,而後往紅黑樹rbr添加樹節點,用戶進程全部關心的描述符都存在這顆紅黑樹中;

  • 內核會經過ep_ptable_queue_proc函數給每一個文件描述符設置回調ep_poll_callback,對應的文件描述符若是有事件發生,那麼就會調用回調函數,從而觸發內核進行查找紅黑樹,把須要的套接字epitem移動到事件就緒隊列;

  • 最後在執行epoll_wait準備把事件就緒隊列的內容從內核空間拷貝到用戶空間的時候,還會再次調用每一個文件描述符的poll方法,以便肯定確實有事件發生,從而確保事件仍是有效的。

更詳細的實現細節,能夠進一步閱讀epoll的源碼[2]。

4.四、邊緣觸發與條件觸發

先講下邊緣觸發和條件觸發的區別。

4.4.一、邊緣觸發

當一個添加到epoll實例的epoll_event設置爲EPOLLET邊緣觸發(edge-triggered)以後,若是後續有描述符的事件準備好了,調用epoll_wait就會把對應的epoll_event返回給應用進程,注意,在邊緣觸發模式下,只會返回已準備好的描述符的epoll_evnet一次,也就是說程序只有一次的處理機會。

4.4.二、條件觸發

當把要添加到epoll實例的epoll_event設置爲EPOLLLT條件觸發(level-triggered)時,只要已準備好的描述符沒有被處理完,下一次調用epoll_wait的時候,仍是會繼續返回給應用進程處理。這是系統默認處理方式。

EPOLLET邊緣觸發的效率要比EPOLLLT高效,由於對於每一個準備就緒的套接字,只會通知應用進程一次,可是這也要求程序員必須當心處理,不會留屢次機會給你去補償處理套接字。

4.4.三、實現原理

針對條件觸發,返回給內核空間的描述符會再次加入到就緒隊列中,那麼下次調用epoll_wait的時候,這些epoll_item將會被從新處理:調用文件描述符的poll方法,肯定事件是否還有效,若是還有效,那就繼續返回,從而實現了條件觸發。

而邊緣觸發的狀況下,返回給內核空間的描述符則不會再次放會就緒隊列,因此只會返回一次。

4.五、epoll優缺點

優勢

  • epoll每次調用epoll_wait的時候,不像poll調用同樣,每次都要傳遞結構體到內核空間,而是複用一個內核的epoll實例結構體,經過epfd進行引用,從而減少了系統開銷;

  • epoll底層是套接字一旦有事件,就調用回調馬上通知epoll實例,能夠儘早的準備好事件就緒隊列,執行epoll_wait的時候相應的更快;

  • epoll底層基於紅黑樹維護興趣事件列表,這樣每次套接字有新事件觸發回調的時候,能夠更快的找到套接字的epitem進行後續的處理;

  • 提供了性能更佳的邊緣觸發機制。

正是由於epoll這麼多的優勢,不少技術都是基於epoll實現的,如nginx、redis,以及Linux下Java的NIO。

缺點

它還不是真正的異步IO,仍是要應用進程調用IO函數的時候,才把數據從內核拷貝到應用進程。最後點擊這裏便可領取Java架構大禮包哦!!!

相關文章
相關標籤/搜索