首頁>技術>

本節,我們介紹IO複用,透過簡單的例子演示IO複用的使用,以及實現原理,這個技術是目前構建目前的高效能伺服器必備技術,在後面我們介紹到各種網路程式設計模型的時候,會用到IO複用。

看完本文,您將瞭解到:

IO複用的執行流程;select函式的使用和優缺點,以及實現原理;poll函式的使用和優缺點,以及實現原理;epoll函式的使用和優缺點,以及實現原理;epoll的條件觸發和邊緣觸發,以及實現原理。1、I/O複用模型介紹

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

工作原理如下圖所示:

如上圖,這種模型與非阻塞式I/O相比,把輪訓判斷資料是否準備好的處理方式替換為了透過select()系統呼叫的方式來實現。

常用的實現IO複用的相關函式有select,poll和epoll,接下來我們介紹下這三個函式。

2、select函式

**select是實現I/O多路複用的經典系統呼叫函式。**select()可以同時等待多個套接字的變為可讀,只要有任意一個套接字可讀,那麼就會立刻返回,處理已經準備好的套接字了。

2.1、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.2、select函式例子

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

這個例子開啟了一個監聽套接字,然後獲取5個客戶端連線,透過select函式判斷是否有資料到達伺服器端,如果有則讀取,我把詳細的註釋都加上了,下面重點介紹標註了序號的程式碼:

1、SOCKET

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

2、BIND

呼叫bind把本地協議地址賦予套接字;

3、LISTEN

呼叫listen轉換為被動套接字,開始接受指向該套接字的連線請求;

4、得到MAXFD

在獲取5個已連線套接字的過程中,判斷獲取到最大的套接字檔案描述符;

5、初始化FD_SET

在迴圈裡面,每次重新呼叫select之前,都需要重新設定rset,在第7步我們解釋為什麼要這樣做;

fd_set是一個bitmap,由核心固定設定的大小,最大長度為1024,這也限制了我們最多隻能同時監聽1024個描述符。假如我們這裡得到的五個描述符是:1 2 5 6 8,那麼這個點陣圖會是這樣的:

6、SELECT函式傳入的待測試描述符+1

這裡為什麼要加1呢?

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

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

這一步的流程是這樣的:

應用程序呼叫了select之後,會把fd_set從使用者空間複製到核心空間,隨後應用程序進入阻塞;核心根據fd_set得到需要處理的描述符,根據描述符是否準備好資料,給fd_set進行置位程序被喚醒,拿到核心處理過後的fd_set,就可以透過FD_ISSET判斷到套接字資料是否已經準備好了。8、判斷描述符是否可讀

這裡會把已準備好的資料的套接字描述符對應的fd_set中的標識進行標記,透過FD_ISSET即可判斷到標記結果。

2.3、select函式優缺點2.3.1、優點

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

2.3.2、缺點

根據上面的過程描述,我們可以知道select有如下缺點:

fd_set中的bitmap是固定1024位的,也就是說最多隻能監聽1024個套接字。當然也可以改核心原始碼,不過代價比較大;fd_set每次傳入核心之後,都會被改寫,導致不可重用,每次呼叫select都需要重新初始化fd_set;每次呼叫select都需要複製新的fd_set到核心空間,這裡會做一個使用者態到核心態的切換;拿到fd_set的結果後,應用程序需要遍歷整個fd_set,才知道哪些檔案描述符有資料可以處理。3、poll

基於epoll的缺點,於是出現了第二個系統呼叫,poll,poll與核心互動的資料有所不同,並且突破了檔案描述符數量的限制。

3.1、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.2、poll函式例子

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

與select的例子很類似,開啟了一個監聽套接字,然後獲取5個客戶端連線,透過poll函式判斷是否有資料到達伺服器端,如果有則讀取,我把詳細的註釋都加上了,下面重點介紹標註了序號的程式碼:

1、設定POLLFD描述符

這裡透過accept阻塞獲取已連線描述符,賦值給pollfd結構的fd中。

2、設定POLLFD事件

然後給pollfd的events設定POLLIN,指定需要檢測POLLIN,即資料讀入。

3、POLL

呼叫poll函式,傳入剛剛初始化好的pollfds,數量為5,超時時間為10秒。這裡進入阻塞等待,直到從核心返回。

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

應用程序呼叫了poll之後,會把poll_fd從使用者空間複製到核心空間,隨後應用程序進入阻塞;核心根據poll_fd的fd得到需要處理的描述符,根據描述符是否準備好資料,給poll_fd的revents進行置位程序被喚醒,拿到核心處理過後的poll_fd,就可以透過與操作判斷到對應的事件是否被置位,從而知道套接字資料是否已經準備好了。4、判斷事件是否準備好

