標籤:

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

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復用模型如下:

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

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。

整體的使用流程如下圖:

基於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函數註冊要監聽的事件類型。四個參數解釋如下:

      circ epfd表示epoll句柄;

      circ op表示fd操作類型:EPOLL_CTL_ADD(註冊新的fd到epfd中),EPOLL_CTL_MOD(修改已註冊的fd的監聽事件),EPOLL_CTL_DEL(從epfd中刪除一個fd)

      circ fd是要監聽的描述符;

      circ 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。

      circ epfd是epoll句柄

      circ events表示從內核得到的就緒事件集合

      circ maxevents告訴內核events的大小

      circ timeout表示等待的超時事件

    上述三個系統調用的實際實例可參考IO多路復用:select、poll、epoll示例和Linux高性能伺服器編程。

    3 小結

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


    推薦閱讀:

    如何用 Nginx 配置透明 HTTP 和 HTTPS 代理?
    深入實時 Linux
    開始學習 Linux 用什麼發行版比較好?
    我的Linux手冊

    TAG:Linux |