Linux--多路转接之select

其他IO模型:

阻塞:一个接口如果无法完成功能,就会一直等待

非阻塞:一个接口如果无法完成功能,就会直接报错返回

阻塞与非阻塞:通常用于讨论接口特性,表示一个函数调用后是否会立即返回

同步:描述一个任务是由自己完成的

异步:描述一个任务操作是由其他人完成的

同步与异步:讨论一个操作完成的方式,是由自己完成的还是由别人完成的。

几种典型的I/O模型:

I/O操作的操作过程:1.等待i/o就绪 2.拷贝数据

  1. 阻塞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,epoll

select模型:

功能:针对大量描述符进行IO就绪事件监控。

函数接口:

Int select(int maxfd, fd_set*readset,fd_set*writeset,fd_set*excepts, struct timeval*timeout)

功能:针对大量描述符进行不同的io事件的监控

Maxfd:所有要监控的描述符集合中最大的描述符+1

原因:假设此时在内核中,我们要监控3号描述符,在内核中能监控的最大描述符的数量是有限的,默认是1024,但是如果我们将这1024全部遍历一遍效率就会很低。但是如果我们只需要将要监控最大描述符+1,那么后面的剩余的就不遍历,极大的提高的遍历效率。

Readset&writeset&exceptset:描述符集合,存放的要监控的描述符要对那个描述符监控什么事件,就把那个描述符添加到对应的集合中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); 从集合中移除fd

int 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.若就绪了事件则根据事件进行不同的处理即可

代码实现:

代码实现1:

1.简单使用:

编写一个简单的样例,认识select的使用及细节

通过select对标准输入进行可读监控,当触发可读事件,进行读操作

cs 复制代码
/*
编写一个简单的样例,认识select的使用及细节
    通过select对标准输入进行可读监控,当触发可读事件,进行读操作

*/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/select.h>
#include<unistd.h>
#include<fcntl.h>

