Linux I/O複用

epoll

首先看個結構體

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 */
 }

struct epoll_event的成員events是個bit set,有幾種類型:

EPOLLIN:關聯的文件是用來讀的

EPOLLOUT:關聯的文件是用來寫的

EPOLLET:Edge Trigger,與之對應的是Level Trigger,下面會詳細介紹它們的區別。需要注意的是Level Trigger是默認模式,在我這邊(linux-2.6.32)頭文件sys/epoll.h中已經沒有EPOLLLT的定義了,所以在代碼中不要再顯式地寫EPOLLLT了,反正默認情況用的就是它。select和poll都相當於epoll中的Level Trigger模式。

定義兩個變量,後面會用。

struct epoll_event  event, events[20];

epoll系列有3組函數:

  1. int  epfd=epoll_create(int size);    //創建一個epoll實例。size表示建議內核開闢的空間。
  2. int nfds=epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);    //準備好讀/寫的事件存放在參數events中,maxevents是同時監聽的最大事件數,timeout是超時返回。
  3. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);      //op的取值有:EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL,表示你要從監聽集中添加、去除或修改某個文件描述符。

看個例子就知道該怎麼用了:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<unistd.h>
#include<errno.h>

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

int main(){
	int i,maxi,listenfd,connfd,sockfd,epfd,nfds;
	int n;
	int yes=1;
	char line[MAXLINE+1];
	socklen_t clilen;
	//聲明epoll_event結構體變量,ev用於註冊事件,數組用於回傳要處理的事件
	struct epoll_event ev,events[20];
	//生成用於處理accept的epoll專用文件描述符
	epfd=epoll_create(256);
	struct sockaddr_in serveraddr;
	struct sockaddr_in clientaddr;
	listenfd=socket(PF_INET,SOCK_STREAM,0);
	//設置套接口選項
	setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int));
	//設置與要處理的事件相關的文件描述符
	ev.data.fd=listenfd;
	//設置要處理的事件類型
	ev.events=EPOLLET|EPOLLIN;
	//註冊epoll事件
	epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

	bzero(&serveraddr,sizeof(serveraddr));
	serveraddr.sin_family=AF_INET;
	char *local_addr="127.0.0.1";
	inet_pton(AF_INET,local_addr,&(serveraddr.sin_addr));
	serveraddr.sin_port=htons(SERV_PORT);
	bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
	listen(listenfd,LISTENQ);
	maxi=0;
	while(1){
		//等待epoll事件的發生
		nfds=epoll_wait(epfd,events,20,500);
		//處理所發生的事件
		for(i=0;i<nfds;++i){
			if(events[i].data.fd==listenfd){
				clilen=sizeof(clientaddr);
				connfd=accept(listenfd,(struct sockaddr*)&clientaddr,&clilen);
				if(connfd<0){
					perror("connfd<0");
					exit(1);
				}
				char *str=inet_ntoa(clientaddr.sin_addr);
				printf("accept a connection from %s\n",str);
				//設置用於讀操作的文件描述符
				ev.data.fd=connfd;
				//設置用於註冊的讀操作事件
				ev.events=EPOLLET|EPOLLIN;
				//註冊ev
				epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
			}
			else if(events[i].events & EPOLLIN){
				sockfd = events[i].data.fd;
				printf("read:");
				if((n=read(sockfd,line,MAXLINE))<0){
					printf("read error,close connection\n");
					epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ev);
					close(sockfd);			
				}
				line[n]='\0';
				printf("|%s|\n",line);
			}
		}
	}
	return 0;
}

上述代碼中,服務端建好套接字listenfd後開始監聽它,並把它放到epoll中(第38行)。當有新的連接到來時,第53行的if語句成立,服務端accept該連接,並把該連接的描述符connfd放入epoll中(第67行)。如果TCP連接上有可讀事件發生,則第69行的if語句成立,服務端從連接上讀取數據後打印在標準輸出上,如果讀取時發生錯誤則關閉該連接,同時把相應的connfd從epoll中移除。

下面給一個客戶端代碼負責寫服務端寫入數據。

#!/usr/bin/perl

use IO::Socket;

my $host="127.0.0.1";
my $port=5000;

my $socket=IO::Socket::INET->new("$host:$port") or die "create socket error $@";
my $msg_out="1234567890";
print $socket $msg_out;
print "now send over,go to sleep...\n";

