Select
I/O复用,可以调用系统调用select和poll!在这两个系统调用中的某一个阻塞,而不是真正的阻塞I/O系统调用!
select() 函数的重点在于它可以同时监控多个描述符(一般最大为1024),并且在描述符集中没有可操作的描述符时会进入睡眠状态。 实际应用中,若需要同时处理多个描述符的读写时,如果只是创建了一系列的read()和write()就会导致在有些描述符没有准备好读写时而被阻塞,这样当然不是我们期望的,因此这时就需要应用select()。
下面主要介绍I/O复用中的select函数!select函数可以指示内核等待多个事件中的任一个发生,仅在一个或多个事件发生,或者等待一个足够的时间后才唤醒进程!
select函数的原型
select函数的原型如下:
#include <sys/types.h>
#include<sys/time.h>
int select ( int maxfdp1,
fd_set *readset ,
fd_set * writeset ,
fd_set *excpetset ,
const struct timeval *timeout);
参数
readfds, writefds, exceptfds为所要监听的三个描述符集:
——readfds 监听文件描述符是否可读,不监听可以传入 NULL
——writefds 监听文件描述符是否可写 ,不监听可以传入 NULL
——exceptfds 监听文件描述符是否有异常,不监听可以传入 NULL
maxfdp1 是 select() 监听的三个描述符集中描述符的最大值+1
timeout 设置超时时间 ,它表示等待内核中的一组描述符任一个准备好需要花费多久的时间!其中timeval指定了秒数和微秒数。
struct timeval {
long tv_sec;//秒数
long tv_usec;//微秒数
};
select的超时时间,这个参数至关重要,它可以使select处于三种状态:
第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
返回值
select有三个可能的返回值:
1.正常情况下返回就绪的文件描述符个数;
2.经过了timeout时长后仍无设备准备好,返回值为0;
3.如果select被某个信号中断,它将返回-1并设置errno为EINTR。
4.如果出错,返回-1并设置相应的errno:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足
描述字集
中间的三个参数readset writeset和excpetset指定我们要让内核测试读、写、异常条件所需的描述字!函数select使用描述字集,它一般是一个整型的数组,每个数中的每一位代表一个描述符。
系统提供了4个宏对描述符集进行操作:
#include <sys/select.h>
#include <sys/time.h>
//设置文件描述符集fdset中对应于文件描述符fd的位
void FD_SET(int fd, fd_set *fdset);
//清除文件描述符集fdset中对应于文件描述符fd的位(设置为0)
void FD_CLR(int fd, fd_set *fdset);
//清除文件描述符集fdset中的所有位(既把所有位都设置为0)
void FD_ZERO(fd_set *fdset);
//在调用select后使用FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。
void FD_ISSET(int fd, fd_set *fdset);
例如下面一段代码:
fd_set readset;
FD_ZERO(&readset);
FD_SET(5, &readset);
FD_SET(33, &readset);
则文件描述符集readset中对应于文件描述符6和33的相应位被置为1
再执行如下程序后:
FD_CLR(5, &readset);
则文件描述符集readset对应于文件描述符6的相应位被置为0
通常,操作系统通过宏FD_SETSIZE来声明在一个进程中select所能操作的文件描述符的最大数目。
一般情况下被定义为1024.一个整数占4个 字节,既32位,那么就是用包含32个元素的整数数组来表示文件描述符集。
我们可以在头文件中修改这个值来改变select使用的文件描述符集的大小,但 是需要注意的是:必须重新编译内核才能使修改后的值有效。
如果我们对其中的某一个描述符集不感兴趣的话,可以设置为空指针。如果我们把三个描述符集都设为空指针,就实现了一个比sleep更准确的定时器。
注意select函数的第一个参数maxfdp1,是所有加入集合的句柄值的最大那个值还要加1。比如我们的描述符为1 4 5,那么maxfdp1就为6,描述符从0开始。
当我们调用函数时,指定我们关心的描述符集,当返回时,指示那些描述符已经准备好了。
注意事项 :
1. maxfdp1必须被正确设置,一般取描述符集中描述符的最大值并加1。
2. 在非必须的情况下,尽量使用不超时的select(),即将utimeout参数设置为NULL。
/*参数 timeout 置为 NULL*/
select(maxfdp1, &readfds, &writefds, &exceptfds, NULL);
3. timeout的值必须在每次select()之前重新赋值,因为操作系统会修改此值
while(1)
{
timeout.tv_sec = 1;
timeout.tv_usec = 0;
select(maxfdp1, &readfds, &writefds, &exceptfds, &timeout);
}
4. 由于select()会修改字符集,因此如果select()调用是在一个循环中,则描述符集必须被重新赋值。
/*以read操作为例*/
while(1)
{
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
select(maxfdp1, &readfds, NULL, NULL, NULL);
}
5. 函数read(),write(),recv(),send()以及select()可能会返回-1并且errno置位为EINTR,或这errno被赋值为EAGAIN(EWOULDBLOCK),这种情况需要被正确处理。如果程序中不接收任何信号,则不会得到EINTR。如果程序设为阻塞I/O,则不会收到EAGAIN。
/*一般只需对EINTR进行处理就可以了,例子如下*/
while(1)
{
ret = select(maxfdp1, &readfds, NULL, NULL, NULL);
if(ret == -1 && errno == EINTR)
continue;
}
6. 当read(),write(),recv()和send()返回0时建议关闭描述符并在字符集中移除此描述符(不关闭描述符并移除的话可能会导致未知错误,还是对此情况处理的好)。
定时器 :
在没有usleep函数的系统中,可以应用select来实现,下例中实现了0.2秒的延时:
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 200000; /* 0.2 秒*/
select(0, NULL, NULL, NULL, &tv);
-------------------------------------------------------------------------------------------------------------------------
例子1:
-------------------------------------------------------------------------------------------------------------------------
main()
{
int sock;
FILE *fp;
struct fd_set fds;
struct timeval timeout={3,0}; //select等待3秒,3秒轮询,要非阻塞就置0
char buffer[256]={0}; //256字节的接收缓冲区
/* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开
sock=socket(...);
bind(...);
fp=fopen(...); */
while(1)
{
FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
FD_SET(sock,&fds); //添加描述符
FD_SET(fp,&fds); //同上
maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1
switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用
{
//select错误,退出程序
case -1:
exit(-1);
break;
//再次轮询
case 0:
break;
default:
//测试sock是否可读,即是否网络上有数据
if(FD_ISSET(sock,&fds))
{
recvfrom(sock,buffer,256,.....);//接受网络数据
//测试文件是否可写
if(FD_ISSET(fp,&fds))
{
fwrite(fp,buffer...);//写入文件
}
//buffer清空;
}
}// end if break;
}// end switch
}//end while
}//end main
-------------------------------------------------------------------------------------------------------------------------
例子2: Linux下用select查询串口数据
-------------------------------------------------------------------------------------------------------------------------
Linux下直接用read读串口可能会造成堵塞,或数据读出错误。然而用select先查询com口,再用read去读就可以避免,并且当com口延时时,程序可以退出,这样就不至于由于com口堵塞,程序就死了。我的代码如下:
bool ReadDevice( int hComm, unsigned long uLen, char* pData )
{
int nread = 0;
char inbuf[uLen];
char buff[uLen];
memset( inbuff, '\0', uLen );
memset( buff, '\0', uLen );
fd_set readset;
struct timeval tv;
int MaxFd = 0;
int c = 0;
int z;
do
{
FD_ZERO( &readset );
if( hComm >= 0 )
FD_SET( hComm, &readset );
MaxFd = hComm + 1;
tv.tv_sec = 0;
tv.tv_usec = 500000;
do
{
z = select( MaxFd, &readset, 0, 0, &tv);
} while ( z==-1 && errno==EINTR );
if ( z == -1 )
printf("select(2)\n");
if( z == 0 )
{
hComm = -1;
}
if ( hComm>=0 && FD_ISSET(hComm, &readset) )
{
z = read( hComm, buff, uLen - c );
c += z;
if( z == -1 )
{
hComm = -1;
}
if ( z > 0 )
{
buff[ z + 1 ] = '\0';
strcat( inbuff, buff );
memset( buff, 0x00, uLen );
}
else
{
hComm = -1;
}
}
} while ( hComm >= 0 );
memcpy( pData, inbuff, c );
return true;
}
-------------------------------------------------------------------------------------------------------------------------
例子: 监视标准输入的变化,即文件描述符为0的变化,
-------------------------------------------------------------------------------------------------------------------------
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, char* argv[])
{
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
FD_ZERO(&reads);
FD_SET(0, &reads);//监视文件描述符0的变化, 即标准输入的变化
/*
超时不能在此设置!
因为调用select后,结构体timeval的成员tv_sec和tv_usec的值将被替换为超时前剩余时间.
调用select函数前,每次都需要初始化timeval结构体变量.
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
*/
while(1)
{
/*
将准备好的fd_set变量reads的内容复制到temps变量,因为调用select函数后,除了发生变化的fd对应位外,剩下的所有位
都将初始化为0,为了记住初始值,必须经过这种复制过程。
*/
temps = reads;
//设置超时
timeout.tv_sec = 5;
timeout.tv_usec = 0;
//调用select函数. 若有控制台输入数据,则返回大于0的整数,如果没有输入数据而引发超时,返回0.
result = select(1, &temps, 0, 0, &timeout);
if(result == -1)
{
perror("select() error");
break;
}
else if(result == 0)
{
puts("timeout");
}
else
{
//读取数据并输出
if(FD_ISSET(0, &temps))
{
str_len = read(0, buf, BUF_SIZE);
buf[str_len] = 0;
printf("message from console: %s", buf);
}
}
}
return 0;
}
程序运行结果:
/*
nihao
message from console: nihao
goodbye
message from console: goodbye
timeout
timeout
*/
-------------------------------------------------------------------------------------------------------------------------
例子:select函数实现I/O复用服务端
-------------------------------------------------------------------------------------------------------------------------
/*******************************************************
***服务器端***
********************************************************/
//server.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/select.h>
#define BUF_SIZE 100
void error_handing(char* buf);
int main(int argc, char* argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
if(argc != 2)
{
printf("Usage: %s <port> \n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handing("bind() error");
if(listen(serv_sock, 5) == -1)
error_handing("listen() error");
FD_ZERO(&reads);
FD_SET(serv_sock, &reads); //将服务端套接字注册入fd_set,即添加了服务器端套接字为监视对象
fd_max = serv_sock;
while(1)
{
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
//无限循环调用select 监视可读事件
if((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout)) == -1)
{
perror("select error");break;
}
if(fd_num == 0)
continue;
for(i = 0; i < fd_max + 1; i++)
{
if(FD_ISSET(i, &cpy_reads))
{
/*发生状态变化时,首先验证服务器端套接字中是否有变化.
①若是服务端套接字变化,接受连接请求。
②若是新客户端连接,注册与客户端连接的套接字文件描述符.
*/
if(i == serv_sock)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)& clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else
{
str_len = read(i, buf, BUF_SIZE);
if(str_len == 0) //读取数据完毕关闭套接字
{
FD_CLR(i, &reads);//从reads中删除相关信息
close(i);
printf("closed client: %d \n", i);
}
else
{
write(i, buf, str_len);//执行回声服务 即echo
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handing(char* buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
/*******************************************************
***客户端***
********************************************************/
//client.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#define BUF_SIZE 1024
void error_handling(char* message);
int main(int argc, char* argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc != 3)
{
printf("Usage: %s <IP> <port> \n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("connected....");
while(1)
{
fputs("Input message:(输入Q退出):", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
/*******************************************************
***编译执行测试***
********************************************************/
编译程序:
gcc server.c –o server
gcc client.c –o client
启动服务端:
yu@ubuntu:~/qtProjects/echo_selectserv$ ./server 8899
启动客户端1:
yu@ubuntu:~/qtProjects/echo_selectserv$ ./client 127.0.0.1 8899
connected....
Input message:(输入Q退出):你好哇2017年2月12日20:25:43
Message from server: 你好哇2017年2月12日20:25:43
Input message:(输入Q退出):你好哇2017-02-12 20:25:52
Message from server: 你好哇2017-02-12 20:25:52
Input message:(输入Q退出):q
启动客户端2:
yu@ubuntu:~/qtProjects/echo_selectserv$ ./client 127.0.0.1 8899
connected....
Input message:(输入Q退出):你好2017年2月12日20:25:11
Message from server: 你好2017年2月12日20:25:11
Input message:(输入Q退出):你好2017年2月12日20:25:24
Message from server: 你好2017年2月12日20:25:24
Input message:(输入Q退出):q
服务端情况:
yu@ubuntu:~/qtProjects/echo_selectserv$ ./server 8899
connected client: 4
connected client: 5
closed client: 5
closed client: 4
-------------------------------------------------------------------------------------------------------------------------
例子:
-------------------------------------------------------------------------------------------------------------------------
/*
使用select函数可以以非阻塞的方式和多个socket通信。程序只是演示select函数的使用,即使某个连接关闭以后也不会修改当前连接数,连接数达到最大值后会终止程序。
1. 程序使用了一个数组fd,通信开始后把需要通信的多个socket描述符都放入此数组
2. 首先生成一个叫sock_fd的socket描述符,用于监听端口。
3. 将sock_fd和数组fd中不为0的描述符放入select将检查的集合fdsr。
4. 处理fdsr中可以接收数据的连接。如果是sock_fd,表明有新连接加入,将新加入连接的socket描述符放置到fd。
*/
// select_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MYPORT 1234 //连接时使用的端口
#define MAXCLINE 5 //连接队列中的个数
#define BUF_SIZE 200
int fd[MAXCLINE]; //连接的fd
int conn_amount; //当前的连接数
void showclient()
{
int i;
printf("client amount:%d\n",conn_amount);
for(i=0;i<MAXCLINE;i++)
{
printf("[%d]:%d ",i,fd[i]);
}
printf("\n\n");
}
int main(void)
{
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;
//建立sock_fd套接字
if((sock_fd = socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("setsockopt");
exit(1);
}
//设置套接口的选项 SO_REUSEADDR 允许在同一个端口启动服务器的多个实例
// setsockopt的第二个参数SOL SOCKET 指定系统中,解释选项的级别 普通套接字
if(setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int))==-1)
{
perror("setsockopt error \n");
exit(1);
}
server_addr.sin_family = AF_INET; //主机字节序
server_addr.sin_port = htons(MYPORT);
server_addr.sin_addr.s_addr = INADDR_ANY;//通配IP
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 error!\n");
exit(1);
}
if(listen(sock_fd,MAXCLINE)==-1)
{
perror("listen error!\n");
exit(1);
}
printf("listen port %d\n",MYPORT);
fd_set fdsr; //文件描述符集的定义
int maxsock;
struct timeval tv;
conn_amount =0;
sin_size = sizeof(client_addr);
maxsock = sock_fd;
while(1)
{
//初始化文件描述符集合
FD_ZERO(&fdsr); //清除描述符集
FD_SET(sock_fd,&fdsr); //把sock_fd加入描述符集
//超时的设定
tv.tv_sec = 30;
tv.tv_usec =0;
//添加活动的连接
for(i=0;i<MAXCLINE;i++)
{
if(fd[i]!=0)
{
FD_SET(fd[i],&fdsr);
}
}
//如果文件描述符中有连接请求 会做相应的处理,实现I/O的复用 多用户的连接通讯
ret = select(maxsock +1,&fdsr,NULL,NULL,&tv);
if(ret <0) //没有找到有效的连接 失败
{
perror("select error!\n");
break;
}
else if(ret ==0)// 指定的时间到,
{
printf("timeout \n");
continue;
}
//循环判断有效的连接是否有数据到达
for(i=0;i<conn_amount;i++)
{
if(FD_ISSET(fd[i],&fdsr))
{
ret = recv(fd[i],buf,sizeof(buf),0);
if(ret <=0) //客户端连接关闭,清除文件描述符集中的相应的位
{
printf("client[%d] close\n",i);
close(fd[i]);
FD_CLR(fd[i],&fdsr);
fd[i]=0;
conn_amount--;
}
//否则有相应的数据发送过来 ,进行相应的处理
else
{
if(ret <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 error\n");
continue;
}
//添加新的fd 到数组中 判断有效的连接数是否小于最大的连接数,如果小于的话,就把新的连接套接字加入集合
if(conn_amount <MAXCLINE)
{
for(i=0;i< MAXCLINE;i++)
{
if(fd[i]==0)
{
fd[i] = new_fd;
break;
}
}
conn_amount++;
printf("new connection client[%d]%s:%d\n",conn_amount,inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
if(new_fd > maxsock)
{
maxsock = new_fd;
}
}
else
{
printf("max connections arrive ,exit\n");
send(new_fd,"bye",4,0);
close(new_fd);
continue;
}
}
showclient();
}
for(i=0;i<MAXCLINE;i++)
{
if(fd[i]!=0)
{
close(fd[i]);
}
}
exit(0);
}
//客户端的一个简单的实现,只是为了证实一下,服务器端程序的正确性
//select_client.c
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#define MAXDATASIZE 100
#define SERVPORT 1234
#define MAXLINE 1024
int main(int argc,char *argv[])
{
int sockfd,sendbytes;
// char send[MAXLINE];
char send[MAXLINE];
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if(argc <2)
{
fprintf(stderr,"Please enter the server's hostname\n");
exit(1);
}
if((host = gethostbyname(argv[1])) == NULL)
{
perror("gethostbyname");
exit(1);
}
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket error \n");
exit(1);
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) ==-1)
{
perror("connect \n");
exit(1);
}
while(fgets(send,1024,stdin)!=NULL)
{
if((sendbytes = write(sockfd,send,100)) ==-1)
{
perror("send error \n");
exit(1);
}
}
close(sockfd);
}