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連接總會等待至少伺服器的一次往返時間。
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) //判斷指定描述符是否在集合中
- select函數的返回值就緒描述符的數目,超時時返回0,出錯返回-1。
- 第一個參數max_fd指待測試的fd個數,它的值是待測試的最大文件描述符加1,文件描述符從0開始到max_fd-1都將被測試。
- 中間三個參數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;
- poll()函數返回fds集合中就緒的讀、寫,或出錯的描述符數量,返回0表示超時,返回-1表示出錯;
- fds是一個struct pollfd類型的數組,用於存放需要檢測其狀態的socket描述符,並且調用poll函數之後fds數組不會被清空;
- nfds記錄數組fds中描述符的總數量;
- timeout是調用poll函數阻塞的超時時間,單位毫秒;
- 一個pollfd結構體表示一個被監視的文件描述符,通過傳遞fds[]指示 poll() 監視多個文件描述符。其中,結構體的events域是監視該文件描述符的事件掩碼,由用戶來設置這個域,結構體的revents域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域。events域中請求的任何事件都可能在revents域中返回。
POLLWRNORM 寫普通數據不會導致阻塞 POLLWRBAND 寫優先數據不會導致阻塞 POLLMSGSIGPOLL 消息可用
當需要監聽多個事件時,使用POLLIN | POLLRDNORM設置 events 域;當poll調用之後檢測某事件是否發生時,fds[i].revents & POLLIN進行判斷。2.3 epoll詳解
epoll在Linux2.6內核正式提出,是基於事件驅動的I/O方式,相對於select和poll來說,epoll沒有描述符個數限制,使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。優點如下:
- 沒有最大並發連接的限制,能打開的fd上限遠大於1024(1G的內存能監聽約10萬個埠)
- 採用回調的方式,效率提升。只有活躍可用的fd才會調用callback函數,也就是說 epoll 只管你「活躍」的連接,而跟連接總數無關,因此在實際的網路環境中,epoll的效率就會遠遠高於select和poll。
- 內存拷貝。使用mmap()文件映射內存來加速與內核空間的消息傳遞,減少複製開銷。
水平觸發:默認工作模式,即當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);
- epoll_create函數創建一個epoll句柄,參數size表明內核要監聽的描述符數量。調用成功時返回一個epoll句柄描述符,失敗時返回-1。
- epoll_ctl函數註冊要監聽的事件類型。四個參數解釋如下:
epfd表示epoll句柄;
op表示fd操作類型:EPOLL_CTL_ADD(註冊新的fd到epfd中),EPOLL_CTL_MOD(修改已註冊的fd的監聽事件),EPOLL_CTL_DEL(從epfd中刪除一個fd) fd是要監聽的描述符; 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;
- epoll_wait函數等待事件的就緒,成功時返回就緒的事件數目,調用失敗時返回 -1,等待超時返回 0。 epfd是epoll句柄 events表示從內核得到的就緒事件集合 maxevents告訴內核events的大小 timeout表示等待的超時事件
上述三個系統調用的實際實例可參考IO多路復用:select、poll、epoll示例和Linux高性能伺服器編程。
3 小結
epoll是Linux目前大規模網路並發程序開發的首選模型。在絕大多數情況下性能遠超select和poll。目前流行的高性能web伺服器Nginx正式依賴於epoll提供的高效網路套接字輪詢服務。但是,在並發連接不高的情況下,多線程+阻塞I/O方式可能性能更好。
推薦閱讀:
※如何用 Nginx 配置透明 HTTP 和 HTTPS 代理?
※深入實時 Linux
※開始學習 Linux 用什麼發行版比較好?
※我的Linux手冊
TAG:Linux |