select()函数和pselect()函数都用于用于IO复用,它们监视多个文件描述符的集合,判断是否有符合条件的事件发生。
select()函数与recv()函数和send()函数不同的是,recv()函数和send()函数可直接操作文件的描述符。但是使用select()函数时,需要先对所要操作的文件描述符进行查询,查看目标文件的描述符是否可以进行读、写、或者错误操作,然后当文件的描述符满足操作的条件时才进行真正的IO操作,即读和写操作。
函数select()的原型为:
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
各参数含义为:
nfds:整型变量,它比所有文件描述符集合中的最大值大1。使用select的时候必须计算最大值的文件描述符的值,将值通过nfds传入。
readfds:这个文件描述符集合监视文件集合中的任何文件是否有数据可读,当select()函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符,即可以被recv()函数、read()函数等进行读数据的操作。
writefds:这个文件描述符集合监视文件集合中的任何文件是否有数据可写,当select()函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符,即可以被send()函数、write()函数等进行写数据的操作。
exceptfds:这个文件描述符集合监视文件集合中的任何文件是否发生错误,其实它可以用于其他的用途,例如,监视带外数据OOB,带外数据使用MSG_OOB标志发送到套接字上。当select()函数返回的时候,readfds将清除其中的其他文件描述符,只留下可读OOB数据。
timeout:用来描述等待描述符就绪需要的事件。设置在select()函数所坚实的文件集合中的事件没有发生时,最长的等待时间,当超过此时间时,函数会返回。当超时时间为NULL时,表示阻塞操作,会一直等待,直到某个监视的文件集合中的某个文件描述符符合返回条件。当timeout的值为0时,select()会立即返回。timeout告知系统内核等待指定描述符中的任何一个就绪可花费多少时间。其timeval结构体用于指定这段时间的秒数和微秒数。
struct timeval
{
time_t tv_sec; //秒
long tv_usec; //微秒
};
这个结构体参数有三种可能的结果:
(1)永远等待下去:仅在有一个描述符准备好I/O时才返回,为此,可把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过由该参数所指定的秒数或微秒数。
(3)不等待:检查描述符后立即返回,这种方法称为轮询,为此,该参数必须指向一个timeval结构,而且其定时器值须为0。
select()函数有3个可能的返回值:成功执行时返回就绪描述符的数目;经过了timeout时长后仍无设备准备好,即超时,返回0;如果出错,返回-1并设置相应的errno,如果select()执行过程中被某个信号中断,返回-1并设置errno为EINTR。
errorno的取值及含义:
EBADF:文件描述符无效或该文件已被关闭
EINVAL:传递了不合法参数
EINTR:接收到中断信号
ENOMEM:没有足够内存
readset,writeset,exceptset都是值-结果参数,即传入指针进去,函数根据指针可以修改对应fd_set。
通常,操作系统通过宏FD_SETSIZE来声明一个进程中select所能操作的文件描述符的最大数目。在/usr/include/linux/posix_types.h中关于FD_SETSIZE是这样定义的:
#undef _FD_SETSIZE
#define _FD_SETSIZE 1024
除此之外,还有4个宏可以用来操作文件描述符的集合:
FD_ZERO(fd_set* fdset):将指定的文件描述符集合清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于系统在分配内存空间后通常不作清空处理,所以结果是不可知的。
FD_SET(fd_set* fdset):用于在文件描述符集合中增加一个新的文件描述符
FD_CLR(fd_set* fdset):用于在文件描述符集合中删除一个文件描述符
FD_ISSET(int fd,fd_set* fdset):用于检测指定的文件描述符是否在该文件描述符集合中
在内核中,socket对应struct socket结构,但在返回给用户空间之前,内核做了一个关联,调用get_unused_fd_flags从当前进程中获取一个可用的文件描述符fd,将struct socket结构关联到该fd,并返回fd给用户空间。所以在用户空间中,socket为文件描述符。另外,进程可以打开的文件数是有限的,为1024,所以socket的取值要小于1024。
select()函数使用流程:
select()函数监视的文件描述符可分为3类,分别是readfds,writefds,exceptfds。调用后,select()函数会阻塞,直到有描述符就绪(有数据、可读、可写或者有错误)时,或者超时时才返回,当select()函数返回后,可以通过便利fdset来找到就绪的描述符。需要注意的是,当声明了一个文件描述符集合后,必须用FD_ZERO()函数来将所有位置为0。
fd_set rset; //声明描述符集合
int fd; //定义文件描述符
FD_ZERO(&rset); //将描述符集合置为0
FD_SET(fd,&rset);
FD_SET(stdin,&rset);
然后调用select()函数,拥塞等待文件描述符事件的到来,如果超过设定的时间,则不再等待,继续往下执行。
select(fd + 1,&rset,NULL,NULL,NULL);
select()返回后,用FD_ISSET()函数检测所指定的描述符是否置位。
if(FD_ISSET(fd,&rset))
{
...
}
select()函数的第一个参数是nfds,是所有加入集合的句柄值的最大的那个值还要加1。比如我们的描述符集合位为1、4、5,那么nfds就为6。当调用select()函数时,描述符从0开始,在描述符集合里匹配我们指定的描述符,当函数返回时,表示那些描述符已经准备好了。那么,怎么判断这个描述符是否准备好呢?我们把这些条件称为socket就绪条件,一下几种情况说明了socket就绪的条件:
以下四个条件当中的任何一个满足时,套接口准备好读,称为读就绪:
(1)套接口内核接收缓冲区的数据字节数大于等于套接口接收缓冲区低潮限度的当前值,可以通过SO_REVILOAT来设置此低潮限度,此时可以无阻塞地读该socket,并且读操作返回地字节数大于0。
(2)连接的读这一半关闭,也就是接收了FIN的TCP连接。
(3)套接口是一个监听套接口且已完成的连接数为非0。
(4)有一个套接口错误待处理。
以下四个条件当中的任何一个满足时,套接口准备好写,称为写就绪:
(1)套接口接收缓冲区的空间字节数大于等于套接口接收缓冲区低潮限度的当前值,且套接口已连接或者套接口不要求连接,可以通过SO_REVILOAT来设置此低潮限度。
(2)连接的写这一半关闭,对这样的套接口写操作将产生信号SIGPIEP
(3)socket使用非阻塞connect连接成功或者失败(超时)之后。
(4)有一个套接口错误待处理。
select()能处理的异常情况只有一种:socket上接收到带外数据。
select()函数的优缺点:
优点:select()目前几乎在所有的平台上支持,其良好的跨平台支持也是它的一个优点。
缺点:select最大的缺陷就是单个进程所打开的fd的数量是有一定限制的,它由FD_SETSIZE设置,默认值是1024。一般来说,这个数目和系统的内存关系很大,32位机默认是1024,64位机默认是2048;对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个socket来完成调度,不管哪个socket是活跃的,都得遍历一遍,这会在无形中浪费很多CPU时间,如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询;需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
select()函数的使用:
1.用select()函数实现定时器
timer.c
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#include <errno.h>
//秒级定时器
void second_timer(unsigned seconds)
{
struct timeval tv;
tv.tv_sec = seconds;
tv.tv_usec = 0;
int err;
do{
err = select(0,NULL,NULL,NULL,&tv);
}while(err < 0 && errno == EINTR);
}
//毫秒级定时器
void msecond_timer(unsigned long mseconds)
{
struct timeval tv;
tv.tv_sec = mseconds / 1000;
tv.tv_usec = (mseconds % 1000) * 1000;
int err;
do{
err = select(0,NULL,NULL,NULL,&tv);
}while(err < 0 && errno == EINTR);
}
//微秒级定时器
void usecond_timer(unsigned long useconds)
{
struct timeval tv;
tv.tv_sec = useconds / 1000000;
tv.tv_usec = useconds % 1000000;
int err;
do{
err = select(0,NULL,NULL,NULL,&tv);
}while(err < 0 && errno == EINTR);
}
int main()
{
int i;
for(i = 0;i < 10;i++)
{
printf("%d\n",i);
second_timer(1);
//msecond_timer(1500);
//usecond_timer(1900000);
}
return 0;
}
2.检查标准输入
checkstdin.c
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
//描述符的集合
fd_set rset;
//清空描述符集合
FD_ZERO(&rset);
//将标准输入加入到描述符集合中
FD_SET(0,&rset);
while(1)
{
fflush(stdout);
//检测描述符,等待事件发生
int nselect = select(1,&rset,NULL,NULL,NULL);
if(nselect < 0)
{
perror("select:");
continue;
}
if(FD_ISSET(0,&rset))
{
char buffer[1024] = {0};
read(0,buffer,sizeof(buffer) - 1);
printf("input:%s",buffer);
}
else
{
printf("error!Invaild fd!\n");
continue;
}
//清空描述符集合
FD_ZERO(&rset);
//将0加入到描述符集合中
FD_SET(0,&rset);
}
return 0;
}
3.用select()函数实现回射服务器。客户端从标准输入读入一行,发送到服务器,服务器从网络读取一行,然后输出到客户端,客户端收到服务器的响应,输出这一行到标准输出,通俗来说就是客户端把输入的东西再输出。
Select_Server.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void handle(int clientfds[],int max_fd,fd_set* rset,fd_set* allset)
{
int nread;
int i;
char buffer[1024];
for(i = 0;i < max_fd;i++)
{
if(clientfds[i] != -1)
{
if(FD_ISSET(clientfds[i],rset))
{
//读取客户端socket流
nread = read(clientfds[i],buffer,1024);
if(nread < 0)
{
perror("read:");
close(clientfds[i]);
FD_CLR(clientfds[i],allset);
clientfds[i] = -1;
continue;
}
if(nread == 0)
{
printf("客户端关闭了连接!\n");
close(clientfds[i]);
FD_CLR(clientfds[i],allset);
clientfds[i] = -1;
continue;
}
write(clientfds[i],buffer,nread);
}
}
}
}
int main()
{
//设置服务器的端口号为6888
short s_port = 6888;
//设置默认监听队列的长度为1024
int backlog = 1024;
//地址结构
struct sockaddr_in clientaddr; //客户端地址
struct sockaddr_in serveraddr; //服务器地址
//存放客户端通信描述符的数组
int clientfds[FD_SETSIZE];
//所监听的描述符
int listen_fd;
//描述符的集合
fd_set allset,rset;
//用来记录select()函数的返回值
int nselect;
//用来记录最大的描述符
int max_fd;
//用来记录客户端的socket描述符
int client_fd;
//缓冲区长度
char buffer[1024];
//地址结构的长度
int socketlength = sizeof(struct sockaddr_in);
//创建TCPsocket
listen_fd = socket(PF_INET,SOCK_STREAM,0);
//如果创建失败
if(listen_fd < 0)
{
perror("socket:");
return -1;
}
int opt = 1;
//将监听的端口设置为可以复用的
if(setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)) < 0)
{
perror("setsockopt:");
}
//socket地址结构
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(s_port);
//绑定
int nbind = bind(listen_fd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr_in));
if(nbind == -1)
{
perror("bind:");
return -1;
}
//监听
int nlisten = listen(listen_fd,backlog);
if(nlisten < 0)
{
perror("listen:");
return -1;
}
//初始化存放客户端通信描述符的数组
int i = 0;
for(;i < FD_SETSIZE;i++)
{
clientfds[i] = -1;
}
//清理描述符集合
FD_ZERO(&allset);
//将监听的socket描述符加入集合
FD_SET(listen_fd,&allset);
max_fd = listen_fd;
printf("服务器正在监听端口%d......\n",s_port);
while(1)
{
rset = allset;
//等待select事件发生
nselect = select(max_fd+1,&rset,NULL,NULL,NULL);
if(nselect < 0)
{
perror("select:");
return -1;
}
//处理客户端的连接
if(FD_ISSET(listen_fd,&rset)) //检测监听的描述符是否存在于描述符集合中
{
client_fd = accept(listen_fd,(struct sockaddr*)&clientaddr,&socketlength);
if(client_fd < 0)
{
perror("accept:");
continue;
}
sprintf(buffer,"接受来自%s:%d\n",inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
printf(buffer);
//将客户端的socket描述符加入数组中
for(i = 0;i < FD_SETSIZE;i++)
{
if(clientfds[i] == -1)
{
clientfds[i] = client_fd;
break;
}
}
//如果达到了最大连接数
if(i == FD_SETSIZE)
{
printf("已达到最大连接数!\n");
close(client_fd);
}
if(client_fd > max_fd)
{
max_fd = client_fd;
}
//将socket加入集合中
FD_SET(client_fd,&allset);
if(--nselect <= 0)
{
continue;
}
}
//处理客户端收据的收发
handle(clientfds,max_fd,&rset,&allset);
}
return 0;
}
Select_Client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define max(a,b) ((a) > (b) ? (a) : (b))
void handle(int client_fd)
{
FILE* fp = stdin;
//发送队列和接收队列,设置为1024
char sendqueue[1024],receivequeue[1024];
fd_set rset;
FD_ZERO(&rset);
int max_fd = max(fileno(fp),client_fd) + 1;
for(;;)
{
FD_SET(fileno(fp),&rset);
FD_SET(client_fd,&rset);
int nselect = select(max_fd,&rset,NULL,NULL,NULL);
if(nselect == -1)
{
perror("select:");
continue;
}
if(FD_ISSET(client_fd,&rset))
{
//接收到服务器的响应
int nread = read(client_fd,receivequeue,1024);
if(nread == 0)
{
printf("服务器关闭了连接!\n");
break;
}
else if(nread == -1)
{
perror("read:");
break;
}
else
{
write(STDOUT_FILENO,receivequeue,nread);
}
}
if(FD_ISSET(fileno(fp),&rset))
{
//标准输入可读
if(fgets(sendqueue,1024,fp) == NULL)
{
break;
}
else
{
write(client_fd,sendqueue,strlen(sendqueue));
}
}
}
}
int main()
{
char* s_iaddr = "127.0.0.1";
int s_port = 6888;
int client_fd;
char buffer[1024];
struct sockaddr_in serveraddr;
client_fd = socket(AF_INET,SOCK_STREAM,0);
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(s_port);
inet_pton(AF_INET,s_iaddr,&serveraddr.sin_addr);
//建立连接
if(connect(client_fd,(struct sockaddr*)&serveraddr,sizeof(serveraddr)) < 0)
{
perror("connect:");
return -1;
}
printf("---回射服务器的客户端---\n");
handle(client_fd);
close(client_fd);
printf("exit\n");
exit(0);
}
运行结果: