1. 程式人生 > >【Socket程式設計】篇六之IO多路複用——select、poll、epoll

【Socket程式設計】篇六之IO多路複用——select、poll、epoll

在上一篇中,我簡單學習了 IO多路複用的基本概念,這裡我將初學其三種實現手段:selectpollepoll

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);
			}		
		}
		else
			printf("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);
			}	
		}
		else
			printf("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: 需要監聽的檔案描述符

event: 告訴核心要監聽什麼事件,struct epoll_event 結構如:

// 儲存觸發事件的某個檔案描述符相關的資料(與具體使用方式有關)  
typedef union epoll_data {  
    void *ptr;  
    int fd;  
    __uint32_t u32;  
    __uint64_t u64;  
} epoll_data_t;  
  
// 感興趣的事件和被觸發的事件  
struct epoll_event {  
    __uint32_t events; /* Epoll events */  
    epoll_data_t data; /* User data variable */  
};  
events 可以是以下幾個巨集的集合:

EPOLLIN :表示對應的檔案描述符可以讀(包括對端 SOCKET 正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被結束通話;
EPOLLET :將 EPOLL 設為邊緣觸發(Edge Trigger)模式,這是相對於水平觸發(Level Trigger)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個 socket 的話,需要再次把這個 socket 加入到 EPOLL 佇列裡

返回值:

成功:0

失敗:-1

int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );

功能:

等待事件的產生,收集在 epoll 監控的事件中已經發送的事件,類似於 select() 呼叫。

引數:

epfd: epoll 專用的檔案描述符,epoll_create()的返回值

events: 分配好的 epoll_event 結構體陣列,epoll 將會把發生的事件賦值到events 陣列中(events 不可以是空指標,核心只負責把資料複製到這個 events 陣列中,不會去幫助我們在使用者態中分配記憶體)。

maxevents: maxevents 告之核心這個 events 有多少個 。

timeout: 超時時間,單位為毫秒,為 -1 時,函式為阻塞。

返回值:

成功:返回需要處理的事件數目,如返回 0 表示已超時

失敗:-1

epoll 對檔案描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)。LT 模式是預設模式LT 模式與 ET 模式的區別如下

LT 模式:支援block和no-block socket。當 epoll_wait 檢測到描述符事件發生並將此事件通知應用程式,應用程式可以不立即處理該事件。下次呼叫 epoll_wait 時,會再次響應應用程式並通知此事件。效率會低於ET觸發,尤其在大併發,大流量的情況下。但是LT對程式碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有資料沒有被獲取,核心就不斷通知你,因此不用擔心事件丟失的情況。
ET 模式:只支援no-block socket。當 epoll_wait 檢測到描述符事件發生並將此事件通知應用程式,應用程式必須立即處理該事件。如果不處理,下次呼叫 epoll_wait 時,不會再次響應應用程式並通知此事件。該模式效率非常高,尤其在高併發,大流量的情況下,會比LT少很多epoll的系統呼叫。但是對程式設計要求高,需要細緻的處理每個請求,否則容易發生丟失事件的情況。

接下來,我們將上面的例子,改為用 epoll 實現:

#include <cstdio>
#include <sys/epoll.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, i;

	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));

	tv.tv_sec = 10;
	tv.tv_usec = 0;
	
	struct epoll_event event;
	struct epoll_event wait_event[2];
	
	int epfd = epoll_create(10);
	assert(epfd != -1);
	
	event.data.fd = connfd1;
	event.events = EPOLLIN;
	assert(epoll_ctl(epfd, EPOLL_CTL_ADD, connfd1, &event) != -1);
	event.data.fd = connfd2;
	event.events = EPOLLIN;
	assert(epoll_ctl(epfd, EPOLL_CTL_ADD, connfd2, &event) != -1);

	
	while(1)
	{
		printf("epoll...\n");
		ret = epoll_wait(epfd, wait_event, 2, -1);
		
		if(ret == -1)
			perror("epoll()");
		else if(ret > 0)
		{	
			for(i = 0; i < ret; ++i)
			{
				if(wait_event[i].data.fd == connfd1 && (wait_event[i].events & EPOLLIN) == EPOLLIN)
				{	
					n = recv(connfd1, buff1, BUFFER_SIZE, 0);
					buff1[n] = '\0';
					printf("connfd1: %s\n", buff1);
				}
				else if(wait_event[i].data.fd == connfd2 && (wait_event[i].events & EPOLLIN) == EPOLLIN)
				{
					n = recv(connfd2, buff2, BUFFER_SIZE, 0);
					buff2[n] = '\0';
					printf("connfd2: %s\n", buff2);
				}	
			}
		}
		else
			printf("time out\n");
	}

	return 0;
}

在 select/poll中,程序只有在呼叫一定的方法後,核心才對所有監視的檔案描述符進行掃描,而 epoll() 事先通過 epoll_ctl() 來註冊一個檔案描述符,一旦某個檔案描述符就緒時,核心會採用類似 callback 的回撥機制(軟體中斷 ),迅速啟用這個檔案描述符,當程序呼叫 epoll_wait() 時便得到通知。

