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組函數:
- int epfd=epoll_create(int size); //創建一個epoll實例。size表示建議內核開闢的空間。
- int nfds=epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); //準備好讀/寫的事件存放在參數events中,maxevents是同時監聽的最大事件數,timeout是超時返回。
-
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。