文章參考自:http://blog.csdn.net/tennysonsky/article/details/45745887(秋葉原 — Mike VS 麥克《Linux系統編程——I/O多工select、poll、epoll的區別使用》)
此外,還有一篇好文:epoll機制:epoll_create、epoll_ctl、epoll_wait、close(魚思故淵的專欄)
在上一篇中,我簡單學習了 IO多工基本概念,這裡我將初學其三種實現手段:select,poll,epoll。
I/O 多工是為瞭解決進程或線程阻塞到某個 I/O 系統調用而出現的技術,使進程或線程不阻塞於某個特定的 I/O 系統調用。
select(),poll(),epoll()都是I/O多工機制。I/O多工通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒,就是這個檔案描述符進行讀寫操作之前),能夠通知程式進行相應的讀寫操作。但select(),poll(),epoll()本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。
與多線程(TPC(Thread Per Connection)模型)和多進程(典型的Apache模型(Process Per Connection,簡稱PPC))相比,I/O 多工最大優勢是系統開銷小,系統不需要建立新的進程或者線程,也不必維護這些線程和進程。
select()的使用
所需標頭檔:
#include <sys/select.h>
函數原型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
函數描述:
監視並等待多個檔案描述符的屬性變化(可讀、可寫或錯誤異常)。select()函數監視的檔案描述符分 3 類,分別是writefds、readfds、和 exceptfds。調用後 select() 函數會阻塞,直到有描述符就緒(有資料可讀、可寫、或者有錯誤異常),或者逾時( timeout 指定等待時間),函數才返回。當 select()函數返回後,可以通過遍曆 fdset,來找到就緒的描述符。
參數描述:
nfds: 要監視的檔案描述符的範圍,一般取監視的描述符數的最大值+1,如這裡寫 10, 這樣的話,描述符 0,1, 2 …… 9 都會被監視,在 Linux 上最大值一般為1024。
readfd: 監視的可讀描述符集合,只要有檔案描述符讀操作準備就緒,這個檔案描述符就儲存到這。
writefds: 監視的可寫描述符集合。
exceptfds: 監視的錯誤異常描述符集合。
三個參數 readfds、writefds 和 exceptfds 指定我們要讓核心監測讀、寫和異常條件的描述字。如果不需要使用某一個的條件,就可以把它設為NULL 。
幾個較為重要的宏:
//清空集合void FD_ZERO(fd_set *fdset); //將一個給定的檔案描述符加入集合之中void FD_SET(int fd, fd_set *fdset);//將一個給定的檔案描述符從集合中刪除void FD_CLR(int fd, fd_set *fdset);//檢查集合中指定的檔案描述符是否可以讀寫 int FD_ISSET(int fd, fd_set *fdset); timeout: 逾時時間,它告知核心等待所指定描述字中的任何一個就緒可花多少時間。其 timeval 結構用於指定這段時間的秒數和微秒數。struct timeval{time_t tv_sec; /* 秒 */suseconds_t tv_usec; /* 微秒 */};
三種可能的函數返回情況:
1)永遠等待下去:timeout 設定為空白指標 NULL,且沒有一個描述符準備好。
2)等待固定時間:timeout 設定為某個固定時間,在有一個描述符準備好時返回,如果時間到了,就算沒有檔案描述符準備就緒,這個函數也會返回 0。
3)不等待(不阻塞):檢查描述字後立即返回,這稱為輪詢。為此,struct timeval變數的時間值指定為 0 秒 0 微秒,檔案描述符屬性無變化返回 0,有變化返回準備好的描述符數量。
函數傳回值:
成功:就緒描述符的數目(同時修改readfds、writefds 和 exceptfds 三個參數),逾時返回 0;
出錯:-1。
下面用 Socket 舉例,兩個用戶端,其中一個每隔 5s 發一個固定的字串到伺服器,另一個採集終端的鍵盤輸入,將其發給伺服器,一個伺服器,使用 IO 多工處理這兩個用戶端。代碼如下:
伺服器:
#include <cstdio>#include <sys/select.h>#include <unistd.h>#include <stdlib.h>#include <cstring>#include <cassert>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>const int BUFFER_SIZE = 4096;const int SERVER_PORT = 2222;inline int max(int a, int b){ return (a > b ? : a, b);}int main(){int server_socket;char buff1[BUFFER_SIZE];char buff2[BUFFER_SIZE];fd_set rfds;struct timeval tv;int ret;int n;server_socket = socket(AF_INET, SOCK_STREAM, 0);assert(server_socket != -1);struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);server_addr.sin_addr.s_addr = htonl(INADDR_ANY);assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);assert(listen(server_socket, 5) != -1);struct sockaddr_in client_addr1, client_addr2;socklen_t client_addr_len = sizeof(struct sockaddr_in);printf("waiting...\n");//此處先建立兩個 TCP 串連int connfd1 = accept(server_socket, (struct sockaddr*)&client_addr1, &client_addr_len);assert(connfd1 != -1);printf("connect from %s:%d\n", inet_ntoa(client_addr1.sin_addr), ntohs(client_addr1.sin_port));int connfd2 = accept(server_socket, (struct sockaddr*)&client_addr2, &client_addr_len);assert(connfd2 != -1);printf("connect from %s:%d\n", inet_ntoa(client_addr2.sin_addr), ntohs(client_addr2.sin_port));while(1){FD_ZERO(&rfds);FD_SET(connfd1, &rfds);FD_SET(connfd2, &rfds);tv.tv_sec = 10;tv.tv_usec = 0;printf("select...\n");ret = select(max(connfd1, connfd2) + 1, &rfds, NULL, NULL, NULL);//ret = select(max(connfd1, connfd2) + 1, &rfds, NULL, NULL, &tv);if(ret == -1)perror("select()");else if(ret > 0){if(FD_ISSET(connfd1, &rfds)){n = recv(connfd1, buff1, BUFFER_SIZE, 0);buff1[n] = '\0';//注意手動添加字串結束符printf("connfd1: %s\n", buff1);}if(FD_ISSET(connfd2, &rfds)){n = recv(connfd2, buff2, BUFFER_SIZE, 0);buff2[n] = '\0';//注意手動添加字串結束符printf("connfd2: %s\n", buff2);}}elseprintf("time out\n");}return 0;}
用戶端1:
#include <cstdio>#include <unistd.h>#include <stdlib.h>#include <cstring>#include <cassert>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>const int BUFFER_SIZE = 4096;const int SERVER_PORT = 2222;int main(){int client_socket;const char *server_ip = "127.0.0.1";char buffSend[BUFFER_SIZE] = "I'm from d.cpp";client_socket = socket(AF_INET, SOCK_STREAM, 0);assert(client_socket != -1);struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);server_addr.sin_addr.s_addr = inet_addr(server_ip);assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);while(1){assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1);sleep(5);}close(client_socket);return 0;}
用戶端2:
#include <cstdio>#include <unistd.h>#include <stdlib.h>#include <cstring>#include <cassert>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>const int BUFFER_SIZE = 4096;const int SERVER_PORT = 2222;int main(){int client_socket;const char *server_ip = "127.0.0.1";char buffSend[BUFFER_SIZE];client_socket = socket(AF_INET, SOCK_STREAM, 0);assert(client_socket != -1);struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);server_addr.sin_addr.s_addr = inet_addr(server_ip);assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);while(1){fgets(buffSend, BUFFER_SIZE, stdin);assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1);}close(client_socket);return 0;}
以上三份代碼有缺陷,代碼沒有很好的結束方式,都是 while(1) 死迴圈,啟動並執行結束需要用 Ctrl + c ⊙﹏⊙
poll()的使用
select() 和 poll() 系統調用的本質一樣,前者在 BSD UNIX 中引入的,後者在 System V 中引入的。poll() 的機制與 select() 類似,與 select() 在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是 poll() 沒有最大檔案描述符數量的限制(但是數量過大後效能也是會下降)。poll() 和 select() 同樣存在一個缺點就是,包含大量檔案描述符的數組被整體複製於使用者態和核心的地址空間之間,而不論這些檔案描述符是否就緒,它的開銷隨著檔案描述符數量的增加而線性增大。
所需標頭檔:
#include <poll.h>
函數原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函數描述:
監視並等待多個檔案描述符的屬性變化。
函數參數:
1)fds:與 select() 使用三個 fd_set 的方式不同,poll() 使用一個 pollfd 的指標實現。一個 pollfd 結構體數組,其中包括了你想監視的檔案描述符和事件, 事件由結構中事件域 events 來確定,調用後實際發生的事件將被填寫在結構體的 revents 域。
struct pollfd{int fd; /* 檔案描述符 */short events; /* 等待的事件 */short revents; /* 實際發生了的事件 */};
_1、fd:每一個 pollfd 結構體指定了一個被監視的檔案描述符,可以傳遞多個結構體,指示 poll() 監視多個檔案描述符。
_2、events:每個結構體的 events 域是監視該檔案描述符的事件掩碼,由使用者來設定這個域。
_3、revents:revents 域是檔案描述符的操作結果事件掩碼,核心在調用返回時設定這個域。events 域中請求的任何事件都可能在 revents 域中返回。
事件的掩碼取值如下:
POLLIN | POLLPRI 等價於 select() 的讀事件,POLLOUT | POLLWRBAND 等價於 select() 的寫事件。POLLIN 等價於 POLLRDNORM | POLLRDBAND,而 POLLOUT 則等價於 POLLWRNORM 。例如,要同時監視一個檔案描述符是否可讀和可寫,我們可以設定 events 為 POLLIN | POLLOUT。
每個結構體的 events 域是由使用者來設定,告訴核心我們關注的是什麼,而 revents 域是返回時核心設定的,以說明對該描述符發生了什麼事件。
2)nfds:用來指定第一個參數數組元素個數。
3)timeout: 指定等待的毫秒數,無論 I/O 是否準備好,poll() 都會返回。
函數傳回值:
成功時,poll() 返回結構體中 revents 域不為 0 的檔案描述符個數,如果在逾時前沒有任何事件發生,poll()返回 0;
失敗時,poll() 返回 -1。
此處我們將上面的例子用 poll() 重新實現如下,只用修改伺服器端代碼:
#include <cstdio>#include <poll.h>#include <unistd.h>#include <stdlib.h>#include <cstring>#include <cassert>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>const int BUFFER_SIZE = 4096;const int SERVER_PORT = 2222;int main(){int server_socket;char buff1[BUFFER_SIZE];char buff2[BUFFER_SIZE];struct timeval tv;int ret;int n;server_socket = socket(AF_INET, SOCK_STREAM, 0);assert(server_socket != -1);struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);server_addr.sin_addr.s_addr = htonl(INADDR_ANY);assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);assert(listen(server_socket, 5) != -1);struct sockaddr_in client_addr1, client_addr2;socklen_t client_addr_len = sizeof(struct sockaddr_in);printf("waiting...\n");int connfd1 = accept(server_socket, (struct sockaddr*)&client_addr1, &client_addr_len);assert(connfd1 != -1);printf("connect from %s:%d\n", inet_ntoa(client_addr1.sin_addr), ntohs(client_addr1.sin_port));int connfd2 = accept(server_socket, (struct sockaddr*)&client_addr2, &client_addr_len);assert(connfd2 != -1);printf("connect from %s:%d\n", inet_ntoa(client_addr2.sin_addr), ntohs(client_addr2.sin_port));struct pollfd rfds[2];rfds[0].fd = connfd1;rfds[0].events = POLLIN;rfds[1].fd = connfd2;rfds[1].events = POLLIN;tv.tv_sec = 10;tv.tv_usec = 0;while(1){printf("poll...\n");ret = poll(rfds, 2, -1);if(ret == -1)perror("poll()");else if(ret > 0){if((rfds[0].revents & POLLIN) == POLLIN){n = recv(connfd1, buff1, BUFFER_SIZE, 0);buff1[n] = '\0';printf("connfd1: %s\n", buff1);}if((rfds[1].revents & POLLIN) == POLLIN){n = recv(connfd2, buff2, BUFFER_SIZE, 0);buff2[n] = '\0';printf("connfd2: %s\n", buff2);}}elseprintf("time out\n");}return 0;}
epoll()的使用
epoll 是在核心 2.6 中提出的,是之前的 select() 和 poll() 的增強版本。相對於 select() 和 poll() 來說,epoll 更加靈活,沒有描述符限制。epoll 使用一個檔案描述符管理多個描述符,將使用者關心的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的 copy 只需一次。
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);
int epoll_create(int size);
功能:
該函數產生一個 epoll 專用的檔案描述符。
參數:
size: 用來告訴核心這個監聽的數目一共有多大,參數 size 並不是限制了 epoll 所能監聽的描述符最大個數,只是對核心初始分配內部資料結構的一個建議。自從 linux 2.6.8 之後,size 參數是被忽略的,也就是說可以填只有大於 0 的任意值。
傳回值:
成功:epoll 專用的檔案描述符
失敗:-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:
epoll 的事件註冊函數,它不同於 select() 是在監聽事件時告訴核心要監聽什麼類型的事件,而是在這裡先註冊要監聽的事件類型。
參數:
epfd: epoll 專用的檔案描述符,epoll_create()的傳回值
op: 表示動作,用三個宏來表示:
EPOLL_CTL_ADD:註冊新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從 epfd 中刪除一個 fd;
fd: 需要監聽的檔案描述符