版权声明:转载请附带原博主的网址 https://blog.csdn.net/qq_43260665/article/details/88850038
前面提到过,使用多进程和多线程来提高系统运行的效率,多线程的并发相对多进程来说占用的资源更少,效率相比多进程更快, select()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时 间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理。
#include <sys/select.h>
#include <sys/time.h>
struct timeval {
long tv_sec; //seconds long tv_usec; //microseconds
};
FD_ZERO(fd_set* fds) //清空集合
FD_SET(int fd, fd_set* fds) //将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds) //判断指定描述符是否在集合中
FD_CLR(int fd, fd_set* fds) //将给定的描述符从文件中删除
int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval
*timeout);
说明: select监视并等待多个文件描述符的属性发生变化,它监视的属性分3类,分别是readfds(文件描述符有数据
到来可读)、 writefds(文件描述符可写)、和exceptfds(文件描述符异常)。调用后select函数会阻塞,直到有描述
符就绪(有数据可读、可写、 或者有错误异常),或者超时( timeout 指定等待时间)发生函数才返回。当
select()函数返回后,可以通过遍历 fdset,来找到 究竟是哪些文件描述符就绪。
1. select函数的返回值是就绪描述符的数目,超时时返回0,出错返回-1;
2. 第一个参数max_fd指待测试的fd的总个数,它的值是待测试的最大文件描述符加1。Linux内核从0开始到max_fd-
1扫描文件描述 符,如果有数据出现事件(读、写、异常)将会返回;假设需要监测的文件描述符是8,9,10,那么
Linux内核实际也要监测0~7,此时真 正带测试的文件描述符是0~10总共11个,即max(8,9,10)+1,所以第一个
参数是所有要监听的文件描述符中最大的+1。
3. 中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试的
可以设置为 NULL;
5. 最后一个参数是设置select的超时时间,如果设置为NULL则永不超时;
需要注意的是待测试的描述集总是从0, 1, 2, ...开始的。 所以, 假如你要检测的描述符为8, 9, 10,
那么系统实际也要 监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是
max(8, 9, 10) + 1
在Linux内核有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数中,这也意味着select所用到的FD_SET是有
限的,也正是这个 原因select()默认只能同时处理1024个客户端的连接请求:
/linux/posix_types.h:
#define __FD_SETSIZE 1024
基于select的I/O复用模型的是单进程执行可以为多个客户端服务,这样可以减少创建线程或进程所需要的CPU时间片或内存 资源的开销;此外几乎所有的平台上都支持select(),其良好跨平台支持是它的另一个优点。当然它也有两个主要的缺点:
1. 每次调用 select()都需要把fd集合从用户态拷贝到内核态,之后内核需要遍历所有传递进来的fd,这时如果客户端fd很多 时会导致系统开销很大;
2. 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过setrlimit()、修改宏定义甚至重 新编译内核等方式来提升这一限制,但是这样也会造成效率的降低;
下面给出select多路复用提升socket服务器端效率的伪代码:
#include<>
int main(int argc,char *argv[])
{
int listen_fd;
int judge=0;//这是一个普通的判定标志,用于判断集合fd_set是否已满
int max_fd=0;//监听文件描述符的数量;
fd_set rd_set;//这里暂时先只处理可读文件描述符
listen_fd=socket();
bind(listen_fd, , );
listen(listen_fd, );
int array[__FD_SETSIZE];//定义数组最大可容纳文件描述符数量为FD_SET的最大值
for(int i=0;i<__FD_SETSIZE;i++)
{
array[i]=-1;
}
array[0]=listen_fd;//加入listen_fd,
while(1)
{
FD_ZERO(&rd_set);//清空集合;
for(int i=1;i<__FD_SETSIZE;i++)
{
if(array[i]<0)
continue;
max_fd=array[i]>max_fd?array[i]:max_fd;
FD_SET(array[i],&rd_set);
}//强调一下这个for循环,函数select每次返回都会清空rd_set集合,所以每一次循环都要重新加入所有文件描述符,参数max_fd的值就是集合中最大的文件描述符的值。
rv=select(max_fd+1,&rd_set,NULL,NULL,NULL);//最后一个参数设为NULL,代表select永不超时,如果没有文件描述符响应,会永远阻塞。rv<0,函数select的调用出错,rv==0,代表select函数超时,这里不会发生。至此函数select阻塞,第一次能继续运行下去会是listen_fd发生响应。
if(FD_ISSET(listen_fd/*array[0]*/,&fd_set))//用宏FD_ISSET测试fd_set中相应位是否为1,如果是,进入条件语句。
{
judge=0;
conn_fd=accept();
for(int i=1;i<__FD_SETSIZE;i++)
{
if(array[i]<0)
{
array[i]=conn_fd;
judge=1;
}
}
if(!judge)//fd_set满执行该条件
{
printf("fd_set is full\n");
close(conn_fd);
}
continue;
}
//如果select函数返回,而且响应并不是listen_fd,则响应来自客户端
for(int i=1;i<__FD_SETSIZE;i++)
{
if(ISSET(array[i],&rd_set)
{
read();
write();
//如果读写失败,要将该文件描述符关闭,再从array中删除,并置相应元素为-1;
}
}
}
close(listen_fd);
return 0;
}
select系列函数,宏的工作原理:
先来看看fd_set的大小
#include<stdio.h>
#include<sys/select.h>
int main()
{
printf("%d\n",sizeof(fd_set));
return 0;
}
compile and run:
iot@Public_RPi:~/zhanghang $ gcc test.c
iot@Public_RPi:~/zhanghang $ ./a.out
128
运行结果显示fd_set的大小为128个字节,也就是128byte*8bit=1024bit
fd_set的工作原理就是将加入集合的文件描述符的值对应到fd_set的1024个位的相应位上。
宏FD_ZERO(&rd_set)会将fd_set的所有位全部置为0。
假设两个文件描述符的值分别为7和10,则执行FD_SET(8,&rd_set),FD_SET(10,&rd_set)后,fd_set的第8和10位
置为1(从右往左)。在select函数返回后,假设8号位响应使select返回,而10号位并没有响应,此时select返回
后,只有8号位为1,10号位已经被置为0。因此,每一次while(1)循环,都要将原来监听的文件描述符重新加入到
fd_set中,所以使用一个数组保存原来的文件描述符会使加入更方便。
另外,FD_ISSET(a,&rd_set)用于判断rd_set的a号位是否为1。
FD_CLR(a,&rd_set)表示清除rd_set中的a号位。
最后分析一下select多路复用的缺点:
select缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很⼤
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很⼤
(3)select支持的文件描述符数量太小了,默认是1024