深入理解select模型
阻塞和非阻塞两种I/O模式:
阻塞模式: 执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字传统(connect、accept、recv或recvfrom这样的阻塞程序)默认为阻塞模式。可以通过多进程或者多线程技术进行优化处理。
阻塞模式下,进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞(死等在被阻塞的地方),函数不会立即返回
非阻塞模式:执行I/O操作时,Winsock函数会返回并交出控制权。这种模式使用起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回错误(当非阻塞socket的TCP连接正在进行时,Linux的错误号EINPROGRESS,Windows的错误号为WSAEWOULDBLOCK)。但功能强大。它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
非阻塞non-block模式下,进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以非阻塞模式效率较高
为了解决这个问题,提出了进行I/O操作的一些I/O模型。
linux下最常见的三种就是select模式,poll模式,epoll模型。
select描述:
select系统调用是用来让我们的程序监视多个文件句柄(file descrīptor)的状态变化的。通过select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在select这里等待,直到被监视的文件描述符有某一个或多个发生了状态改变。
select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪些Socket或文件可读可写。
![这里写图片描述](https://img-blog.csdn.net/20170803103627820?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvV1JOR1Q=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
参数详解:
ndfs:select监视的文件句柄数,视进程中打开的文件数而定,一般设为要监视各文件中的最大文件描述符值加1。
readfds:这个文件描述符集合监视文件集中的任何文件是否有数据可读,当select函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符。
writefds:这个文件描述符集合监视文件集中的任何文件是否有数据可写,当select函数返回的时候,writefds将清除其中不可写的文件描述符,只留下可写的文件描述符。
exceptfds:这个文件集将监视文件集中的任何文件是否发生错误,其实,它可用于其他的用途,例如,监视带外数据OOB,带外数据使用MSG_OOB标志发送到套接字上。当select函数返回的时候,exceptfds将清除其中的其他文件描述符,只留下标记有OOB数据的文件描述符。
timeout:本次select()的超时结束时间。这个参数至关重要,它可以使select处于三种状态:
若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
FD_ZERO(fd_set *set) 用来清除描述词组set的全部位
FD_SET(int fd,fd_set*set) 用来设置描述词组set中相关fd的位
FD_ISSET(int fd,fd_set *set) 用来测试描述词组set中相关fd 的位是否为真
FD_CLR(inr fd,fd_set* set) 用来清除描述词组set中相关fd 的位
select函数的返回值:
正值:表示监视的文件集中有文件描述符符合要求
零值:表示select超时
负值:表示发生了错误
注意:select()调用会清空传递给它的集合参数中的内容,也就是会清空readfds、writefd、exceptfds这三个指针参数所指定的描述符集合。因此,在每次调用select()之前,必须重新初始化并把需要监视的描述符填写到相应的描述符集合中。select()调用也会清空timeout指针所指向的struct timeval结构,所以在每次调用select()之前也要重新填充timeout指针所指向的struct timeval结构。
使用select
select()机制中提供一fd_set的数据结构,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一socket或文件发生了可读或可写事件。
用法:
fd_set set;
FD_ZERO(&set); //将set清零使集合中不含任何fd
FD_SET(fd, &set); //将fd加入set集合
FD_CLR(fd, &set); //将fd从set集合中清除/
FD_ISSET(fd, &set);//在调用select()函数后,用FD_ISSET来检测fd是否在set集合中,当检测到fd在set中则返回真,否则,返回假(0)
理解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被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)。
另外,如果select调用中设置了等待时间,那么每次调用时都需要重新对这个时间赋值么?就像对fd_set处理一样。
select缺点:
(1)每次调⽤用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 ’
(3)select⽀支持的⽂文件描述符数量太小了,默认是1024
设计一个简单的select服务器:
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int fds[sizeof(fd_set)*8];
static void Usage(char*proc)
{
printf("%s [local_ip] [local port]\n");
}
int startUp(char*ip,int port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
exit(2);
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(port));
server.sin_addr.s_addr=inet_addr(ip);
if(bind(sock,(struct sockaddr*)&server,sizeof(struct sockaddr_in))<0)
{
perror("bind");
exit(3);
}
if(listen(sock,10)<0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argv,char*argc[])
{
if(argv!=3)
{
Usage(argc[0]);
return 1;
}
int listen_sock=startUp(argc[1],argc[2]);
int nums=sizeof(fd_set)*8;
fd_set rds;
int i=0;
for(i=0;i<nums;i++)
{
fds[i]=-1;
}
while(1)
{
int max=-1;
struct timeval timeout={5,0};
fds[0]=listen_sock;
for(i=0;i<nums;i++)
{
if(fds[i]>-1)
{
FD_SET(fds[i],&rds);
}
if(max<fds[i])
{
max=fds[i];
}
}
switch(select(max+1,&rds,NULL,NULL,&timeout))
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("select");
break;
default:
for(i=0;i<nums;i++)
{
if(i==0 && FD_ISSET(fds[i],&rds))
{
struct sockaddr_in client;
socklen_t len=sizeof(client);
int newsock=accept(listen_sock,(struct socketaddr*)&client,&len);
if(newsock<0)
{
perror("accept");
return 2;
}
else
{
int j=0;
for(j=0;j<nums;j++)
{
if(fds[j]==-1)
{
break;
}
}
if(j==nums)
{
close(newsock);
}
else
{
fds[j]=newsock;
}
}
}
else if(i!=0&&FD_ISSET(fds[i],&rds))
{
char buf[1024];
ssize_t s=read(fds[i],buf,sizeof(buf)-1);
if(s>0)
{
printf("client say:%s\n",buf);
}
else if(s==0)
{
printf("client quit!\n");
close(fds[i]);
fds[i]=-1;
}
else
{
perror("read");
close(fds[i]);
fds[i]=-1;
}
}
}
}
}
return 0;
}