while(1){
	sleep(1);
}

客戶端向服務端寫入“1234567890”後並沒有關閉連接,而是進入了永久的休眠。

運行程序服務端輸出:

accept a connection from 127.0.0.1
read:|12345|

爲什麼只輸出了前5個字節?首先要清楚,上層應用在調用send、recv在TCP連接上收發數據時,send並沒有真正地向網絡對端發送數據,發送數據的工作中由TCP協議完成的。send僅是檢查套接口的發送緩存是否有足夠的空間,如果有則send直接將要發送的數據放入緩存區,sned返回成功;如果緩存空間不足,則send阻塞(如果沒有設置O_NONBLOCK的話),直到TCP協議發送完緩存區中原有的數據。recv也同樣。

Edge Trigger僅當有讀/寫事件發生時它才觸發,上例中client僅發送了一次數據,所以在server端只讀取一次,只從緩存區中讀取了前MAXLINE(爲5)個字節。

Level Trigger只要緩存區中還有數據可讀就還會觸發。下面的代碼採用Level Trigger。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<unistd.h>
#include<errno.h>

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

int main(){
	int i,maxi,listenfd,connfd,sockfd,epfd,nfds;
	int n;
	int yes=1;
	char line[MAXLINE+1];
	socklen_t clilen;
	struct epoll_event ev,events[20];
	epfd=epoll_create(256);
	struct sockaddr_in serveraddr;
	struct sockaddr_in clientaddr;
	listenfd=socket(PF_INET,SOCK_STREAM,0);
	setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int));
	ev.data.fd=listenfd;
	ev.events=EPOLLIN;
	epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

	bzero(&serveraddr,sizeof(serveraddr));
	serveraddr.sin_family=AF_INET;
	char *local_addr="127.0.0.1";
	inet_pton(AF_INET,local_addr,&(serveraddr.sin_addr));
	serveraddr.sin_port=htons(SERV_PORT);
	bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
	listen(listenfd,LISTENQ);
	maxi=0;
	while(1){
		nfds=epoll_wait(epfd,events,20,500);
		for(i=0;i<nfds;++i){
			if(events[i].data.fd==listenfd){
				clilen=sizeof(clientaddr);
				connfd=accept(listenfd,(struct sockaddr*)&clientaddr,&clilen);
				if(connfd<0){
					perror("connfd<0");
					exit(1);
				}
				char *str=inet_ntoa(clientaddr.sin_addr);
				printf("accept a connection from %s\n",str);
				ev.data.fd=connfd;
				ev.events=EPOLLIN;
				epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
			}
			else if(events[i].events & EPOLLIN){
				sockfd = events[i].data.fd;
				printf("read:");
				if((n=read(sockfd,line,MAXLINE))<0){
					printf("read error,close connection\n");
					epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ev);
					close(sockfd);			
				}
				line[n]='\0';
				printf("|%s|\n",line);
			}
		}
	}
	return 0;
}

運行輸出:

accept a connection from 127.0.0.1
read:|12345|
read:|67890|

我們要分兩種情況來討論read()系統調用:

ssize_t read(int fd, void *buf, size_t count);

(1)讀取文件。出錯時返回-1;成功時返回讀到的字節數,這個數字大部分情況下等於count,只有當文件中剩餘的內容不足count時返回值才小於count,如果已讀到文件末尾則返回0。所以要想讀取一個文件的全部內容,只需要把read()放到while(true)循環中,當read()的返回值小於count時,就可以肯定文件讀完了,可以退出循環了。read()系統調用在用戶空間是不設緩存的。

(2)讀取套接口。說得更具體些是讀取本地socket的接收緩存。這裏跟讀取文件的不同之處在於:當接收緩存已無數據可讀時,read()不會返回0(如果沒有設置O_NONBLOCK的話),而是一直阻塞,除非對方斷開連接read()才返回0。

要想在Edge Trigger模式下讀取緩衝區中的所有數據呢,必須和O_NONBLOCK綜合使用,此時當接收緩衝區無數據可讀時read()返回一個負值(注意不是0),並且把errno置爲EAGAIN或EWOULDBLOCK。這裏解釋一下read()系統調用在返回失敗時的幾種可能情況:

返回EINTR:在read之前收到信號被中斷。

