select高效的原因
首先要知道一个概念,一次I/O分两个部分(①等待数据就绪 ②进行I/O),减少等的比重,增加I/O的比重就可以达到高效服务器的目的。select工作原理就是这个,同时监控多个文件描述符(或者说文件句柄),一旦其中某一个进入就绪状态,就进行I/O操作。监控多个文件句柄可以达到提高就绪状态出现的概率,就可以使CPU在大多数时间下都处于忙碌状态,大大提高CPU的性能。达到高效服务器的目的。
可以理解为select轮询监控多个文件句柄或套接字。
缺点
1、每次进行select都要把文件描述符集fd由用户态拷贝到内核态,这样的开销会很大。
2、实现select服务器,内部要不断对文件描述符集fd进行循环遍历,当fd很多时,开销也很大。
3、select能监控文件描述符的数量有限,一般为1024。(sizeof(fd_set) * 8 = 1024(fd_set内部是以位图表示文件描述符))
文件描述符集
select监控文件描述符时是以文件描述符集的方式进行监控(类似于信号集,信号量集)
类型:fd_set
(内部是以为位图的方式来表示文件描述符的状态,就绪或没有就绪)
关于文件描述符集的四个函数:
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
void FD_CLR(int fd,fd_set *set); ---- 将文件描述符fd从文件描述符集指针set指向的文件描述符集中清除掉
int FD_ISSET(int fd,fd_set *set); ---- 用来判断文件描述符fd是否在文件描述符集指针set指向的文件描述符集中。
- 存在 返回1;
- 不存在 返回0;
void FD_SET(int fd,fd_set*set); ---- 将文件描述符fd设置进文件描述符集指针set指向的文件描述符集中。
void FD_ZERO(int fd,fd_set*set); ----将文件描述符集指针set指向的文件描述符集清0。
select函数
函数功能的实现:通过参数readfds,writefds,execptfds将要监控的文件描述符写入,就可以监控这些文件描述符。
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *execptfds,struct timeval *timeout)
参数
- nfds:表示最大文件描述符+1,用来表示文件描述符的范围。
这个参数有什么用呢?系统迫使我们计算文件描述符的最大值,所为的不过是效率问题,我们在上面分析过,调用select需要不断拷贝转移描述符集fd_set,当数量庞大时,会是一个很大的负担,利用这个最大值就可以避免复制一些并不存在的描述符。 readfds:表示指向读文件描述符集的指针
writefds:表示指向写文件描述符集的指针
execptfds:表示指向错误输出文件描述符集的指针。
参数readfds,writefds,execptfds既是输入参数,又是输出参数。
输入:将要监控的文件描述符传给select
输出:将处于就绪状态的文件描述符返回。 (所以要在每次处理完一就绪事件后要将readfds,writefds,execptfd三个参数重置)timeout:表示超时时间限制。(有三种情况)
timeout也是一种输入输出两用参数,输入表示设定超时时间,输出表示超时时间还剩多少。
timeout是一个timeval结构体类型的指针。
timeval结构如下:
struct timeval {
long tv_sec; /* seconds */ //表示秒
long tv_usec; /* microseconds */ //表示微秒
};
我们可以通过设置上面结构体内两个元素的值来设定select的超时时间。
设置timeout的方法
struct timeval time = {5,0};//第一个值表示秒,第二个值表示微秒。
select(...,&time);//超时时间为5秒
timeout的时间设定有三种情况:
① 上面示例的正常设定,设定一个正常的时间。等待一段时间,返回一次
②将时间设为0,—-表示一直轮询等待。或者说根本不等待,检查完描述符后不管有没有就绪立即返回
③传参时传为NULL,—-表示一直阻塞等待
返回值
select的返回值有三种,
- -1 —– 执行错误
- 0 —– timeout时间到达
- 其他 —– 正确执行,并且有就绪事件到达
文件描述符就绪的条件
虽然跟下面没什么关系,但还是说一下。
读就绪
①该套接字接受缓冲区中的数据字节数大于等于当前缓冲区的低水位标记的大小
②该TCP连接处于读半关闭状态(四次挥手还没有完成)
③有一个套接字错误待处理
④该套接字是一个监听套接字且已完成连接的数目不为0.写就绪
①该套接字发送缓冲区中的数据字节数大于等于当前缓冲区的低水位标记的大小
②该TCP连接处于写半关闭状态(四次挥手还没有完成)
③使用非阻塞式sonnect的套接字已经连接或者connect已经失败
④有一个套接字错误待处理- 错误就绪
一个套接字存在带外数据或处于带外标记。
当某个套接字发生错误时,它将被select标记为既可读又可写。
带外标记,带外数据:也可称加速标记,加速数据,即通信双方一方有重要的事情要通知另外一方。
select服务器(单进程服务器)
这里我只考虑读文件描述符。
实现:通过select函数实现I/O多路复用,同时监控多个文件描述符,达到提高CPU处理实际I/O的比重,减少了等待I/O的比重,提高了CPU效率,是一种高效服务器。
实现逻辑:
准备工作:
①我们需要定义一个数组来保存所有的文件描述符 ——- int gfds[];(如果三种文件描述符都考虑,则设立三个数组)
原因:因为readfds,writefds,execptfds 每次调用select函数,这三个参数的值都会变化,为了保证下次调用select能够正常执行,我们需要利用这个数组来重置这些参数。
② 定义一个值来保存文件描述符的最大值 —– int max_fd; —– 方便我们传参。
逻辑图以及代码
逻辑图是用思维导图(Mindmanager)画的,截图不好截,我就上传到github上了,需要的可以自己下载。
https://github.com/LLZK/Linux_study/tree/master/select_server
阅读代码最好按着逻辑图读,好理解写。
代码:
#include<stdio.h>
#nclude<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/time.h>
#include<unistd.h>
#include<assert.h>
#define _SIZE_ 128
int makesock(const char* ip,const char* port)
{
assert(ip);
assert(port);
int listensock = socket(AF_INET,SOCK_STREAM,0);
if(listensock < 0)
{
perror("socket");
return 2;
}
int opt = 1;
int retset = setsockopt(listensock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if(retset < 0)
{
perror("setsockopt");
return 3;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(port));
addr.sin_addr.s_addr = inet_addr(ip);
if(bind(listensock,(struct sockaddr*)&addr,sizeof(addr)) < 0)
{
perror("bind");
return 4;
}
if(listen(listensock,5) < 0)
{
perror("listen");
return 5;
}
return listensock;
}
int main(int argc,char* argv[])
{
//printf("%d",sizeof(fd_set));
if(argc < 3)
{
printf("Please input:%s [ip] [port]\n",argv[0]);
return 1;
}
int listensock = makesock(argv[1],argv[2]);
int gfds[_SIZE_];
int i = 0;
for(i = 0;i < _SIZE_;i++)
{
gfds[i] = -1;
}
gfds[0] = listensock;
int max_fd = gfds[0];
while(1)
{
fd_set rfds;
FD_ZERO(&rfds);
int j = 0;
for(;j < _SIZE_;j++)
{
if(gfds[j] != -1)
{
FD_SET(gfds[j],&rfds);
}
if(max_fd < gfds[j])
{
max_fd = gfds[j];
}
}
struct timeval timeout = {5,0};
switch(select(max_fd+1,&rfds,NULL,NULL,&timeout))
{
case -1://error
perror("select");
break;
case 0://timeout
printf("...timeout\n");
break;
default:
{
int k = 0;
for(;k < _SIZE_;k++)
{
if(gfds[k] != -1 && FD_ISSET(gfds[k],&rfds))
{
if(gfds[k] == listensock)
{
struct sockaddr_in peer;
int len = 0;
memset(&peer,'\0',sizeof(peer));
int sock = accept(listensock,(struct sockaddr*)&peer,&len);
if(sock < 0)
{
perror("accept");
continue;
}
printf("%s:%d(%d)",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),sock);
int m = 0;
for(;m < _SIZE_;m++)
{
if(gfds[m] == -1)
{
gfds[m] = sock;
break;
}
}
if(m == _SIZE_)
{
printf("gfds have no space!\n");
continue;
}
}
else
{
char buf[1024];
ssize_t s = read(gfds[k],buf,sizeof(buf));
if(s < 0)
{
perror("read");
continue;
}
else if(s == 0)
{
printf("client is quit!\n");
close(gfds[k]);
gfds[k] = -1;
continue;
}
else
{
buf[s] = '\0';
printf("client say# %s\n",buf);
}
}
}
}
break;
}
}
}
close(listensock);
return 0;
}