天天看點

Linux下I/O多路複用系統調用(select, poll, epoll)介紹

轉自:https://zhuanlan.zhihu.com/p/22834126

1. 概念引入

I/O多路複用(multiplexing)的本質是通過一種機制(系統核心緩沖I/O資料),讓單個程序可以監視多個檔案描述符,一旦某個描述符就緒(一般是讀就緒或寫就緒),能夠通知程式進行相應的讀寫操作。

Linux中基于socket的通信本質也是一種I/O,使用socket()函數建立的套接字預設都是阻塞的,這意味着當sockets API的調用不能立即完成時,線程一直處于等待狀态,直到操作完成獲得結果或者逾時出錯。會引起阻塞的socket API分為以下四種:

  • 輸入操作: recv()、recvfrom()。以阻塞套接字為參數調用該函數接收資料時,如果套接字緩沖區内沒有資料可讀,則調用線程在資料到來前一直睡眠。
  • 輸出操作: send()、sendto()。以阻塞套接字為參數調用該函數發送資料時,如果套接字緩沖區沒有可用空間,線程會一直睡眠,直到有空間。
  • 接受連接配接:accept()。以阻塞套接字為參數調用該函數,等待接受對方的連接配接請求。如果此時沒有連接配接請求,線程就會進入睡眠狀态。
  • 外出連接配接:connect()。對于TCP連接配接,用戶端以阻塞套接字為參數,調用該函數向伺服器發起連接配接。該函數在收到伺服器的應答前,不會傳回。這意味着TCP連接配接總會等待至少伺服器的一次往返時間。

使用阻塞模式的套接字編寫網絡程式比較簡單,容易實作。但是在伺服器端,通常要處理大量的套接字通信請求,如果線程阻塞于上述的某一個輸入或輸出調用時,将無法處理其他任何運算或響應其他網絡請求,這麼做無疑是十分低效的,當然可以采用多線程,但大量的線程占用很大的記憶體空間,并且線程切換會帶來很大的開銷。而I/O多路複用模型能處理多個connection的優點就使其能支援更多的并發連接配接請求。

Linux支援I/O多路複用的系統調用有select、poll、epoll,這些調用都是核心級别的。但select、poll、epoll本質上都是同步I/O,先是block住等待就緒的socket,再block住将資料從核心拷貝到使用者記憶體空間。基于select調用的I/O複用模型如下:

Linux下I/O多路複用系統調用(select, poll, epoll)介紹

2. select, poll, epoll系統調用詳解

select,poll,epoll之間的差別如下圖:

Linux下I/O多路複用系統調用(select, poll, epoll)介紹

2.1 select詳解

Linux提供的select相關函數接口如下:

#include <sys/select.h>
#include <sys/time.h>

int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout)
FD_ZERO(int fd, fd_set* fds)   //清空集合
FD_SET(int fd, fd_set* fds)    //将給定的描述符加入集合
FD_ISSET(int fd, fd_set* fds)  //将給定的描述符從檔案中删除  
FD_CLR(int fd, fd_set* fds)    //判斷指定描述符是否在集合中
           
  1. select函數的傳回值就緒描述符的數目,逾時時傳回0,出錯傳回-1。
  2. 第一個參數max_fd指待測試的fd個數,它的值是待測試的最大檔案描述符加1,檔案描述符從0開始到max_fd-1都将被測試。
  3. 中間三個參數readset、writeset和exceptset指定要讓核心測試讀、寫和異常條件的fd集合,如果不需要測試的可以設定為NULL。

整體的使用流程如下圖:

Linux下I/O多路複用系統調用(select, poll, epoll)介紹

基于select的I/O複用模型的是單程序執行,占用資源少,可以為多個用戶端服務。但是select需要輪詢每一個描述符,在高并發時仍然會存在效率問題,同時select能支援的最大連接配接數通常受限。

2.2 poll詳解

poll的機制與select類似,與select在本質上沒有多大差别,管理多個描述符也是進行輪詢,根據描述符的狀态進行處理,但是poll沒有最大檔案描述符數量的限制。

Linux提供的poll函數接口如下:

#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

typedef struct pollfd {
        int fd;                         // 需要被檢測或選擇的檔案描述符
        short events;                   // 對檔案描述符fd上感興趣的事件
        short revents;                  // 檔案描述符fd上目前實際發生的事件*/
} pollfd_t;
           
  1. poll()函數傳回fds集合中就緒的讀、寫,或出錯的描述符數量,傳回0表示逾時,傳回-1表示出錯;
  2. fds是一個struct pollfd類型的數組,用于存放需要檢測其狀态的socket描述符,并且調用poll函數之後fds數組不會被清空;
  3. nfds記錄數組fds中描述符的總數量;
  4. timeout是調用poll函數阻塞的逾時時間,機關毫秒;
  5. 一個pollfd結構體表示一個被監視的檔案描述符,通過傳遞fds[]訓示 poll() 監視多個檔案描述符。其中,結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域,結構體的revents域是檔案描述符的操作結果事件掩碼,核心在調用傳回時設定這個域。events域中請求的任何事件都可能在revents域中傳回。

