首页 > 代码库 > I/O多路复用之Select

I/O多路复用之Select

  在Linux下有五种I/O模型,分别为:阻塞、非阻塞、信号驱动、复用I/O和异步I/O.

  而在复用I/O中,比较常见的就是select、poll和epoll.

  本文主要介绍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表示非阻塞,-1表示阻塞.
返回值:为0,说明阻塞等待的时间周期到了,没有所关注的就绪事件发生
            大于0,返回三种文件描述符集合中就绪事件的个数.
            -1,发生错误!

   如果大于0,则说明有事件就绪,便需要从参数中获取就绪的事件进行处理.

  即使我把select函数参数、返回值解释的非常清楚,但对初识select模型的同鞋来说,肯定还是一头雾水!

  我们要想会使用select模型,就必须得了解fd_set(文件描述符集).

二、了解fd_set

  1.传参说明

  fd_set其内部是一个位图,其内部如下:(我们以1字节为例来理解.)

  下标:0123 4567

  数据:0010 0100

  假设我们将上述内容,作为select的reafss参数,代表的含义是:我们关注文件描述符2和5的读事件

  相反,如果select函数返回,其上述内容代表的含义是:文件描述符2与5的读事件已就绪!

  2.操作fd_set

  既然fd_set内部是一个位图,如果我们想设置其值,为了代码的可移植性,我们不能直接用按位与(&)或者按位或(|)来操作,而用内核给我们提供的一系列宏来进行操作:

  void FD_CLR(int fd,fd_set* set);  //将文件描述符集清0

  int FD_ISSET(int fd,fd_set* set);  //判断某个文件描述符在文件描述符集中是否存在,存在返回1,不存在返回0

  void FD_SET(int fd,fd_set* set);  //将fd在文件描述符set中置为1

  void FD_ZERO(int fd,fd_set* set);  //将fd在文件描述符set中清0

三、用select实现一个简单的tcp服务器

  讲了这么多select的用法,您没看懂没关系,注意看我的代码,我尽可能将注释标清楚.  

/**************************************
*文件说明:test.c
*作者:高小调
*创建时间:2017年07月15日 星期六 16时35分26秒
*开发环境:Kali Linux/g++ v6.3.0
****************************************/
#include<stdio.h>
#include<unistd.h>
#include<sys/select.h>
#include<stdlib.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
//自维护的一张文件描述符表(里面的文件描述符都关注读事件)
int fd_list[sizeof(fd_set)];

//创建监听套接字
int get_listen_sock(const char* port){
	//创建套接字
	int sock = socket(AF_INET,SOCK_STREAM,0);
	if(sock < 0){
		perror("socket");
		return -1;
	}
	//绑定ip与端口号
	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(atoi(port));
	local.sin_addr.s_addr = htonl(INADDR_ANY);
	if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){
		perror("bind");
		return -1;
	}
	//设置监听队列
	if(listen(sock,5) < 0){
		perror("listen");
		return -1;
	}
	return sock;
}
//程序主要逻辑
int main(int argc,const char* argv[]){
	if(argc!=2){
		printf("Usage:%s [port]\n",argv[0]);
		return 1;
	}
	int listen_sock = get_listen_sock(argv[1]);
	if(listen_sock < 0){
		printf("Can‘t create listen sock\n");
		return 2;
	}
	//将监听套接字放入文件描述符列表中
	fd_list[0] = listen_sock;
	//初始化文件描述符列表
	for(int i=1; i<sizeof(fd_set); ++i){
		fd_list[i] = -1;
	}
	while(1){
		//找出文件描述符表中最大fd,并将文件描述符内的数据放入fd_set中
		int max_fd = -1;
		fd_set readfds;
		FD_ZERO(&readfds);
		for(int i=0; i<sizeof(fd_set); ++i){
			if(fd_list[i]>-1){
				FD_SET(fd_list[i],&readfds);
				if(fd_list[i] > max_fd){
					max_fd = fd_list[i];
				}
			}
		}
		//处理select模型
		switch(select(max_fd+1,&readfds,NULL,NULL,NULL)){
			case 0:
				//time out
				break;
			case -1:
				//Error
				perror("select");
				break;
			default:
			{
				//已有事件就绪,进行处理
				for(int i=0; i<sizeof(fd_set); ++i){
					int fd = fd_list[i];
					if(fd==fd_list[0] && FD_ISSET(fd,&readfds)){
						//lisen_sock已就绪
						struct sockaddr_in client;
						socklen_t len = sizeof(client);
						int client_sock = accept(fd,(struct sockaddr*)&client,&len);
						if(client_sock < 0){
							perror("accept");
							break;
						}
						printf("[%s]:[%d] login in\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
						//将新客户fd放入fd_listi中
						int j=1;
						for(; j<sizeof(fd_set); ++j){
							if(fd_list[j] < 0){
								fd_list[j] = client_sock;
								break;
							}
						}
						if(j==sizeof(fd_set)){
							//文件描述符表已满
							close(client_sock);
							continue;
						}
					}else if(fd > 0 && FD_ISSET(fd,&readfds)){
						//其它读事件就绪(假设数据长度小于1024)
						char buff[1024];
						ssize_t s = read(fd,buff,sizeof(buff)-1);
						if(s > 0){
							buff[s] = 0;
							printf("%s",buff);
							fflush(stdout);
						}else if(s==0){
							printf("some one is out\n");
							fd_list[i] = -1;
							close(fd);
							continue;
						}else{
							perror("read");
							continue;
						}
					}
				}
			}
				break;
		}
	}
	return 0;
}

 四、select模型总结

  唯一的优点:即多路复用I/O的优点:同时检测多个文件描述符状态,相对于阻塞、非阻塞模型等其他I/O模型来说,非常高效

  缺点:1.因为其采用了位图结构,因此select同时检测文件描述符的个数是有上限的,因此随着连接数不断的上升,其性能会急剧下降!

    2.每次调用select,都需要将fd集合从用户态拷贝到内核态,在fd比较多时,开销非常大!

    3.同时,每次调用select,也需要内核遍历传递进来的所有fd,在fd比较多时,开销也非常大!

    4.select函数的参数为输入输出型,因此每次调用select时必须重新进行赋值,需要轮询一遍文件描述符表.

    5.每次select返回后,又需要进行轮询查找就绪文件描述符,比较费时.

I/O多路复用之Select