系统提供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都要遍历整个所有监测的文件描述符来查找就绪的事件。**