合法的事件如下:

POLLIN 有資料可讀 

POLLRDNORM 有普通資料可讀

POLLRDBAND 有優先資料可讀

POLLPRI 有緊迫資料可讀 

POLLOUT 寫資料不會導緻阻塞 

POLLWRNORM 寫普通資料不會導緻阻塞 POLLWRBAND 寫優先資料不會導緻阻塞 POLLMSGSIGPOLL 消息可用

當需要監聽多個事件時,使用POLLIN | POLLRDNORM設定 events 域;當poll調用之後檢測某事件是否發生時,fds[i].revents & POLLIN進行判斷。

2.3 epoll詳解

epoll在Linux2.6核心正式提出,是基于事件驅動的I/O方式,相對于select和poll來說,epoll沒有描述符個數限制,使用一個檔案描述符管理多個描述符,将使用者關心的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy隻需一次。優點如下:

  1. 沒有最大并發連接配接的限制,能打開的fd上限遠大于1024(1G的記憶體能監聽約10萬個端口)
  2. 采用回調的方式,效率提升。隻有活躍可用的fd才會調用callback函數,也就是說 epoll 隻管你“活躍”的連接配接,而跟連接配接總數無關,是以在實際的網絡環境中,epoll的效率就會遠遠高于select和poll。
  3. 記憶體拷貝。使用mmap()檔案映射記憶體來加速與核心空間的消息傳遞,減少複制開銷。

epoll對檔案描述符的操作有兩種模式:LT(level trigger,水準觸發)和ET(edge trigger)。

  • 水準觸發:預設工作模式,即當epoll_wait檢測到某描述符事件就緒并通知應用程式時,應用程式可以不立即處理該事件;下次調用epoll_wait時,會再次通知此事件。
  • 邊緣觸發:當epoll_wait檢測到某描述符事件就緒并通知應用程式時,應用程式必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次通知此事件。(直到你做了某些操作導緻該描述符變成未就緒狀态了,也就是說邊緣觸發隻在狀态由未就緒變為就緒時通知一次)。

ET模式很大程度上減少了epoll事件的觸發次數,是以效率比LT模式下高。

Linux中提供的epoll相關函數接口如下:

#include <sys/epoll.h>
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. epoll_create函數建立一個epoll句柄,參數size表明核心要監聽的描述符數量。調用成功時傳回一個epoll句柄描述符,失敗時傳回-1。
  2. epoll_ctl函數注冊要監聽的事件類型。四個參數解釋如下:
    Linux下I/O多路複用系統調用(select, poll, epoll)介紹
     epfd表示epoll句柄;
    Linux下I/O多路複用系統調用(select, poll, epoll)介紹
     op表示fd操作類型:EPOLL_CTL_ADD(注冊新的fd到epfd中),EPOLL_CTL_MOD(修改已注冊的fd的監聽事件),EPOLL_CTL_DEL(從epfd中删除一個fd)
    Linux下I/O多路複用系統調用(select, poll, epoll)介紹
     fd是要監聽的描述符;
    Linux下I/O多路複用系統調用(select, poll, epoll)介紹

     event表示要監聽的事件

    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;
               
  3. epoll_wait函數等待事件的就緒,成功時傳回就緒的事件數目,調用失敗時傳回 -1,等待逾時傳回 0。
    Linux下I/O多路複用系統調用(select, poll, epoll)介紹
     epfd是epoll句柄
    Linux下I/O多路複用系統調用(select, poll, epoll)介紹
     events表示從核心得到的就緒事件集合
    Linux下I/O多路複用系統調用(select, poll, epoll)介紹
     maxevents告訴核心events的大小
    Linux下I/O多路複用系統調用(select, poll, epoll)介紹
     timeout表示等待的逾時事件

上述三個系統調用的實際執行個體可參考IO多路複用:select、poll、epoll示例和Linux高性能伺服器程式設計。

3. 小結

select的幾大缺點:

  • (1)每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大
  • (2)同時每次調用select都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大
  • (3)select支援的檔案描述符數量太小了,32位系統1024,64位系統2048

poll的實作和select非常相似,隻是描述fd集合的方式不同,poll使用pollfd結構(連結清單結構)而不是select的fd_set結構,是以連接配接數上沒有限制,其他的都差不多。

epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?

  • 對于第一個缺點,epoll的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中隻會拷貝一次。
  • 對于第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待隊列中,而隻在epoll_ctl時把current挂一遍(這一遍必不可少)并為每個fd指定一個回調函數,當裝置就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒連結清單)。epoll_wait的工作實際上就是在這個就緒連結清單中檢視有沒有就緒的fd(利用schedule_timeout()實作睡一會,判斷一會的效果)。
  • 對于第三個缺點,epoll沒有這個限制,它所支援的fd上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。

epoll是Linux目前大規模網絡并發程式開發的首選模型。在絕大多數情況下性能遠超select和poll。目前流行的高性能web伺服器Nginx正式依賴于epoll提供的高效網絡套接字輪詢服務。但是,在并發連接配接不高的情況下,多線程+阻塞I/O方式可能性能更好。