返回EAGAIN:針對普通的文件描述符(不包括套接口描述符),當設置了O_NONBLOCK,而緩存區中又沒有數據可讀,則返回該值。

返回EWOULDBLOCK:跟EAGAIN類似,只是EWOULDBLOCK專用於套接口描述符。

在有的系統中EAGAIN也可用於套接口描述符,所以爲增強可移植性,代碼中應該使用對EAGAIN和EWOULDBLOCK都進行檢測。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<unistd.h>
#include<errno.h>

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

void setnonblocking(int sock){
	int opts;
	opts=fcntl(sock,F_GETFL);
	if(opts<0){
		perror("fcntl_get");
		exit(1);
	}
	opts=opts|O_NONBLOCK;
	if(fcntl(sock,F_SETFL,opts)<0){
		perror("fcntl_set");
		exit(1);
	}
}

int main(){
	int i,maxi,listenfd,connfd,sockfd,epfd,nfds;
	int n;
	int yes=1;
	char line[MAXLINE+1];
	socklen_t clilen;
	//聲明epoll_event結構體變量,ev用於註冊事件,數組用於回傳要處理的事件
	struct epoll_event ev,events[20];
	//生成用於處理accept的epoll專用文件描述符
	epfd=epoll_create(256);
	struct sockaddr_in serveraddr;
	struct sockaddr_in clientaddr;
	listenfd=socket(PF_INET,SOCK_STREAM,0);
	//設置套接口選項
	setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int));
	//把socket設爲非阻塞
	setnonblocking(listenfd);
	//設置與要處理的事件相關的文件描述符
	ev.data.fd=listenfd;
	//設置要處理的事件類型
	ev.events=EPOLLET|EPOLLIN;
	//註冊epoll事件
	epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

	bzero(&serveraddr,sizeof(serveraddr));
	serveraddr.sin_family=AF_INET;
	char *local_addr="127.0.0.1";
	inet_pton(AF_INET,local_addr,&(serveraddr.sin_addr));
	serveraddr.sin_port=htons(SERV_PORT);
	bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
	listen(listenfd,LISTENQ);
	maxi=0;
	while(1){
		//等待epoll事件的發生
		nfds=epoll_wait(epfd,events,20,500);
		//處理所發生的事件
		for(i=0;i<nfds;++i){
			if(events[i].data.fd==listenfd){
				clilen=sizeof(clientaddr);
				connfd=accept(listenfd,(struct sockaddr*)&clientaddr,&clilen);
				if(connfd<0){
					perror("connfd<0");
					exit(1);
				}
				setnonblocking(connfd);
				char *str=inet_ntoa(clientaddr.sin_addr);
				printf("accept a connection from %s\n",str);
				//設置用於讀操作的文件描述符
				ev.data.fd=connfd;
				//設置用於註冊的讀操作事件
				ev.events=EPOLLET|EPOLLIN;
				//註冊ev
				epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
			}
			else if(events[i].events & EPOLLIN){
				sockfd = events[i].data.fd;
				printf("read:");
				while(1){
					n=read(sockfd,line,MAXLINE);
					if(n<0){
						if(errno==EAGAIN || errno==EWOULDBLOCK){
							break;
						}
					}
					else if(n==0){
						break;
					}
					else{
						line[n]='\0';
						printf("|%s|",line);
					}
				}
				printf("\n");		//printf是行緩衝的,如果一直不輸出換行符,第88行和101的內容就不會打印出來的
			}
		}
	}
	return 0;
}

運行輸出:

accept a connection from 127.0.0.1
read:|12345||67890|

邊緣觸發是一種高速工作模式,編程上稍微複雜一些。但是在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。

