系統提供select函數用來實現I/O多路複用輸入/輸出模型。select系統調用是用來讓我們的程序監視多個文件描述狀態變化的。程序會停在select這裏等待,直到被監視的文件描述符有一個或多個發生狀態變化。通常I/O操作有兩個步驟,一個是等,另一個是數據搬遷。select主要是在等的這個狀態阻塞着直到事件發生。
頭文件:
#include<sys/select.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/time.h>
函數原型
int select(int nfds,fd_set *reads,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
參數:
nfds:是需要監視的最大的文件描述符的值+1。
fd_set:
fd_set底層是用位圖實現的,每一個位都代表一個文件描述符。readfds,writefds,exceptfds分別對應於需要檢測的可讀文件描述符結合,可寫描述符集合,異常文件描述符集合,他們都是輸入輸出型參數。
當作爲輸入參數時:只要文件描述符集合中對應的位上爲1,就表示select需要監視這個描述符的狀態。比如readfds裏面的文件描述符就代表他們需要等待讀事件,writefds裏面的文件描述符就代表他們需要等待寫事件。
當作爲輸出參數時,只要文件描述符集合中對應的位上爲1,就代表他們等待的事件已經就緒,這是由內核設定的。
timeout:設置超時時間。
timeout裏面的成員設定爲特定的時間值:
如果在這段時間裏面沒有事件發生,select將超時返回。struct timeval結構用於描述一段時間長度,如果在這個時間內,需要監聽的描述符沒有事件發生則函數返回,返回值爲0。
struct timeval
{
long tv_sec; //秒
long tv_usec; //微秒
}
timeout裏面的成員等於0:表示非阻塞輪詢方式,不斷的去檢測描述符集合的狀態,然後立即返回。
timeout爲NULL:表示以阻塞的方式等待事件發生。
返回值:
成功的話,返回文件描述符狀態已改變的個數。如果返回0代表在描述符狀態改變之前已經超過timeout時間。如果有錯誤發生的話,則返回-1。
下面的宏用來處理描述符集合:
void FD_CLR(int fd,fd_set* set); 用來清除描述符詞組set中相關fd的位。
int FD_ISSET(int fd,fd_set* set); 用來測試set中相關fd的位是否爲真。
void FD_SET(int fd,fd_set* set); 用來設置描述詞組set中相關fd的位。
void FD_ZERO(fd_set *set);用來清除描述符詞組set的全部位。
select模型:
select可監控的描述符取決於sizeof(fd_set)的值,因爲文件描述符是用位圖表示的,所以能監控的描述符的最大數量是sizeof(fd_set)*8,fd_set的大小可以調整。
將fd加入select監控集的同時,還要使用一個額外的數組保存select監控集中的fd。一方面是用於在select返回後,array作爲源數據和fd_set進行FD_ISSET判斷事件是否就緒。另一方面是select返回之後會把以前加入的但並無事件發生fd清空,這是由內核清空的,所以每次開始select前都要重新從array中取得fd加入到fd_set中。
還有就是因爲select第一個參數是當前要監測的文件描述符的最大值加1,可以在掃描array的同時取得fd的最大值maxfd,用於select的第一個參數。
所以select的缺點就是,每次selcet之前都要遍歷數組加入fd,select返回後還要遍歷數組進行判斷哪些事件已經就緒(FD_ISSET判斷是否有事件發生)。
select的缺點:
1、每次調用select,都需要把fd集合從用戶態拷貝到內核態。這個開銷在fd很多的時候會很大。
2、select在返回之後,需要我們遍歷數組去查找事件就緒的描述符。這個過程的時間複雜度是O(N)。而epoll它查找就緒事件的時候是O(1)。
3、select支持的文件描述符的數量太小了,默認是1024。
總結:針對select的缺點來看,即時fd_set可以改動,也不建議將它改的很大,因爲一但支持的文件描述多了,效率自然也就降低了。
例:實現一個服務器,使用select讓服務器可以同時接受多個客戶的鏈接,並將客戶發送的數據打印出來。
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<unistd.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#define SIZE 128
int startup(char *ip,int port)
{
assert(ip);
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(0);
}
int opt=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
exit(1);
}
if(listen(sock,5)<0)
{
perror("listen");
exit(2);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("usae: %s [IP] [PORT]\n",argv[0]);
return 0;
}
int lis_sock=startup(argv[1],atoi(argv[2]));
int gfds[SIZE];
memset(gfds,-1,SIZE*4);
fd_set rfds;
FD_ZERO(&rfds);
while(1)
{
struct timeval timeout={5,0};
gfds[0]=lis_sock;
int max_fd=-1;
int i=0;
for(;i<SIZE;i++)
{
if(max_fd<gfds[i])
{
max_fd=gfds[i];
}
if(gfds[i]>=0)
{
FD_SET(gfds[i],&rfds);
}
}
int ret=select(max_fd+1,&rfds,NULL,NULL,NULL);
switch(ret)
{
case 0:
printf("timeout...\n");
break;
case -1:
printf("error");
break;
default:
if(FD_ISSET(gfds[0],&rfds))
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int connfd=accept(lis_sock,(struct sockaddr*)&peer,&len);
if(connfd<0)
{
perror("accept");
}
else
{
printf("client: %s:%d fd(%d)\n",inet_ntoa(peer.sin_addr),\
ntohs(peer.sin_port),connfd);
int k=0;
for(;k<SIZE;k++)
{
if(gfds[k]==-1)
{
gfds[k]=connfd;
break;
}
}
if(k>=SIZE)
{
close(connfd);
gfds[k]=-1;
}
}
}
int j=1;
for(;j<SIZE;j++)
{
if(FD_ISSET(gfds[j],&rfds))
{
char buf[SIZE];
ssize_t s=read(gfds[j],buf,sizeof(buf));
if(s<0)
{
perror("read");
continue;
}
else if(s==0)
{
printf("client is quit!\n");
close(gfds[j]);
gfds[j]=-1;
}
else
{
buf[s]=0;
printf("client# %s\n",buf);
}
}
}
break;
}
}
return 0;
}
poll也是一種I/O多路轉接的方式,select將三種事件進行了區分,並且用三個位圖來表示不同的監測事件。而poll統一用一種結構來管理要監測的事件。
#include<poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);
參數:
fds:
它是一個結構體數組,其中元素的類型如下:
struct pollfd{
int fd; //保存要監測的文件描述符,由用戶自己設定
int events; //保存要監測的事件,比如讀事件或寫事件,由用戶自己設定
short revents; //保存就緒事件,等到事件就緒,由內核設定
}
pollfd結構裏面包含了要監視的event和已經發生的event。同時pollfd並沒有數量的限制,但是因爲select和poll在返回之後,都需要輪詢來獲取就緒的描述符,所以當監視的文件描述符很多的時候,poll的性能也會下降。fds裏面的每一個元素都代表一個要監測的事件。
nfds:表示數組的長度,也就是要監測事件的個數。
timeout:設置超時時間。
返回值:成功的話返回就緒事件的個數。如果超時的話返回0,失敗的話返回-1。
poll支持的常見事件類型:
POLLIN:數據可讀
POLLOUT:數據可寫
POLLERR:錯誤
POLLRDHUP:TCP連接被對方關閉,或者對方關閉了寫操作,他由GNU引入。在使用的時候要加上#define _GNU_SOURCE。
``
**poll與select的比較:
1、select將讀寫異常的事件分開進行監測,poll將所有的事件類型都統一用一種結構體表示。
2、select的3個fd_set參數都是輸入輸出型參數,當輸入的時候表示要監測的文件描述符,當輸出的時候表示就緒的文件描述符,所以每一次select之前都要重新將要監測的文件描述符設置進fd_set中。而poll用events表示要監測的文件描述符,revents表示就緒的文件描述符,所以poll只需要設置一次就可以了。
3、當事件就緒的時候,select和poll都要遍歷整個所有監測的文件描述符來查找就緒的事件。**