int main(){

    //1.定义事件集合 并对集合进行初始化
    fd_set rfds;//定义了一个变量 
    //定义变量中的数据我们并不知道可能会影响我们后面的操作 
    //所以就要对集合进行初始化 也就是清空集合
    FD_ZERO(&rfds);//清空集合
    //2.将描述符添加到集合中
    FD_SET(0,&rfds);//添加描述符0的到集合中
    int maxfd=0;//所以此时最大描述符
    
    //4.开始监控
    while(1){
    //3.定义监控超时时间

    //放到循环里面就是因为select每次都会清空
    struct timeval tv;
    tv.tv_sec=3;//设置3s监控超时时间
    tv.tv_usec=0;
        fd_set tmp_rfds=rfds;//每次创建临时集合
        //这一步是因为
        //如果我门要监控的描述符监控超时
        //那么此时就意味着这个描述符就是未就绪的描述符 
        //select就会将这个我们本要监控的描述符移出去
        //select(maxfd+1,rfds,wfds,efds,timeout)
        int ret=select(maxfd+1,&tmp_rfds,NULL,NULL,&tv);
        if(ret<0){
            perror("select error");
            continue;
        }
        if(ret==0){
            printf("监控超时,没有描述符就绪\n");
            continue;
        }
        //5.通过判断描述符是否在集合中,确定是否描述符就绪了事件
        for(int i=0;i<=maxfd;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服务器

伪代码:

cs 复制代码
/*
    基于select模型 实现一个但执行流高并发TCP服务器

*/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/select.h>
#include<unistd.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<time.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*ip=argv[1];
        int16_t port=atoi(argv[2]);
        addr->sin_family=AF_INET;
        addr->sin_port=htons(port);
        addr->sin_addr.s_addr=inet_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_fd=socket(AF_INET,SOCK_STREAM,0);
    CHECK_RESULT(listen_fd,"创建套接子失败");
    //为套接字绑定地址信息
    struct sockaddr_in addr;
    init_server_addr(&addr,argv);
    socklen_t addrlen=sizeof(struct sockaddr_in);

    int ret=bind(listen_fd,&addr,addrlen);
    CHECK_RESULT(ret,"绑定地址失败");
    //开始监听
    ret=listen(listen_fd,MAX_LISTEN);
    CHECK_RESULT(ret,"监听失败");
    //定义可读事件的描述符集合
    fd_set rfds;
    FD_ZERO(&rfds);
    //将监听套接字,添加到集合中,并重置maxfd
    FD_SET(listen_fd,&rfds);
    int maxfd=listen_fd;
    while(1){
        //定义监控超时间结构
        struct timeval timeout;
        timeout.tv_sec=3;
        timeout.tv_usec=0;

        //定义临时集合,对临时集合开始监控
        fd_set tmprfds=rfds;
        ret=select(maxfd+1,&rfds,NULL,NULL,&timeout);
        //若出错,或超时,则continue
        if(ret<=0){
            perror("监控出错或超时");
            continue;
        }
        //遍历所有描述符,找到就绪的描述符
        for(int i=0;i<=maxfd;i++){
            if(FD_ISSET(i,&tmprfds)){
            // 若就绪的描述符是监听套接字,则获取新连接
            if(i==listen_fd){
            //将获取到的通信套接字添加到可读事件描述符集合中
            struct sockaddr_in peer;
            int newfd=accept(i,&peer,&addrlen);
            if(newfd<0){
                perror("accept error");
                continue;
            }
            FD_set(newfd,&rfds);//将新连接添加监控
            maxfd=maxfd>newfd?maxfd:newfd;
            }
            else{
            //若就绪的描述符不是监听套接字 则接收客户端通信数据 并进行响应
            char buffer[1024]={0};
            ret=recv(i,buffer,1023,0);
            if(ret<=0){
                perror("recv error");
                close(i);
                FD_CLR(i,&rfds);//移除描述符监控
                for(int j=maxfd;j>=0;j--){
                    if(FD_ISSET(j,&rfds)){
                        maxfd=j;
                        break;
                    }
                }
                continue;
            }
            printf("客户端数据:%s\n",buffer);
            }
           
            } 
       
        //若就绪的描述符不是监听套接字 则接收客户端通信数据 并进行响应
        }

       
    }
    //关闭套接字
    close(listen_fd);
    return 0;
}

优缺点总结:

优点:

1.监控超时阻塞时间,可以精确到微秒

2.select跨平台移植性好(不仅Linux下可以使用,Windows下包括其他平台也都支持)

缺点:

1.select所能监控的描述符数量是有限的,默认FD_SETSIZE=1024

2.select操作稍显麻烦:每次监控都需要重置超时时间,监控集合

3.select的监控性能偏低:内核进行监控的时候,会涉及多次描述符集合遍历

4.select监控完成之后需要程序员再次进行遍历描述符,确定描述符就绪事件

适用场景:

1.适用于监控描述符数量不多,甚至只有一个(仅用于进行超时控制)的场景,或者需要支持跨平台的场景。

相关推荐
梁辰兴1 小时前
软件工程:软件维护的副作用
数据库·软件工程·梁辰兴·控制方法·软件维护的副作用·副作用类型·副作用原因
这个DBA有点耶1 小时前
MySQL 8.0.20移除了Block Nested Loop,之前学的JOIN优化知识还适用吗?
数据库·mysql·架构
nvvas2 小时前
Java 入门(四):数组——数据的集合管理
java·开发语言
吉甫作诵2 小时前
Redis 常用命令大全:11 大类命令速查手册
运维·数据库·redis·缓存·nosql
2501_933670792 小时前
库存管理分析岗校招能力模型:SQL、库存周转、补货预测怎么准备
数据库
Lambert2812 小时前
AgentScope Java 从零(07):官方有权限引擎,我却在工具里写了个 if
java·后端·ai编程
Kyrie_kk2 小时前
Java--ThreadLocal线程副本(一)
java·后端
属于自己的天空3 小时前
不用再手动查表结构了:配好 MCP,Claude Code 自己读数据库生成代码
数据库·后端
Y3815326623 小时前
SERP 数据清洗实战:字段标准化、日期解析与去重键
开发语言·数据库·python·python数据库