最後把上面服務端的程序再擴充一下,server端收到client發來的數據後還要回應一段message,重要的是這個寫事件也要放到epoll中。

	nfds=epoll_wait(epfd,events,20,500);
	for(i=0;i<nfds;++i){
		if(events[i].data.fd==listenfd){
			clilen=sizeof(clientaddr);
			connfd=accept(listenfd,(struct sockaddr*)&clientaddr,&clilen);
			if(connfd<0){
				perror("connfd<0");
				exit(1);
			}
			char *str=inet_ntoa(clientaddr.sin_addr);
			printf("accept a connection from %s\n",str);
			ev.data.fd=connfd;
			ev.events=EPOLLIN;
			epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
		}
		else if(events[i].events & EPOLLIN){
			sockfd = events[i].data.fd;
			printf("read:");
			if((n=read(sockfd,line,MAXLINE))<0){
				printf("read error,close connection\n");
				epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ev);
				close(sockfd);			
			}
			line[n]='\0';
			printf("|%s|\n",line);
			
			ev.data.fd=sockfd;
			//設置用於註冊的寫操作事件
			ev.events=EPOLLOUT;
			//修改sockfd上要處理的事件爲EPOLLOUT
			epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
		}
		else if(events[i].events & EPOLLOUT){
			printf("ready to write\n");
			sockfd=events[i].data.fd;
			write(sockfd,line,n);
			//設置用於讀操作的文件描述符
			ev.data.fd=sockfd;
			//設置用於註冊的讀操作事件
			ev.events=EPOLLIN;
			//修改sockfd上要處理的事件爲EPOLLIN
			epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
		}
	}
}

運行輸出:

accept a connection from 127.0.0.1
read:|12345|
ready to write
read:|67890|
ready to write

select

select允許程序掛起,並等待從不止一個文件描述符的輸入,即程序掛起直到有任何一個文件描述符的數據到達。select設置一個變量中的若干位,用來通知哪一個文件描述符已經有數據到達。

#include<sys/types.h>

#include<sys/time.h>

#include<unistd.h>

int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exeptfds,struct timeval*timeout)

numfds是要檢查的所有文件描述符中號碼最大的加1

readfds讀文件描述符集合

writefds寫文件描述符集合

exeptfds異常處理文件描述符集合

timeout具體值:

NULL:永遠等待,直到捕捉到信號或文件描述符已準備好爲止

struct timeval 類型的指針,若等待爲 timeout 時間還沒有文件描符準備好,就立即返回

0:從不等待,測試所有指定的描述符並立即返回

返回值:readfds、writefds和exeptfds中準備好的fd數目,當觸發time expire時會返回0,發生錯誤時返回-1。

下面的宏提供了處理這三種描述詞組的方式:
FD_CLR(inr fd,fd_set* set);用來清除描述詞組set中相關fd 的位
FD_ISSET(int fd,fd_set *set);用來測試描述詞組set中相關fd 的位是否爲真
FD_SET(int fd,fd_set*set);用來設置描述詞組set中相關fd的位
FD_ZERO(fd_set *set);用來清除描述詞組set的全部位

參數timeout爲結構timeval,用來設置select()的等待時間,其結構定義如下:

struct timeval 
{ 
    time_t tv_sec;//second 
    time_t tv_usec;//minisecond  
};

理解select模型的關鍵在於理解fd_set,爲說明方便,取fd_set長度爲1字節,fd_set中的每一bit可以對應一個文件描述符fd。則1字節長的fd_set最大可以對應8個fd。

(1)執行fd_set set; FD_ZERO(&set);則set用位表示是0000,0000。

(2)若fd=5,執行FD_SET(fd,&set);後set變爲0001,0000(第5位置爲1)

(3)若再加入fd=2,fd=1,則set變爲0001,0011

(4)執行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變爲0000,0011。注意:沒有事件發生的fd=5被清空。

可監控的文件描述符個數取決與sizeof(fd_set)的值。我這邊PC上sizeof(fd_set)=128,每bit表示一個文件描述符,則我服務器上支持的最大文件描述符是128*8=1024。

下面的代碼監聽標準輸入上是否有輸入數據,如果有就把它輸出到標準輸出上。

#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
 
int main(){
    fd_set rset;
    FD_ZERO(&rset);
    FD_SET(0,&rset);
 
    struct timeval t;
    t.tv_sec=4;
    t.tv_usec=500000;
 
    int ret=select(1,&rset,NULL,NULL,&t);
    char buf[10]="";
    if(ret>0){
    if(FD_ISSET(0,&rset)){
        read(0,buf,sizeof(buf));
        write(1,buf,strlen(buf));
    }
    }
    return 0;
}

上面只是個簡單示例,來個複雜一點的,select用於socket編程,實現跟epoll相同的功能。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

#define MYPORT 5000
#define BACKLOG 2		//TCP層接收鏈接池的緩衝隊列大小
#define BUF_SIZE 200	//用於讀寫網絡數據的內存緩衝大小

