linux select函数解析以及示例
背景
linux下的I/O操作定义:在我看来,I/O是指数据流的操作,比如说网络编程的I/O操作,串口的读写等等可以称为I/O操作。在linux系统中一共有下面五种I/O操作模式。
阻塞I/O(blocking I/O)
非阻塞I/O (nonblocking I/O)
I/O复用 (I/O multiplexing)
信号驱动I/O (signal driven I/O (SIGIO))
异步I/O (asynchronous I/O (the POSIX aio_functions))
前四种都是同步I/O,只有最后一种是异步I/O。
本文中只涉及前面三种I/O模式,下面详细分析这三种I/O操作模式。
阻塞I/O
阻塞:进程会一直阻塞,直到数据拷贝完成。应用程序调用一个I/O函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,I/O函数返回成功指示。这个是linux系统默认的I/O下操作模式,也是最常见的I/O操作模式。也就是说,如果你创建了一个套接字,想要使用非阻塞模式,那么你需要进行设置,因为你默认的是阻塞模式。下面会详细讲到。
以recv()函数为例子:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
当你创建了套接字,bind和listen后,以及使用accept连接上后就会使用这个recv函数去等待数据到来,等对方通过socket将数据发送到你的内核,你的内核就会通知你有数据到来,此时你这个函数就会返回,返回的是你接受数据的字节数,否则一直会在阻塞状态等待数据的到来。
非阻塞I/O
当我们告诉内核,如果数据没有到来,你立马给我返回,不用等待数据了。设置成非阻塞的方法如下。
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); //设置成非阻塞模式;
其实非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源
I/O多路复用
先解释下什么是多路复用。前面讲过,阻塞模式是最常用的一种I/O模式,从socket的角度出发,一个client去连接连接一个server端,server端往往需要等待客户端的访问,那么如果此时我们使用阻塞模式的话,如果只使用一个线程的话,如果此时第一个连接过来了,那么你调用了recv()函数,阻塞在等待消息这里,此时如果第二个客户端连接的话,因为你的程序一直在这里等着,无法处理这个连接请求,那么此时你的连接数量是1.那么如果我们使用对线程机制,对每个客户端都使用一个线程,那么如果有大量的客户连接的话,服务端就要创建大量的线程,linux操作系统本身对文件描述符的个数有限制,即使没有限制,大量的线程创建和套接字创建也会消耗linux的资源。此时多路复用就诞生了,在我理解,多路复用就是可以在一个线程中监测多个套接字,比如select,poll,epoll,当这些套接字(文件描述符)中的任意一个进入有数据到来,以上三个函数就会返回,之后进入数据处理状态。
select函数解析
select 函数声明如下
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明、
maxfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。在linux系统中,select的默认最大值为1024。设置这个值的目的是为了不用每次都去轮询这1024个fd,假设我们只需要几个套接字,我们就可以用最大的那个套接字的值加上1作为这个参数的值,当我们在等待是否有套接字准备就绪时,只需要监测maxfd+1个套接字就可以了,这样可以减少轮询时间以及系统的开销。
readfds:首先需要明白,fd_set是什么数据类型,有一点像int,又有点像struct,其实,fd_set声明的是一个集合,也就是说,readfs是一个容器,里面可以容纳多个文件描述符,把需要监视的描述符放入这个集合中,当有文件描述符可读时,select就会返回一个大于0的值,表示有文件可读;
writefds:和readfs类似,表示有一个可写的文件描述符集合,当有文件可写时,select就会返回一个大于0的值,表示有文件可写;
exceptfds 同上面两个参数的意图,用来监视文件错误异常文件。
timeout:这个参数一出来就可以知道,可以选择阻塞,可以选择非阻塞,还可以选择定时返回。当将timeout置为NULL时,表明此时select是阻塞的;当将tineout设置为timeout->tv_sec = 0,timeout->tv_usec = 0时,表明这个函数为非阻塞;当将timeout设置为非0的时间,表明select有超时时间,当这个时间走完,select函数就会返回。
下面我们介绍下操作fd_set的几个宏:
void FD_ZERO(fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
对fd_set的理解:fd_set可以理解为一个集合,那么集合就会有一个数量,在<sys/select.h>总定义了一个常量FD_SETSIZE,默认为1024,也就是说在这个集合内默认最多有1024个文件描述符,但是通常你用不了这么多,你通常只是关心maxfds个描述符。也就是说你现在有maxfds个文件描述符在这个集合里,那么我怎么知道集合里的哪个文件描述符有消息来了呢?你可以将fd_set中的集合看成是二进制bit位,一位代表着一个文件描述符。0代表文件描述符处于睡眠状态,没有数据到来;1代表文件描述符处于准备状态,可以被应用层处理。我觉得select函数可以分下面几步进行理解
-
在开始监测这些描述符时,先将这些文件描述符全部置为0
-
当需要监测的描述符置为1
-
使用select函数监听置为1的文件描述符是否有数据到来
-
当状态为1的文件描述符有数据到来时,此时的状态仍然为1,但是其他状态为1的文件描述如果没有数据到来,那么此时会将这些文件描述符置为0
-
当select函数返回后,可能有一个或者多个文件描述符为1,那么怎么知道是哪个文件描述符准备好了呢?其实select并不会告诉你说,我哪个文件描述符准备好了,他只会告诉你他的那些bit为位哪些是0,哪些是1。也就是说你需要自己用逻辑去判断你要的那个文件描是否准备好了
操作说明
FD_ZERO:将指定集合里面所有的描述符全部置为0,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的
FD_SET:用于在文件描述符集合中增加一个新的文件描述符,将相应的位置置为1
FD_CLR:用来清除集合里面的某个文件描述符
FD_ISSET:用来监测指定的某个描述符是否有数据到来。- 那么假如在我们的程序中有5个客户端已经连接上了服务器,这个时候突然有一条数据过来了。select返回了,但是此时你并不知道是哪个客户发过来的消息,因为你每个客户发过来的消息都是一样重要的。所以你没法去只针对一个套接字使用FD_ISSET,你需要做的是用一个循环去监测(FD_ISSET)到底是哪一个客户发过来的消息,因为如果此时你监测一个套接字的话,其他客户的信息你会丢失。这个也是select的一个缺点,你需要去监测所有的套接字,看看这个套接字到底是谁来的数据。
select函数解析和理解大概就讲到这里了。下面讲一下select的使用以及示例
select的使用以及示例
select的使用流程
- FD_ZERO将指定的fd_set清零
- FD_SET将需要测试的fd加入fd_set
- 调用函数select监测fd_set中的所有fd
- FD_ISSET检查某个fd在函数select调用后相应位是否仍然为1,然后做相应的逻辑处理
Server例子
/*使用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 8100 //连接时使用的端口
#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);
}
printf("sockect_fd = %d\n", sock_fd);
//设置套接口的选项 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);
//超时的设定,这里也可以不需要设置时间,将这个参数设置为NULL,表明此时select为阻塞模式
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);
//如果客户端主动断开连接,会进行四次挥手,会出发一个信号,此时相应的套接字会有数据返回,告诉select,我的客户断开了,你返回-1
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);
}