ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux--多路转接之select

2026/9/12 16:46:59 拓冰建站 浏览量
Linux--多路转接之select 其他IO模型阻塞一个接口如果无法完成功能就会一直等待非阻塞一个接口如果无法完成功能就会直接报错返回阻塞与非阻塞通常用于讨论接口特性表示一个函数调用后是否会立即返回同步描述一个任务是由自己完成的异步描述一个任务操作是由其他人完成的同步与异步讨论一个操作完成的方式是由自己完成的还是由别人完成的。几种典型的I/O模型I/O操作的操作过程1.等待i/o就绪 2.拷贝数据阻塞IO一个函数实现i/o操作 若IO未完成则一直等待直到I/O完成后返回优点实现流程思想非常简单缺点阻塞浪费了CPU资源2.非阻塞io一个函数实现IO操作若IO无法立即完成 则立即报错返回通常需要循环操作优点CPU资源利用更加充分缺点需要循环进行流程稍微复杂不够实时。阻塞和非阻塞的差别一个函数若无法立即完成功能的情况下是否会立即返回3.信号驱动io初始化IO 就绪信号IO就绪时信号通知进程进程发起IO操作优点相较于非阻塞更加实时CPU利用更加充分缺点相较于非阻塞操作操作流程更加复杂4.异步IO我自己发起IO操作但是实际的IO过程由其他人完成优点对于资源的利用最为充分缺点流程的控制复杂度最高同步和异步的差别一个任务的完成方式是有自己完成还是由其他人完成5.多路复用/多路转接IO模型功能针对大量描述符进行IO就绪事件监控监控哪个描述符就绪了IO事件单执行流tcp服务器只能与一个客户端通信一次/只能与一个客户端循环通信本质原因因为无法获知那个描述符就绪了io事件,因此只能在固定的流程中发起多种不同的阻塞操作accept,recv,send多路转接IO是实现高并发服务器的关键技术。io就绪事件分类可读事件一个描述符当前有数据可以读接收缓冲区数据量达到高水位标记可写事件一个描述符当前写缓冲区有空闲空间发送缓冲区空闲空间达到高水位标记异常事件一个描述符当前存在异常二、多路复用的具体实现select,poll,epollselect模型功能针对大量描述符进行IO就绪事件监控。函数接口Int select(int maxfd, fd_set*readsetfd_set*writeset,fd_set*excepts, struct timeval*timeout)功能针对大量描述符进行不同的io事件的监控Maxfd:所有要监控的描述符集合中最大的描述符1原因假设此时在内核中我们要监控3号描述符在内核中能监控的最大描述符的数量是有限的默认是1024但是如果我们将这1024全部遍历一遍效率就会很低。但是如果我们只需要将要监控最大描述符1那么后面的剩余的就不遍历极大的提高的遍历效率。Readsetwritesetexceptset描述符集合存放的要监控的描述符要对那个描述符监控什么事件就把那个描述符添加到对应的集合中fd_set{_fds_bits[]}实际上这个描述符集合是一个位图数据结构添加描述符实际上就是将描述符这个数字对应的比特位置1这个位图是通过数组实现的意味着位图的大小是固定的所能监控的描述符数量是有限的取决于_FD_SETSIZE宏大小默认是1024在64位平台下 是8个字节位图一种数据结构存放二进制比特位。为了能够快速获知哪些数据块未使用因此维护一个很大的数据标记哪些使用了哪些未使用数据结构位图存放二进制比特位每个比特位存放一个0/1一个文件存放到磁盘中的时候必须对该文件进行描述描述Inode结构体文件大小文件权限时间属性存储位置.......struct timeval{size_t tv_sec;//存放秒size_t tv_usec;//存放微秒}timeout参数控制select接口的阻塞属性若timeout中数据为0则表示select非阻塞若timeout中数据不为0则表示阻塞多少秒若timeout为空则表示一直阻塞一个是数据0 也就是数值0 一个是NULL阻塞情况下select会阻塞等待集合中有描述符就绪了所要监控的io事件集合中有描述符就绪了事件 select才会返回非阻塞情况下集合中即使没有描述符就绪也会立即返回限时阻塞在限定时间内 若集合中没有买哦舒服就绪就超时返回。select函数返回值表示当前集合中所有集合中共有多少个描述符就绪返回值0表示出错返回值0表示没有描述符就绪等超时了或者非阻塞情况描述符集合操作辅助宏:void FD_CLR(int fd, fd_set *set); 从集合中移除fdint FD_ISSET(int fd, fd_set *set); 判断fd是否在集合中void FD_SET(int fd, fd_set *set); 将fd添加到集合中void FD_ZERO(fd_set *set); 清空集合select使用流程1.定义所要监控的事件集合2.将要监控的描述符根据所要监控的事件添加到不同的集合中3.根据需要决定是否使用timeout设置监控超时时间若需要设置监控超时时间则定义struct timeval结构体变量4.调用select接口开始进行监控1将几个集合拷贝到内核中2内核根据maxfd遍历集合找出所要监控的描述符添加监控①遍历这些描述符看看是否存在描述符已经就绪②若没有就绪的描述符则为描述符添加监控③当有描述符就绪时进程被唤醒④遍历描述符集合从集合中移除未就绪的描述符3监控超时/有描述符就绪时接口调用返回返回时集合中只有就绪描述符5.遍历所有的描述符通过判断描述符是否还在集合中确定是否就绪了事件6.若就绪了事件则根据事件进行不同的处理即可代码实现代码实现11.简单使用编写一个简单的样例认识select的使用及细节通过select对标准输入进行可读监控当触发可读事件进行读操作/* 编写一个简单的样例认识select的使用及细节 通过select对标准输入进行可读监控当触发可读事件进行读操作 */ #includestdio.h #includestring.h #includestdlib.h #includesys/select.h #includeunistd.h #includefcntl.h int main(){ //1.定义事件集合 并对集合进行初始化 fd_set rfds;//定义了一个变量 //定义变量中的数据我们并不知道可能会影响我们后面的操作 //所以就要对集合进行初始化 也就是清空集合 FD_ZERO(rfds);//清空集合 //2.将描述符添加到集合中 FD_SET(0,rfds);//添加描述符0的到集合中 int maxfd0;//所以此时最大描述符 //4.开始监控 while(1){ //3.定义监控超时时间 //放到循环里面就是因为select每次都会清空 struct timeval tv; tv.tv_sec3;//设置3s监控超时时间 tv.tv_usec0; fd_set tmp_rfdsrfds;//每次创建临时集合 //这一步是因为 //如果我门要监控的描述符监控超时 //那么此时就意味着这个描述符就是未就绪的描述符 //select就会将这个我们本要监控的描述符移出去 //select(maxfd1,rfds,wfds,efds,timeout) int retselect(maxfd1,tmp_rfds,NULL,NULL,tv); if(ret0){ perror(select error); continue; } if(ret0){ printf(监控超时,没有描述符就绪\n); continue; } //5.通过判断描述符是否在集合中确定是否描述符就绪了事件 for(int i0;imaxfd;i){ if(FD_ISSET(0,rfds)){ //6.若就绪了事件根据事件进行处理 char buffer[1024]{0}; read(0,buffer,1023); printf(获取可读数据%s\n,buffer); } } } return 0; }使用注意事项1.select接口在调用返回时修改timeout的值因此每次监控需要重置需要超时时间2.select接口咋返回时会修改所监控的描述符集合移除未就绪描述符因此每次监控前都需要重新添加描述符到集合中代码2基于select模型 实现一个但执行流高并发TCP服务器伪代码/* 基于select模型 实现一个但执行流高并发TCP服务器 */ #includestdio.h #includestring.h #includestdlib.h #includesys/select.h #includeunistd.h #includesys/socket.h #includefcntl.h #includearpa/inet.h #includenetinet/in.h #includetime.h #define CHECK_RESULT(ret,msg)\ if((ret)0){perror(msg);return -1}; void init_server_addr(struct sockaddr_in*addr,char*argv[]){ // ./sever 192.168.14.128 9000 const char*ipargv[1]; int16_t portatoi(argv[2]); addr-sin_familyAF_INET; addr-sin_porthtons(port); addr-sin_addr.s_addrinet_adrr(ip); } #define MAX_LISTEN 1024 int main(int argc,char*argv[]){ if(argc!3){ printf(usage:./server ip port\n); return -1; } //创建套接字bind(listen_fd,addr,addrlen) int listen_fdsocket(AF_INET,SOCK_STREAM,0); CHECK_RESULT(listen_fd,创建套接子失败); //为套接字绑定地址信息 struct sockaddr_in addr; init_server_addr(addr,argv); socklen_t addrlensizeof(struct sockaddr_in); int retbind(listen_fd,addr,addrlen); CHECK_RESULT(ret,绑定地址失败); //开始监听 retlisten(listen_fd,MAX_LISTEN); CHECK_RESULT(ret,监听失败); //定义可读事件的描述符集合 fd_set rfds; FD_ZERO(rfds); //将监听套接字添加到集合中并重置maxfd FD_SET(listen_fd,rfds); int maxfdlisten_fd; while(1){ //定义监控超时间结构 struct timeval timeout; timeout.tv_sec3; timeout.tv_usec0; //定义临时集合对临时集合开始监控 fd_set tmprfdsrfds; retselect(maxfd1,rfds,NULL,NULL,timeout); //若出错或超时则continue if(ret0){ perror(监控出错或超时); continue; } //遍历所有描述符找到就绪的描述符 for(int i0;imaxfd;i){ if(FD_ISSET(i,tmprfds)){ // 若就绪的描述符是监听套接字则获取新连接 if(ilisten_fd){ //将获取到的通信套接字添加到可读事件描述符集合中 struct sockaddr_in peer; int newfdaccept(i,peer,addrlen); if(newfd0){ perror(accept error); continue; } FD_set(newfd,rfds);//将新连接添加监控 maxfdmaxfdnewfd?maxfd:newfd; } else{ //若就绪的描述符不是监听套接字 则接收客户端通信数据 并进行响应 char buffer[1024]{0}; retrecv(i,buffer,1023,0); if(ret0){ perror(recv error); close(i); FD_CLR(i,rfds);//移除描述符监控 for(int jmaxfd;j0;j--){ if(FD_ISSET(j,rfds)){ maxfdj; break; } } continue; } printf(客户端数据:%s\n,buffer); } } //若就绪的描述符不是监听套接字 则接收客户端通信数据 并进行响应 } } //关闭套接字 close(listen_fd); return 0; }优缺点总结优点1.监控超时阻塞时间可以精确到微秒2.select跨平台移植性好不仅Linux下可以使用Windows下包括其他平台也都支持缺点1.select所能监控的描述符数量是有限的默认FD_SETSIZE10242.select操作稍显麻烦每次监控都需要重置超时时间监控集合3.select的监控性能偏低内核进行监控的时候会涉及多次描述符集合遍历4.select监控完成之后需要程序员再次进行遍历描述符确定描述符就绪事件适用场景1.适用于监控描述符数量不多甚至只有一个仅用于进行超时控制的场景或者需要支持跨平台的场景。