下面分析 select、poll、epoll之間的優缺點:


select:

缺點:

1)每次呼叫select,都存在 fd 集合在使用者態與核心態之間的拷貝,I/O 的效率會隨著監視 fd 的數量的增長而線性下降。
2)select()呼叫的內部,需要用輪詢的方式去完整遍歷每一個 fd,如果遍歷完所有 fd 後沒有發現就緒 fd,則掛起當前程序,直到有 fd 就緒或者主動超時(使用 schedule_timeout 實現睡一會兒,判斷一次(被定時器喚醒,注意與 select() 函式裡面的 timeout 引數區分作用)的效果),被喚醒後它又要再次遍歷 fd (直到有 fd 就緒或 select() 函式超時)。這個過程經歷了多次無謂的遍歷。CPU的消耗會隨著監視 fd 的數量的增長而線性增加

步驟總結如下:
1)先把全部fd掃一遍;
2)如果發現有可用的fd,跳到5;
3)如果沒有,當前程序去睡覺xx秒(schedule_timeout機制);
4)xx秒後自己醒了,或者狀態變化的fd喚醒了自己,跳到1;
5)結束迴圈體,返回。(注意函式的返回也可能是 timeout 的超時)

3)select支援的檔案描述符數量太小了,預設是1024。

4)由於 select 引數輸入和輸出使用同樣的 fd_set ,導致每次 select()  之前都要重新初始化要監視的 fd_set,開銷也會比較大。

poll:

poll 的實現和 select 非常相似,它同樣存在 fd 集合在使用者態和核心態間的拷貝,且在函式內部需要輪詢整個 fd 集合。區別於select 的只是描述fd集合的方式不同,poll使用pollfd陣列而不是select的fd_set結構,所以poll克服了select檔案描述符數量的限制,此外,poll 的 polldf 結構體中分別用 events 和 revents 來儲存輸入和輸出,較之 select() 不用每次呼叫 poll() 之前都要重新初始化需要監視的事件。

epoll:

epoll是一種 Reactor 模式,提供了三個函式,epoll_create(),epoll_ctl() 和 epoll_wait()。

優點:

1)對於上面的第一個缺點,epoll 的解決方案在 epoll_ctl() 函式中。每次註冊新的事件到 epoll 描述符中時,會把該 fd 拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll 保證了每個fd在整個過程中只會拷貝一次。
2)對於第二個缺點,epoll 的解決方案不像 select 或 poll 一樣輪詢 fd,而只在 epoll_ctl 時把要監控的 fd 掛一遍,併為每個 fd 指定一個回撥函式,當裝置就緒,這個回撥函式把就緒的 fd 加入一個就緒連結串列。epoll_wait 的工作實際上就是在這個就緒連結串列中檢視有沒有就緒的 fd,也即 epoll_wait 只關心“活躍”的描述符,而不用像 select() 和 poll() 需要遍歷所有 fd,它需要不斷輪詢就緒連結串列,期間也可能多次睡眠和喚醒(類似與 select, poll),但終究它的輪詢只用判斷就續表是否為空即可,其CPU的消耗不會隨著監視 fd 的數量的增長而線性增加,這就是回撥機制的優勢,也正是 epoll 的魅力所在。

同理,select() 和 poll() 函式返回後, 處理就緒 fd 的方法還是輪詢,如下:

int res = select(maxfd+1, &readfds, NULL, NULL, 120);  
if (res > 0)  
{  
    for (int i = 0; i < MAX_CONNECTION; i++)  
    {  
        if (FD_ISSET(allConnection[i], &readfds))  
        {  
            handleEvent(allConnection[i]);  
        }  
    }  
}  
// if(res == 0) handle timeout, res < 0 handle error 

而 epoll() 只需要從就緒連結串列中處理就緒的 fd:

int res = epoll_wait(epfd, events, 20, -1);  
for (int i = 0; i < res;i++)  
{  
    handleEvent(events[n]);  
}  

此處的效率對比也是高下立判。

3)對於第三個缺點,epoll沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個和系統限制有關,linux裡面可以用ulimit檢視檔案開啟數限制。

缺點:epoll是 linux 特有的,而 select 和 poll 是在 POSIX 中規定的,跨平臺支援更好。

綜上:

select 、poll、epoll 的使用要根據具體的使用場合,並不是 epoll 的效能就一定好,因為回撥函式也是有消耗的,當 socket 連線較少時或者是即使 socket 連線很多,但是連線基本都是活躍的情況下,select / poll 的效能與 epoll 是差不多的。即如果沒有大量的 idle-connection 或者 dead-connection,epoll 的效率並不會比 select/poll 高很多,但是當遇到大量的 idle-connection,就會發現epoll 的效率大大高於 select/poll。