int fd_A[BACKLOG];		//存放處於連接中的socket描述符
int conn_amount;		//目前的TCP連接數量

//顯示目前有幾個工作的TCP連接,以及相應的socket描述符
void showclient(){
	int i;
	printf("client amount:%d\nready file descriptor:",conn_amount);
	for(i=0;i<conn_amount;++i)
		printf("%d ",fd_A[i]);
	printf("\n");
}

int main(){
	int sock_fd,new_fd;
	struct sockaddr_in server_addr;
	struct sockaddr_in client_addr;
	socklen_t sin_size;
	int yes=1;
	char buf[BUF_SIZE];
	int ret;
	int i;

	if((sock_fd=socket(PF_INET,SOCK_STREAM,0))==-1){
		perror("socket");
		exit(1);
	}

	if(setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int))==-1){
		perror("socket");
		exit(1);
	}

	server_addr.sin_family=AF_INET;
	server_addr.sin_port=htons(MYPORT);
	server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	memset(server_addr.sin_zero,'\0',sizeof(server_addr.sin_zero));

	if(bind(sock_fd,(struct sockaddr*)&server_addr,sizeof(server_addr))==-1){
		perror("bind");
		exit(1);
	}

	if(listen(sock_fd,BACKLOG)==-1){
		perror("listen");
		exit(1);
	}

	printf("listen on port %d\n",MYPORT);

	fd_set fdsr;
	int maxsock;		//存放在監視的最大文件描述符
	struct timeval tv;

	conn_amount=0;		//初始連接數量爲0
	sin_size=sizeof(client_addr);
	maxsock=sock_fd;
	while(1){
		FD_ZERO(&fdsr);		//清空fdsr
		FD_SET(sock_fd,&fdsr);
		for(i=0;i<BACKLOG;++i){
			if(fd_A[i]!=0){
				FD_SET(fd_A[i],&fdsr);			//把準備就緒的連接全部放入fdsr中
			}
		}
		
		tv.tv_sec=30;
		tv.tv_usec=0;

		//監聽集合fdsr
		ret=select(maxsock+1,&fdsr,NULL,NULL,&tv);
		if(ret<0){
			perror("select");
			break;
		}else if(ret==0){
			printf("time out\n");
			continue;
		}
		
		//逐一遍歷每個連接,看其是否就緒。若是則讀取其上的數據,並返回一串消息
		for(i =0;i<conn_amount;i++){
			if(FD_ISSET(fd_A[i],&fdsr)){
				ret=recv(fd_A[i],buf,sizeof(buf),0);
				char str[]="Good,very nice!\n";
				send(fd_A[i],str,sizeof(str)+1,0);
				if(ret<=0){
					printf("client [%d] close\n",i);
					close(fd_A[i]);
					FD_CLR(fd_A[i],&fdsr);
					fd_A[i]=0;
				}else{
					if(ret<BUF_SIZE)		//若數據量超過了BUF_SIZE,則截斷之
						memset(&buf[ret],'\0',1);
					printf("client [%d] send:%s\n",i,buf);
				}
			}
		}

		if(FD_ISSET(sock_fd,&fdsr)){		//有新的連接請求
			new_fd=accept(sock_fd,(struct sockaddr*)&client_addr,&sin_size);
			if(new_fd<=0){
				perror("accept");
				continue;
			}

			if(conn_amount<BACKLOG){		
				fd_A[conn_amount++]=new_fd;			//把新的連接socket描述符放到fd_A數組中
				printf("accept connecton from %s\n",inet_ntoa(client_addr.sin_addr));
				if(new_fd>maxsock)			//更新maxsock
					maxsock=new_fd;
			}
			else{			//如果連接數達到了BACKLOG,則將最後到來的連接關閉掉,不處理它
				printf("max connection arrive,close the last connection\n");
				send(new_fd,"bye",4,0);
				close(new_fd);
			}
		}
		showclient();
	}

	for(i=0;i<BACKLOG;i++){
		if(fd_A[i]!=0)
			close(fd_A[i]);
	}
	exit(0);
}

在許多測試中我們會看到如果沒有大量的idle-connection或者dead-connection,epoll的效率並不會比 select/poll高很多,但是當我們遇到大量的idle-connection(例如WAN環境中存在大量的慢速連接),就會發現epoll的效率 大大高於select/poll。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章