從核心返回之後,我們迴圈判斷pollfds中每個元素的revents,透過與操作,看看POLLIN是否被置位了,如果置位了就說明資料已經準備好了。

5、重置事件

這裡對revents進行了重置,下次就可以複用這個pollfds,繼續執行poll函數了。

3.3、poll函式優缺點3.3.1、優點與select類似,非阻塞IO直接輪訓查詢資料是否準備好,每次查詢都要切換核心態,輪訓消耗CPU,而poll則是把查詢多個描述符的動作交給了核心,避免了CPU消耗和減少了核心態的切換。與select相比,這裡不是用的bitmap,而是直接用poll_fd陣列,沒有1024個描述符的限制;這裡引入了poll_fd結構體,核心只是修改poll_fd結構體中的revents,這樣每次讀取資料的時候,重置revents,就可以複用poll_fd了,不用像select那樣反覆初始化一個新的rset。3.3.2、缺點每次呼叫poll都需要複製新的poll_fd到核心空間,這裡會做一個使用者態到核心態的切換;拿到poll_fd的結果後,應用程序需要遍歷整個poll_fd,才知道哪些檔案描述符有資料可以處理。4、epoll

與poll不同,epoll本身不是系統呼叫,而是一種核心資料結構,它允許程序在多個檔案描述符上多路複用I / O。

4.1、epoll的相關函式4.1.1、EPOLL_CREATE

epoll例項是透過epoll_create系統呼叫建立的,該系統呼叫將檔案描述符返回到epoll例項,函式定義如下:

#include <sys/epoll.h>int epoll_create(int size);

size引數向核心指示程序要監視的檔案描述符的數量,這有助於核心確定epoll例項的大小。從Linux 2.6.8開始,此引數將被忽略,因為epoll資料結構會隨著檔案描述符的新增或刪除而動態調整大小。

如下圖,使用者程序最終拿到了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.2、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.3、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.2、epoll例子

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

1、定義EPOLL_EVENT

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

2、呼叫EPOLL_CREATE

在核心中建立epoll例項,併發揮epfd檔案描述符。

3、設定EVENT.DATA.FD

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

4、設定EVENT.EVENTS

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

5、呼叫EPOLL_CTL

將要監視的event新增到epoll例項。

6、呼叫EPOLL_WAIT

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

4.3、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.4.1、邊緣觸發

當一個新增到epoll例項的epoll_event設定為EPOLLET邊緣觸發(edge-triggered)之後,如果後續有描述符的事件準備好了,呼叫epoll_wait就會把對應的epoll_event返回給應用程序,注意,在邊緣觸發模式下,只會返回已準備好的描述符的epoll_evnet一次,也就是說程式只有一次的處理機會。

4.4.2、條件觸發

當把要新增到epoll例項的epoll_event設定為EPOLLLT條件觸發(level-triggered)時,只要已準備好的描述符沒有被處理完,下一次呼叫epoll_wait的時候,還是會繼續返回給應用程序處理。這是系統預設處理方式。

EPOLLET邊緣觸發的效率要比EPOLLLT高效,因為對於每個準備就緒的套接字,只會通知應用程序一次,但是這也要求程式設計師必須小心處理,不會留多次機會給你去補償處理套接字。

4.4.3、實現原理

針對條件觸發,返回給核心空間的描述符會再次加入到就緒佇列中,那麼下次呼叫epoll_wait的時候,這些epoll_item將會被重新處理:呼叫檔案描述符的poll方法,確定事件是否還有效,如果還有效,那就繼續返回,從而實現了條件觸發。

而邊緣觸發的情況下,返回給核心空間的描述符則不會再次放回就緒佇列,所以只會返回一次。

4.5、epoll優缺點4.5.1、優點epoll每次呼叫epoll_wait的時候,不像poll呼叫一樣,每次都要傳遞結構體到核心空間,而是複用一個核心的epoll例項結構體,透過epfd進行引用,從而減小了系統開銷;epoll底層是套接字一旦有事件,就呼叫回撥立刻通知epoll例項,可以儘早的準備好事件就緒佇列,執行epoll_wait的時候相應的更快;epoll底層基於紅黑樹維護興趣事件列表,這樣每次套接字有新事件觸發回撥的時候,可以更快的找到套接字的epitem進行後續的處理;提供了效能更佳的邊緣觸發機制。

正是因為epoll這麼多的優點,很多技術都是基於epoll實現的,如nginx、redis,以及Linux下Java的NIO。

4.5.2、缺點

它還不是真正的非同步IO,還是要應用程序呼叫IO函式的時候,才把資料從核心複製到應用程序。

28
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 靜態站點生成器概述 Gatsby ,Hugo 和Jekyll