前面写了关于IO的5种常见模型:五种基本常见IO
今天,我们来学习一下select系统调用。
**select作用:让程序监视多个文件描述符的状态变化.** |
函数原型:
#include <sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数:
nfds:需要监视的最大文件描述符+1
readfds:读文件描述符集合
writefds:写文件描述符集合
exceptfds:异常文件描述符集合
timeout:表示超时时间.若设置为0:表示非阻塞;设置为NULL:表示阻塞,知道某个文件描述符就绪;设置为大于0的值,表示等待的时间,若在等待的这段时间内有文件描述符就绪了,那么就立即返回.若在这段时间内没有就会的文件描述符,也会在时间结束时返回(超时返回).
关于timeval结构体:
struct timeval
{
_time_t tv_sec; //单位为秒
_suseconds_t tv_usec; //单位为微秒
};
关于select的返回值:
- 执行成功且返回值大于0,返回值即表示就绪的文件描述符的个数.
- 执行成功但是返回值为0,那么就表示所有的文件描述符都没有就绪.
- 当有错误发生时,就返回-1,并且错误原因errno中.
研究select的执行过程:
对于fd_set到底有多大,我们可以在程序中进行简单的测试.
所以在这里我们只取其1个字节(8bit)进行模拟select执行过程.
- 首先进行初始化:FD_ZERO(&set);(set表示:0000 0000)
- 若fd = 3,执行FD_SET(fd,&set)后,set为:0000 1000(注意:是从0开始数的)
- 再加入fd = 5,FD_SET(fd,&set);fd = 2,FD_SET(fd,&set)后set表示:0010 1100
- 此时系统函数select执行:select(5+1,&set,NULL,NULL,NULL)设置为阻塞等待.
- 若fd = 3 和 fd = 5都发生了可读事件.那么select就会返回2.并且此时set表示为:0010 1000.注意:此时没有发生任何事件的fd = 2就会被清空,可是如果2号文件描述符并没有关闭,还需要进行监视,那么就要重新设置set
关于fd_set:就是设置文件描述符的相关属性
在操作系统内核中,fd_set其实是一个位图.可以根据位图对应的位来表示监视的文件描述符的状态.
下面是fd_set常见的几个接口:
void FD_CLR(int fd,fd_set *set); //清除集合set中的fd位 void FD_ISSET(int fd,fd_set *set); //判断set集合中的fd位是否为真 void FD_SET(int fd,fd_set *set); //将set集合中的fd位进行设置 void FD_ZERO(fd_set *set); //用来清除set集合中的全部位 |
select就绪条件:
读就绪:
- socket内核中,接受缓冲区有一个阈值,当接受缓冲区的字节数大于这个阈值时就可以进行无阻塞的读取.
- socket的TCP通信中,对端关闭连接,若依旧对socket读,就返回0;
- 监听的socket上有新的连接请求.(可理解为有数据过来要进行读取)
- socket上有未处理的请求.
写就绪:
- socket内核中,发送缓冲区也有一个空闲的阈值,当发送缓冲区的空闲区大于这个阈值时,就可以进行无阻塞的写,并且返回值大于0;
- 当socket的写操作被关闭时,对这个写操作被关闭的socket进行写操作,就会触发一个SIGPIPE信号.
- socket使用非阻塞connect连接成功或失败之后.
使用select来监视标准输入输出:
代码如下:
#include<stdio.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/time.h>
int main()
{
struct timeval t;
while(1)
{
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(0,&read_fds);
//每次都要初始化,当文件描述符变多时,将该初始化放在外面,就会存在有的描述符可能会越来越少.
t.tv_sec = 2;
t.tv_usec = 20;
printf(">");
fflush(stdout);
int ret = select(1,&read_fds,NULL,NULL,&t);
//select:第一个参数表示有效的文件描述符的最大值+1
//返回的是有效的描述符的个数
//返回0,表示超时
//返回-1,表示出现了致命的错误
if(ret == 0)
{
//如果超时了就会返回0
printf("ret == 0\n");
}
if(ret < 0)
{
perror("select");
return 1;
}
if(FD_ISSET(0,&read_fds))
{
//0号描述符读就绪
char buf[1024] = {0};
ssize_t read_size = read(0,buf,sizeof(buf) - 1);
if(read_size < 0)
{
perror("read");
continue;
}
if(read_size == 0)
{
printf("read done\n");
return 0;
}
printf("stdout:%s",buf);
}
}
return 0;
}
执行结果如下:
可以看出:超时后会立即返回.
使用select实现一个基于TCP的回显服务器:(要求这个服务器可以同时监控多个客户端)
服务器的代码如下:
若想看所有的服务器+客户端的代码:请戳:select的相关代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
//获取最大的文件描述符
typedef struct fds
{
fd_set fd;
int max_fd; //当前文件描述符中最大的文件描述符
}fds;
void FD_SET_init(fds *set)
{
if(set == NULL)
{
return;
}
set->max_fd = -1;
FD_ZERO(&set->fd);
}
void Set_Add(fds *set,int fd)
{
if(set == NULL)
{
return;
}
FD_SET(fd,&set->fd);
if(fd > set->max_fd)
{
set->max_fd = fd;
}
}
void Del_Set(fds *set,int fd)
{
if(set == NULL)
{
return;
}
FD_CLR(fd,&set->fd);
int max = -1;
int i = 0;
for(i = 0;i <= set->max_fd;++i)
{
if(!FD_ISSET(i,&set->fd))
{
continue;
}
if(max < i)
{
max = i;
}
}
set->max_fd = max;
}
int start_server(const char* ip,short port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return -1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
//绑定
int ret_bind = bind(sock,(sockaddr*)&addr,sizeof(addr));
if(ret_bind < 0)
{
perror("bind");
return -1;
}
//将服务器变为被动形式,进行监听
int ret_listen = listen(sock,5);
if(ret_listen < 0)
{
perror("listen");
return -1;
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:./select_server [ip] [port]\n");
return 1;
}
//初始化服务器
int ret = start_server(argv[1],atoi(argv[2]));
if(ret < 0)
{
printf("start failed\n");
return 1;
}
printf("server success\n");
//进入循环事件
fds rfd;
FD_SET_init(&rfd);
Set_Add(&rfd,ret);
while(1)
{
//为了保证accept返回的也可以添加到读就绪中.
fds fd = rfd;
//原来写的服务器的阻塞等待是由read和write完成的.现在使用select来完成这个过程.
//该服务器仍是属于回显服务器,接收到什么就回复什么
int ret_sel = select(fd.max_fd + 1,&fd.fd,NULL,NULL,NULL);
if(ret_sel < 0)
{
perror("select");
continue;
}
if(ret_sel == 0)
{
printf("timeout\n");
continue;
}
//说明现在有就绪的文件描述符
if(FD_ISSET(ret,&fd.fd))
{
//并且就绪的文件描述符是ret读就绪
//此时就要调用accept,获取连接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int ret_accept = accept(ret,(struct sockaddr*)&peer,&len);
if(ret_accept < 0)
{
perror("accept");
continue;
}
//如果ret就绪,就要将它添加到读继续中
Set_Add(&rfd,ret_accept);
printf("client %d id connected\n",ret_accept);
}
else //这里有坑!!!!!!,在后面进行坑的讲解
{
//此时返回的是accept的就绪
int i = 0;
char buff[1024] = {0};
for(i = 0;i <= fd.max_fd;++i)
{
if(!FD_ISSET(i,&fd.fd))
{
continue;
}
//在这儿不使用循环读写,因为等待的过程都是有select完成
ssize_t read_size = read(i,buff,sizeof(buff) - 1);
if(read_size < 0)
{
perror("read");
continue;
}
if(read_size == 0)
{
//如果读完了就要关闭该文件描述符
printf("%dread done,goodbye\n",i);
close(i);
Del_Set(&rfd,i);
}
else
{
printf("文件描述符%d,说%s\n",i,buff);
write(i,buff,strlen(buff));
}
}//end else(for)
}//end else
}//end while
return 0;
}
上述代码中有个地方需要注意一下:当select返回的结果大于0时,就说明有就绪的文件描述符.那么接下来就要进行判断时socket就绪要进行accept还是accept就绪要进行读写.在程序中我们使用了if-else语句.当socket就绪要进行accept时就进入if条件中,而
accept就绪要进行读写时则进入else条件.那么如若此时就绪的文件描述符中这两种情况都同时包含,那么首先会进入if条件中执行,那么accept就绪要进行读写的文件描述符就只能等待下次去执行了,这样并不会出现错误,只是可能会效率比较低.但是能保证所有的文件描述符一定会被处理.这样的处理方式叫做水平触发,与之相对的还有一个边缘触发.在后面再进行讲解.
select的缺点(就是epoll的优点):
1. 每次调用select,都要手动设置fd_set的集合,使用很比方便.
2.每次调用select,都要把fd_set集合(3个集合,读,写,异常)从用户态拷贝到内核态,那么如果频繁的调用select,开销就会很大.同样在内核和用户要遍历fd_set该集合,已返回就绪的文件描述符,同样是一种很大的开销
3.select的文件描述符数量太小.