首页 > 代码库 > 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